diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..1963b6f5f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,17 @@
+name: CI
+on: [push, pull_request, workflow_dispatch]
+jobs:
+ Test-Git-Meta:
+ 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
+ - 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/.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/.travis.yml b/.travis.yml
deleted file mode 100644
index 4a2ae80b0..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-language: node_js
-node_js:
- - "6.9.1"
-
-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:
- - npm install
-
-script:
- - npm test
-
-notifications:
- email: false
diff --git a/README.md b/README.md
index 14c9498d9..ab951ece4 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@
+[](https://travis-ci.org/twosigma/git-meta)
+
# What is git-meta?
Git-meta allows developers to work with extremely large codebases --
diff --git a/doc/architecture.md b/doc/architecture.md
index 71c143e81..20ba9dc5a 100644
--- a/doc/architecture.md
+++ b/doc/architecture.md
@@ -108,23 +108,29 @@ 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)
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, 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 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).
+- 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
+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 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
@@ -292,13 +298,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.
@@ -322,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
@@ -341,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',
@@ -365,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] |
`-----------------------,
@@ -381,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] |
`-----------------------,
@@ -452,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
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/.gitignore b/node/.gitignore
index 25c74d353..de9a96fbc 100644
--- a/node/.gitignore
+++ b/node/.gitignore
@@ -1,2 +1,5 @@
/node_modules/
/coverage
+/yarn/
+.vscode
+
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/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/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/cmd/add.js b/node/lib/cmd/add.js
index 7aeec8df2..19709f90f 100644
--- a/node/lib/cmd/add.js
+++ b/node/lib/cmd/add.js
@@ -60,6 +60,22 @@ 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(["-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",
@@ -69,8 +85,9 @@ 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: "+",
+ nargs: "*",
type: "string",
help: "the paths to add",
});
@@ -84,15 +101,44 @@ 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 PrintStatusUtil = require("../util/print_status_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 Add.stagePaths(repo, paths, args.meta);
+ 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 = PrintStatusUtil.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);
+ return;
+ }
+ }
+ else {
+ userPaths = args.paths.map(filename => {
+ return GitUtil.resolveRelativePath(workdir, cwd, filename);
+ });
+ }
+ yield Add.stagePaths(repo, userPaths, args.meta, args.update, args.verbose);
});
diff --git a/node/lib/cmd/add_submodule.js b/node/lib/cmd/add_submodule.js
index e9c97e117..b9e73124c 100644
--- a/node/lib/cmd/add_submodule.js
+++ b/node/lib/cmd/add_submodule.js
@@ -78,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) {
@@ -121,7 +122,10 @@ 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.`);
}
+
+ //Run post-add-submodule hook with submodule names which added successfully.
+ yield Hook.execHook(repo, "post-add-submodule", [args.path]);
});
diff --git a/node/lib/cmd/checkout.js b/node/lib/cmd/checkout.js
index 06fcd736d..fb0b20544 100644
--- a/node/lib/cmd/checkout.js
+++ b/node/lib/cmd/checkout.js
@@ -51,16 +51,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",
@@ -75,6 +81,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.
*
@@ -83,10 +123,14 @@ 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");
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"];
@@ -94,13 +138,41 @@ exports.executeableSubcommand = co.wrap(function *(args) {
newBranch = newBranchNameArr[0];
}
const repo = yield GitUtil.getCurrentRepo();
+ const headId = yield repo.getHeadCommit();
+ const zeroSha = "0000000000000000000000000000000000000000";
+ const oldHead = headId === null ? zeroSha : headId.id().tostrS();
// 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 (null === op.commit && !op.checkoutFromIndex && null === op.newBranch) {
+ throw new UserError(`pathspec '${committish}' did not match any \
+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;
+ }
// If we're already on this branch, note it and exit.
@@ -131,9 +203,18 @@ exports.executeableSubcommand = co.wrap(function *(args) {
// In this case, we're not making a branch; just let the user know what
// we checked out.
- console.log(`Checked out ${colors.green(args.committish)}.`);
+ process.stderr.write(`Checked out ${colors.green(committish)}.\n`);
}
+ // 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").
+
+ yield Hook.execHook(repo, "post-checkout",
+ [oldHead, newHead, "1"]);
// 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
new file mode 100644
index 000000000..725a99bf2
--- /dev/null
+++ b/node/lib/cmd/cherry_pick.js
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+"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.
+ */
+
+/**
+ * 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",
+ });
+ parser.addArgument(["-n", "--no-commit"], {
+ action: "storeConst",
+ constant: true,
+ 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,
+ // 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`,
+ });
+};
+
+
+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`.
+ *
+ * @async
+ * @param {Object} args
+ * @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();
+
+ if (args.continue + args.abort > 1) {
+ throw new UserError("Cannot use continue and abort together.");
+ }
+
+ if (args.continue) {
+ if (args.commit.length) {
+ 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 (args.commit.length) {
+ throw new UserError("Cannot specify a commit with '--abort'.");
+ }
+ yield CherryPickUtil.abort(repo);
+ return; // RETURN
+ }
+
+ if (!args.commit.length) {
+ throw new UserError("No commit to cherry-pick.");
+ }
+
+ // TOOD: check if we are mid-rebase already
+
+ const commits = yield exports.resolveRange(repo, args.commit);
+ const result = yield CherryPickUtil.cherryPick(repo, commits);
+
+ 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");
+ }
+});
diff --git a/node/lib/cmd/cherrypick.js b/node/lib/cmd/cherrypick.js
deleted file mode 100644
index a00ec89bf..000000000
--- a/node/lib/cmd/cherrypick.js
+++ /dev/null
@@ -1,100 +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");
-
-/**
- * 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);
- }
- }
-});
diff --git a/node/lib/cmd/commit-shadow.js b/node/lib/cmd/commit-shadow.js
new file mode 100644
index 000000000..9be8e8bc2
--- /dev/null
+++ b/node/lib/cmd/commit-shadow.js
@@ -0,0 +1,134 @@
+/*
+ * 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,
+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.
+ *
+ * @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",
+ action: "append",
+ required: true,
+ help: "commit message for shadow commits",
+ });
+ parser.addArgument(["-e", "--epoch-timestamp"], {
+ required: false,
+ action: "storeConst",
+ constant: true,
+ defaultValue: false,
+ 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",
+ });
+ parser.addArgument(["-s", "--include-subrepos"], {
+ type: "string",
+ required: false,
+ nargs: "+",
+ help: "only include specified sub-repos",
+ });
+};
+
+/**
+ * Execute 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 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,
+ message,
+ incrementTimestamp,
+ false,
+ args.include_untracked,
+ includedSubrepos,
+ false);
+ if (null === result) {
+ const head = yield repo.getHeadCommit();
+ console.log(head.id().tostrS());
+ }
+ else {
+ console.log(result.metaCommit);
+ }
+});
diff --git a/node/lib/cmd/commit.js b/node/lib/cmd/commit.js
index 8f5451c8a..c1e91c40c 100644
--- a/node/lib/cmd/commit.js
+++ b/node/lib/cmd/commit.js
@@ -64,24 +64,16 @@ exports.configureParser = function (parser) {
});
parser.addArgument(["-m", "--message"], {
type: "string",
- defaultValue: null,
+ action: "append",
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",
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.`,
});
@@ -92,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",
@@ -116,40 +115,55 @@ 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();
+ const message = args.message ? args.message.join("\n\n") : null;
yield Commit.doCommitCommand(repo,
cwd,
- args.message,
- args.meta,
+ message,
args.all,
args.file,
args.interactive,
- GitUtil.editMessage);
+ args.no_verify,
+ true);
+ yield Hook.execHook(repo, "post-commit");
});
const doAmend = co.wrap(function *(args) {
- const Commit = require("../util/commit");
- const GitUtil = require("../util/git_util");
- 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;
if (usingPaths) {
throw new UserError("Paths not supported with amend yet.");
}
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,
- process.cwd(),
- args.message,
- args.meta,
+ cwd,
+ message,
args.all,
args.interactive,
- args.no_edit ? null : GitUtil.editMessage);
+ args.no_verify,
+ !args.no_edit);
+ yield Hook.execHook(repo, "post-commit");
});
/**
@@ -178,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
}
diff --git a/node/lib/cmd/forward.js b/node/lib/cmd/forward.js
index 7c2e9fdf9..48f131cab 100644
--- a/node/lib/cmd/forward.js
+++ b/node/lib/cmd/forward.js
@@ -30,53 +30,7 @@
*/
"use strict";
-const ArgParse = require("argparse");
-const assert = require("chai").assert;
-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) {
-
- 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");
- },
- };
-};
-
-/**
- * @property {Set} set of commands to forward
- */
-exports.forwardedCommands = new Set([
- "branch",
- "fetch",
- "remote",
- "tag",
-]);
+const co = require("co");
/**
* Forward the specified `args` to the Git command having the specified `name`.
@@ -87,17 +41,22 @@ exports.forwardedCommands = new Set([
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 = [];
+
+ const rootDir = GitUtilFast.getRootGitDirectory();
+ if (rootDir) {
+ gitArgs.push("-C", rootDir);
+ }
+ gitArgs.push(name);
- const gitArgs = [
- "-C",
- GitUtil.getRootGitDirectory(),
- name,
- ].concat(args);
try {
- yield ChildProcess.spawn("git", gitArgs, {
+ yield ChildProcess.spawn("git", gitArgs.concat(args), {
stdio: "inherit",
- shell: true,
});
}
catch (e) {
diff --git a/node/lib/cmd/merge.js b/node/lib/cmd/merge.js
index d17779b9d..b55407f72 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.
@@ -58,7 +59,13 @@ exports.configureParser = function (parser) {
parser.addArgument(["-m", "--message"], {
type: "string",
help: "commit message",
- required: true,
+ action: "append",
+ 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",
@@ -75,9 +82,28 @@ 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"
+ 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 +121,103 @@ exports.executeableSubcommand = co.wrap(function *(args) {
const colors = require("colors");
- const Merge = require("../util/merge");
- const GitUtil = require("../util/git_util");
- const StatusUtil = require("../util/status_util");
- 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 = Merge.MODE;
+ const MODE = MergeCommon.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.message && args.message_file) {
+ throw new UserError("Cannot use -m and -F 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 + 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.");
+ }
+
+ 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);
- const commitish = yield GitUtil.resolveCommitish(repo, args.commit);
+ 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
+ }
+ let commitName = args.commit;
+ if (null === commitName) {
+ commitName = yield GitUtil.getCurrentTrackingBranchName(repo);
+ }
+ if (null === commitName) {
+ throw new UserError("No remote for the current branch.");
+ }
+ 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 '${commitName}'
+
+# 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 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);
+ }
+
+ let message = args.message ? args.message.join("\n\n") : null;
+ if (args.message_file) {
+ message = yield fs.readFile(args.message_file, "utf8");
+ }
+
const commit = yield repo.getCommit(commitish.id());
- yield Merge.merge(repo, status, commit, mode, args.message);
+ const result = yield MergeUtil.merge(repo,
+ null,
+ commit,
+ mode,
+ Open.SUB_OPEN_OPTION.FORCE_OPEN,
+ doNotRecurse,
+ 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
+ // 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/merge_bare.js b/node/lib/cmd/merge_bare.js
new file mode 100755
index 000000000..69c652a0a
--- /dev/null
+++ b/node/lib/cmd/merge_bare.js
@@ -0,0 +1,169 @@
+/*
+ * 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");
+const fs = require("fs-promise");
+
+/**
+ * This module contains methods for implementing the `merge-bare` 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",
+ action: "append",
+ help: "commit message",
+ required: false,
+ });
+ parser.addArgument(["-F", "--message-file"], {
+ type: "string",
+ help: "commit message file name",
+ required: false,
+ });
+
+ parser.addArgument(["ourCommit"], {
+ type: "string",
+ help: "our side commitish to merge",
+ });
+ parser.addArgument(["theirCommit"], {
+ type: "string",
+ help: "their side commitish to merge",
+ });
+
+ parser.addArgument(["--no-ff"], {
+ help: "create a merge commit even if fast-forwarding is possible",
+ 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",
+ });
+};
+
+/**
+ * Execute the `merge_bare` 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 MergeUtil = require("../util/merge_util");
+ const MergeCommon = require("../util/merge_common");
+ const GitUtil = require("../util/git_util");
+ 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 :
+ MergeCommon.MODE.NORMAL;
+ let ourCommitName = args.ourCommit;
+ let theirCommitName = args.theirCommit;
+ if (null === ourCommitName || null === theirCommitName) {
+ throw new UserError("Two 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 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);
+ }
+
+ let message = args.message ? args.message.join("\n\n") : null;
+ 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() {};
+ const result = yield MergeUtil.merge(repo,
+ ourCommit,
+ theirCommit,
+ mode,
+ Open.SUB_OPEN_OPTION.FORCE_BARE,
+ doNotRecurse,
+ message,
+ noopEditor);
+ if (null !== result.errorMessage) {
+ throw new UserError(result.errorMessage);
+ }
+ if (null !== result.metaCommit) {
+ console.log(result.metaCommit);
+ }
+});
diff --git a/node/lib/cmd/open.js b/node/lib/cmd/open.js
index 2ea59b575..74bcb0e6b 100644
--- a/node/lib/cmd/open.js
+++ b/node/lib/cmd/open.js
@@ -34,7 +34,12 @@
* This module is the entry point for the `open` command.
*/
-const co = require("co");
+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");
/**
* help text for the `open` command
@@ -65,22 +70,112 @@ 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, 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, commit) {
+ assert.instanceOf(repo, NodeGit.Repository);
+
+ args.path = Array.from(new Set(args.path));
+ if (args.path.length > 0) {
+ 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));
+ }
+ if (args.c === null) {
+ throw new UserError(
+ "Please supply a submodule to open, or -c $commitish");
+ }
+ 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);
+});
+
+/**
+ * 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`.
*
* @param {Object} args
* @param {String} args.path
+ * @param {String} args.c
*/
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");
const SubmoduleFetcher = require("../util/submodule_fetcher");
const SubmoduleUtil = require("../util/submodule_util");
@@ -89,32 +184,56 @@ 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 subsToOpen = yield SubmoduleUtil.resolveSubmoduleNames(workdir,
- cwd,
- subs,
- args.path);
- const index = yield repo.index();
+ const {commit, index} = yield getCommitAndIndex(repo, args.c);
+
+ const subs = Object.keys(
+ yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index)
+ );
+
+ const paths = yield parseArgs(repo, args, commit);
+ const subsToOpen = yield SubmoduleUtil.resolveSubmodules(workdir,
+ cwd,
+ subs,
+ paths,
+ !args.force);
+ const subNames = Object.keys(subsToOpen);
const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index,
- subsToOpen);
- const head = yield repo.getHeadCommit();
- const fetcher = new SubmoduleFetcher(repo, head);
+ subNames);
+ const fetcher = new SubmoduleFetcher(repo, commit);
let failed = false;
+ let subsOpenSuccessfully = [];
const openSubs = new Set(yield SubmoduleUtil.listOpenSubmodules(repo));
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.`);
+ 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
+ }
+
+ 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,
@@ -122,7 +241,13 @@ 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[idx],
+ templatePath,
+ args.half);
+ subsOpenSuccessfully.push(name);
+ console.log(`Finished opening ${colors.blue(name)}.`);
}
catch (e) {
if (e instanceof UserError) {
@@ -134,9 +259,13 @@ Opening ${colors.blue(name)} on ${colors.green(shas[index])}.`);
throw e;
}
}
- console.log(`Finished opening ${colors.blue(name)}.`);
});
- yield DoWorkQueue.doInParallel(subsToOpen, opener, 30);
+ yield DoWorkQueue.doInParallel(subNames, opener, 10);
+
+ // Make sure the index entries are updated in case we're in sparse mode.
+ if (!args.half) {
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+ }
if (failed) {
process.exit(1);
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..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)`,
+ });
};
/**
@@ -101,25 +113,39 @@ 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)) || {};
+
+ // 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.remoteName || "origin";
-
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/cmd/rebase.js b/node/lib/cmd/rebase.js
index 27d252543..dced3fb76 100644
--- a/node/lib/cmd/rebase.js
+++ b/node/lib/cmd/rebase.js
@@ -96,18 +96,28 @@ exports.executeableSubcommand = co.wrap(function *(args) {
yield RebaseUtil.abort(repo);
}
else if (args.continue) {
- yield RebaseUtil.continue(repo);
+ const result = yield RebaseUtil.continue(repo);
+ if (null !== result.errorMessage) {
+ throw new UserError(result.errorMessage);
+ }
}
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());
- 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/cmd/reset.js b/node/lib/cmd/reset.js
index a60ab0034..22a08d20b 100644
--- a/node/lib/cmd/reset.js
+++ b/node/lib/cmd/reset.js
@@ -130,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");
@@ -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(repo, "post-reset");
});
diff --git a/node/lib/cmd/rm.js b/node/lib/cmd/rm.js
new file mode 100644
index 000000000..e074c3600
--- /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);
+});
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/cmd/stash.js b/node/lib/cmd/stash.js
index c93231cb9..76a653287 100644
--- a/node/lib/cmd/stash.js
+++ b/node/lib/cmd/stash.js
@@ -54,10 +54,18 @@ and unstaged commits to the sub-repos.`;
exports.configureParser = function (parser) {
+ parser.addArgument(["-m", "--message"], {
+ type: "string",
+ action: "append",
+ 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; 'save' is
-default`,
+'save' to save a stash, 'pop' to restore, 'list' to show stashes, 'drop' to \
+discard a stash, 'apply' to apply a change without popping from stashes; \
+'save' is default`,
type: "string",
nargs: "?",
defaultValue: "save",
@@ -75,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) {
@@ -83,7 +98,18 @@ 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, 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) {
@@ -91,7 +117,20 @@ 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.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
+ return false;
+ }
+ if (null === wd) {
+ continue;
+ }
+ if ((!wd.status.isClean(includeUntracked)) ||
+ wd.status.headCommit !== sub.commit.sha) {
return false;
}
}
@@ -110,23 +149,48 @@ 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.");
return; // RETURN
}
- yield StashUtil.save(repo, status, includeUntracked || false);
+ const message = args.message ? args.message.join("\n\n") : null;
+ yield StashUtil.save(repo,
+ status,
+ includeUntracked || false,
+ 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);
});
+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);
+});
+
/**
* Execute the `stash` command according to the specified `args`.
*
@@ -137,8 +201,10 @@ 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);
default: {
console.error(`Invalid type ${colors.red(args.type)}`);
process.exit(1);
diff --git a/node/lib/cmd/status.js b/node/lib/cmd/status.js
index 357b264a0..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.
*/
@@ -56,13 +58,26 @@ 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"], {
+ parser.addArgument(["-s", "--short"], {
required: false,
action: "storeConst",
constant: true,
- help: `
-Include changes to the meta-repo; disabled by default to improve performance.`,
- defaultValue: false,
+ 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",
@@ -72,13 +87,12 @@ Include changes to the meta-repo; disabled by default to improve performance.`,
};
/**
- * 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");
@@ -90,12 +104,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,
+ untrackedFilesOption: args.untrackedFilesOption
});
// Compute the current directory relative to the working directory of the
@@ -103,6 +115,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/cmd/submodule.js b/node/lib/cmd/submodule.js
index 09be0cd85..11a0513ce 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
@@ -84,12 +85,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: "*",
});
@@ -119,30 +121,96 @@ 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) {
- 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 => {
+ 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,
});
- const relCwd = path.relative(workdir, cwd);
- process.stdout.write(PrintStatusUtil.printSubmoduleStatus(status,
- relCwd,
- verbose));
+ 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(SubmoduleUtil.resolvePaths(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) {
@@ -167,13 +235,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)}.`);
@@ -200,7 +267,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);
+ 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
@@ -241,29 +309,9 @@ 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);
+const doForeachCommand = co.wrap(function *() {
+ const args = process.argv.slice(3);
+ yield Forward.execute("submodule", args);
});
/**
@@ -280,7 +328,20 @@ 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();
+ }
+ else if ("foreach" === args.command) {
+ return doForeachCommand();
}
return doFindCommand(args.path,
args.meta_committish,
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/destitch.js b/node/lib/destitch.js
new file mode 100755
index 000000000..fefc87209
--- /dev/null
+++ b/node/lib/destitch.js
@@ -0,0 +1,84 @@
+#!/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 NodeGit = require("nodegit");
+
+const DestitchUtil = require("./util/destitch_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 location = yield NodeGit.Repository.discover(".", 0, "");
+ const repo = yield NodeGit.Repository.open(location);
+ 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/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;
diff --git a/node/lib/git-meta.js b/node/lib/git-meta.js
index 316df738b..47a59fb9c 100755
--- a/node/lib/git-meta.js
+++ b/node/lib/git-meta.js
@@ -36,35 +36,33 @@
*/
const ArgumentParser = require("argparse").ArgumentParser;
-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");
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");
const rebase = require("./cmd/rebase");
const reset = require("./cmd/reset");
+const rm = require("./cmd/rm");
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 syncrefs = require("./cmd/syncrefs");
+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`.
@@ -72,11 +70,9 @@ NodeGit.setThreadSafetyStatus(NodeGit.THREAD_SAFETY.ENABLED_FOR_ASYNC_ONLY);
* @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,
@@ -84,6 +80,18 @@ function configureSubcommand(parser, commandName, module) {
module.configureParser(subParser);
subParser.setDefaults({
func: function (args) {
+ // 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) {
@@ -91,7 +99,10 @@ function configureSubcommand(parser, commandName, module) {
// 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 {
console.error(error.stack);
@@ -108,7 +119,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,
@@ -126,39 +139,75 @@ const commands = {
"include": include,
"ls-files": listFiles,
"merge": merge,
+ "merge-bare": mergeBare,
"add-submodule": addSubmodule,
"open": open,
"pull": pull,
"push": push,
"rebase": rebase,
"reset": reset,
+ "rm": rm,
"root": root,
+ "commit-shadow": commitShadow,
"stash": stash,
"submodule": submodule,
"status": status,
+ "sync-refs": syncrefs,
"version": version,
};
-// Configure forwarded commands.
-
-Array.from(Forward.forwardedCommands).forEach(name => {
- commands[name] = Forward.makeModule(name);
-});
+// These optimized commands do not require NodeGit, and can skip importing it.
+const optimized = {
+ "root": true,
+ "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, (undefined !== optimized[name]));
});
+const do_not_forward = 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])) {
+ !do_not_forward.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(() => {
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);
+ }
+});
diff --git a/node/lib/stitch.js b/node/lib/stitch.js
new file mode 100755
index 000000000..a841f1568
--- /dev/null
+++ b/node/lib/stitch.js
@@ -0,0 +1,165 @@
+#!/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 UserError = require("./util/user_error");
+
+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'.",
+});
+
+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) ?
+ 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),
+ preloadCache: args.preload_cache,
+ };
+ 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) {
+ if (e instanceof UserError) {
+ console.error(e.message);
+ } else {
+ console.error(e.stack);
+ }
+ process.exit(1);
+ }
+});
diff --git a/node/lib/util/add.js b/node/lib/util/add.js
index d473dafb4..961df0be6 100644
--- a/node/lib/util/add.js
+++ b/node/lib/util/add.js
@@ -32,32 +32,58 @@
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 StatusUtil = require("./status_util");
-const SubmoduleUtil = require("./submodule_util");
+const DiffUtil = require("./diff_util");
+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
* 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
* @param {String []} paths
*/
-exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges) {
+exports.stagePaths = co.wrap(function *(repo,
+ paths,
+ stageMetaChanges,
+ update,
+ verbose) {
assert.instanceOf(repo, NodeGit.Repository);
assert.isArray(paths);
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)}`);
+ }
+ }));
+
+ 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,
- showAllUntracked: true,
+ untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL,
});
// First, stage submodules.
@@ -69,8 +95,31 @@ 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 (RepoStatus.FILESTATUS.REMOVED === workdir[filename]) {
+ log_verbose(name, filename, "remove");
+ return index.removeByPath(filename);
+ } else if (update) {
+ if (RepoStatus.FILESTATUS.ADDED !== workdir[filename]) {
+ log_verbose(name, filename, "modify");
+ return index.addByPath(filename);
+ }
+ } else {
+ 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) {
+ log_verbose(name, filename, "add");
+ yield index.addByPath(filename);
+ }
+ }));
yield index.write();
}
}));
@@ -81,6 +130,6 @@ exports.stagePaths = co.wrap(function *(repo, paths, stageMetaChanges) {
if (0 !== toAdd.length) {
const index = yield repo.index();
yield toAdd.map(filename => index.addByPath(filename));
- yield index.write();
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
}
});
diff --git a/node/lib/util/add_submodule.js b/node/lib/util/add_submodule.js
index 2e6ec9454..98e7ef589 100644
--- a/node/lib/util/add_submodule.js
+++ b/node/lib/util/add_submodule.js
@@ -33,11 +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 path = require("path");
const GitUtil = require("./git_util");
+const SparseCheckoutUtil = require("./sparse_checkout_util");
const SubmoduleConfigUtil = require("./submodule_config_util");
const UserError = require("./user_error");
@@ -63,16 +62,12 @@ 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);
- fs.appendFileSync(modulesPath, `\
-[submodule "${filename}"]
- path = ${filename}
- url = ${url}
-`);
const index = yield repo.index();
- yield index.addByPath(SubmoduleConfigUtil.modulesFileName);
- yield index.write();
+ const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index);
+ urls[filename] = url;
+ yield SubmoduleConfigUtil.writeUrls(repo, index, urls);
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+
const metaUrl = yield GitUtil.getOriginUrl(repo);
const templatePath = yield SubmoduleConfigUtil.getTemplatePath(repo);
const subRepo = yield SubmoduleConfigUtil.initSubmoduleAndRepo(
@@ -80,7 +75,8 @@ exports.addSubmodule = co.wrap(function *(repo, url, filename, importArg) {
repo,
filename,
url,
- templatePath);
+ templatePath,
+ false);
if (null === importArg) {
return subRepo; // RETURN
}
@@ -92,7 +88,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);
diff --git a/node/lib/util/bulk_notes_util.js b/node/lib/util/bulk_notes_util.js
new file mode 100644
index 000000000..53a3ff52f
--- /dev/null
+++ b/node/lib/util/bulk_notes_util.js
@@ -0,0 +1,233 @@
+/*
+ * 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 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");
+
+/**
+ * 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) {
+ return path.join(sha.substr(0, 2), sha.substr(2, 2), sha.substr(4));
+};
+
+/**
+ * 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
+ */
+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;
+ }
+});
+
+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 = [];
+ const ref = yield GitUtil.getReference(repo, refName);
+ if (null !== ref) {
+ const 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);
+ const sharded = exports.shardSha(sha);
+ changes[sharded] = 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);
+
+ 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
+ * 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();
+ yield processTree(result, repo, tree, "");
+ 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/checkout.js b/node/lib/util/checkout.js
index a263d87a9..66854adfc 100644
--- a/node/lib/util/checkout.js
+++ b/node/lib/util/checkout.js
@@ -38,12 +38,15 @@ const co = require("co");
const colors = require("colors");
const NodeGit = require("nodegit");
-const GitUtil = require("./git_util");
-const SubmoduleFetcher = require("./submodule_fetcher");
-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
@@ -57,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];
@@ -81,74 +84,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);
- yield open.map(co.wrap(function *(name) {
+ 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(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
@@ -189,17 +164,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;
});
@@ -207,25 +186,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.
@@ -234,10 +218,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, false);
+ 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;
@@ -246,18 +234,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 SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
});
/**
@@ -309,6 +287,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
@@ -317,11 +296,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);
@@ -329,12 +310,19 @@ 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,
+ checkoutFromIndex: false
};
const ensureBranchDoesntExist = co.wrap(function *(name) {
@@ -377,15 +365,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) {
@@ -404,18 +392,20 @@ 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)}.`);
+ if (null === annotated && !result.newBranch) {
+ // If we didn't resolve anything from `committish`, try it
+ // as a file.
+ files.splice(0, 0, committish);
+ result.checkoutFromIndex = true;
}
}
else {
-
const commit = yield repo.getCommit(annotated.id());
result.commit = commit;
@@ -437,15 +427,41 @@ 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.
+ if (files.length === 0) {
+ // 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;
+ }
+ }
+ }
- 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);
+
+ const workdir = repo.workdir();
+ const cwd = process.cwd();
+
+ const absfiles = files.map(filename =>
+ GitUtil.resolveRelativePath(workdir, cwd,
+ filename));
+
+ result.resolvedPaths = SubmoduleUtil.resolvePaths(
+ absfiles, indexSubNames, openSubmodules, true);
+
+ if (null === result.commit) {
+ result.checkoutFromIndex = true;
}
+
+ return result;
}
+
if (null !== newBranch) {
// If we have a `newBranch`, we need to make sure it doesn't already
// exist.
@@ -466,10 +482,10 @@ 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("/");
+ const parts = committishBranch.shorthand().split(/\/(.+)/);
tracking = {
remoteName: parts[0],
branchName: parts[1],
@@ -563,3 +579,114 @@ exports.executeCheckout = co.wrap(function *(repo,
yield repo.setHead(`refs/heads/${switchBranch}`);
}
});
+
+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 {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, options) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ const resolvedPaths = options.resolvedPaths;
+
+ // 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 submodules = Object.keys(resolvedPaths);
+ const subNames = null === options.commit ?
+ submodules.filter(submodule => openSubmodules.includes(submodule)) :
+ submodules;
+
+ let subCommits;
+ let stage = 0;
+ if (undefined !== options.stage) {
+ assert(options.checkoutFromIndex);
+ stage = options.stage;
+ assert.isNumber(stage);
+ }
+
+ 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);
+ 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 opts = new NodeGit.CheckoutOptions();
+ opts.checkoutStrategy = NodeGit.Checkout.STRATEGY.FORCE;
+ if (paths.length > 0) {
+ opts.paths = paths;
+ }
+ if (options.checkoutFromIndex) {
+ yield NodeGit.Checkout.index(subRepo, info.index, opts);
+ } else {
+ yield NodeGit.Checkout.tree(subRepo, info.resolvedSubCommit, opts);
+ }
+ });
+
+ yield DoWorkQueue.doInParallel(subNames, checkoutSub);
+});
diff --git a/node/lib/util/cherry_pick_util.js b/node/lib/util/cherry_pick_util.js
new file mode 100644
index 000000000..86ee141d7
--- /dev/null
+++ b/node/lib/util/cherry_pick_util.js
@@ -0,0 +1,941 @@
+/*
+ * 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 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");
+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 TreeUtil = require("./tree_util");
+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
+ * 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`
+ * and if that submodule is open, reset its HEAD, index, and worktree to
+ * 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
+ * @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,
+ urlsInIndex) {
+ 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 = (urlsInIndex === null || urlsInIndex === undefined) ?
+ (yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index)) :
+ urlsInIndex;
+ 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) {
+ 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);
+ yield fetcher.fetchSha(subRepo, name, sub.sha);
+ const commit = yield subRepo.getCommit(sub.sha);
+ 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);
+ 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);
+});
+
+/**
+* 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
+ */
+exports.changeSubmodulesBare = co.wrap(function *(repo,
+ index,
+ submodules) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(index, NodeGit.Index);
+ assert.isObject(submodules);
+ if (0 === Object.keys(submodules).count) {
+ return; // RETURN
+ }
+ const urls = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo, index);
+ 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) {
+ delete urls[name];
+ continue;
+ }
+ yield exports.addSubmoduleCommit(index, name, sub.sha);
+ urls[name] = sub.url;
+ }
+ // 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
+ * 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, 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 baseUrls = {};
+ if (undefined !== baseCommit) {
+ baseUrls =
+ yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, baseCommit);
+ }
+ const commitUrls =
+ yield SubmoduleConfigUtil.getSubmodulesFromCommit(repo, commit);
+ for (let name in baseUrls) {
+ const baseUrl = baseUrls[name];
+ const commitUrl = commitUrls[name];
+ if (undefined !== commitUrl && baseUrl !== commitUrl) {
+ return true; // RETURN
+ }
+ }
+ 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);
+ 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(sha);
+ 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;
+ }
+ }
+ }
+});
+
+/**
+ *
+ * @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
+ * 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 `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.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 conflicts = {};
+
+ // Group together all parts of conflicted entries.
+ const conflictEntries = new Map(); // name -> normal, ours, theirs
+ const entries = index.entries();
+ 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;
+ }
+ }
+
+ // 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 changes = {};
+ function makeConflict(entry) {
+ if (undefined === entry) {
+ return null;
+ }
+ return new ConflictEntry(entry.mode, entry.id.tostrS());
+ }
+
+ const libgit2MergeBugData = {
+ head: srcCommit,
+ 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];
+ if (undefined !== ancestor &&
+ undefined !== ours &&
+ undefined !== theirs &&
+ COMMIT === ours.mode &&
+ COMMIT === theirs.mode) {
+ changes[name] = new SubmoduleChange(ancestor.id.tostrS(),
+ theirs.id.tostrS(),
+ ours.id.tostrS());
+ } else if (SubmoduleConfigUtil.modulesFileName !== name) {
+ conflicts[name] = new Conflict(makeConflict(ancestor),
+ makeConflict(ours),
+ makeConflict(theirs));
+ }
+ }
+
+ // 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
+ // requires that we clean the conflicts. Anything we've already diagnosed
+ // as either a conflict or a non-simple change will be ignored here.
+
+ yield index.conflictCleanup();
+ const simpleChanges = {};
+ const treeId = yield index.writeTreeTo(repo);
+ const tree = yield NodeGit.Tree.lookup(repo, treeId);
+ 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) {
+ // Skip changes we've already taken into account and the `.gitmodules`
+ // file.
+
+ if (SubmoduleConfigUtil.modulesFileName === name ||
+ name in changes ||
+ name in conflicts) {
+ continue; // CONTINUE
+ }
+ 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,
+ urls: urls,
+ };
+});
+
+/**
+ * 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 access submodule repos.
+ *
+ * @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
+ * @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);
+ assert.instanceOf(opener, Open.Opener);
+ assert.instanceOf(metaIndex, NodeGit.Index);
+ assert.isObject(subs);
+ const result = {
+ commits: {},
+ conflicts: {},
+ ffwds: {},
+ };
+ const fetcher = yield opener.fetcher();
+ const pickSub = co.wrap(function *(name) {
+ 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) + "]";
+ console.log(`Sub-repo ${colors.blue(name)}: applying 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 SubmoduleRebaseUtil.rewriteCommits(
+ repo,
+ 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;
+ }
+ });
+ yield DoWorkQueue.doInParallel(Object.keys(subs), pickSub);
+ 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
+ * 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;
+});
+
+/**
+ * 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 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)}`);
+ return true;
+ }
+ return false;
+ });
+ yield SubmoduleConfigUtil.deinit(repo, toClose);
+});
+
+/**
+ * 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.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, commandName) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(commit, NodeGit.Commit);
+
+ yield exports.ensureNoURLChanges(repo, commit);
+
+ 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,
+ // and fast-forwards.
+
+ const opener = new Open.Opener(repo, null);
+ yield exports.changeSubmodules(repo,
+ opener,
+ index,
+ changes.simpleChanges,
+ null);
+
+ // 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;
+
+ yield exports.closeSubs(opener, picks);
+
+ Object.keys(conflicts).sort().forEach(name => {
+ 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,
+ errorMessage: errorMessage === "" ? null : errorMessage,
+ newMetaCommit: null,
+ };
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+ 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);
+ }
+
+ 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, "cherry-pick");
+ 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 `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}] commits
+ * @return {Object} return
+ * @return {String} return.newMetaCommit
+ * @return {Object} returm.submoduleCommits
+ * @return {String|null} return.errorMessage
+ */
+exports.cherryPick = co.wrap(function *(metaRepo, commits) {
+ assert.instanceOf(metaRepo, NodeGit.Repository);
+ assert.instanceOf(commits[0], 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.`);
+ }
+
+ // 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 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 is bogus for cherry-picks but must be filled in anyway
+ target: new CommitAndRef(lastCommit, null),
+ currentCommit: 0,
+ commits: commitIdStrs,
+ });
+ yield SequencerStateUtil.writeSequencerState(metaRepo.path(), seq);
+
+ return yield pickRemainingCommits(metaRepo, seq);
+});
+
+/**
+ * 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 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(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 (subResult.errorMessage !== null ||
+ seq.currentCommit + 1 === seq.commits.length) {
+ yield SequencerStateUtil.cleanSequencerState(repo.path());
+ return result;
+ }
+ const newSeq = seq.copy({currentCommit : seq.currentCommit + 1});
+ return yield pickRemainingCommits(repo, newSeq);
+});
+
+/**
+ * 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 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 SequencerStateUtil.cleanSequencerState(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 5126e4fad..000000000
--- a/node/lib/util/cherrypick.js
+++ /dev/null
@@ -1,169 +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.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));
- }
- });
-
- // 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/close_util.js b/node/lib/util/close_util.js
index 2152b4551..af2032ef0 100644
--- a/node/lib/util/close_util.js
+++ b/node/lib/util/close_util.js
@@ -35,10 +35,12 @@ const NodeGit = require("nodegit");
const co = require("co");
const colors = require("colors");
-const DeinitUtil = require("../util/deinit_util");
-const StatusUtil = require("../util/status_util");
-const SubmoduleUtil = require("../util/submodule_util");
-const UserError = require("../util/user_error");
+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");
+const UserError = require("../util/user_error");
/**
@@ -59,23 +61,25 @@ 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);
+ let subsToClose = yield SubmoduleUtil.resolveSubmoduleNames(workdir,
+ cwd,
+ subs,
+ paths);
+ subsToClose = Array.from(new Set(subsToClose));
- const subsToClose = yield SubmoduleUtil.resolveSubmoduleNames(workdir,
- cwd,
- subs,
- paths);
- const closers = subsToClose.map(co.wrap(function *(name) {
+ const repoStatus = yield StatusUtil.getRepoStatus(repo, {
+ paths: subsToClose,
+ });
+ const subStats = repoStatus.submodules;
+ let errorMessage = "";
+ const subsClosedSuccessfully = subsToClose.filter(name => {
const sub = subStats[name];
- const subWorkdir = sub.workdir;
- if (null === subWorkdir) {
- return; // RETURN
+ if (undefined === sub || null === sub.workdir) {
+ return false; // RETURN
}
+ const subWorkdir = sub.workdir;
const subRepo = subWorkdir.status;
if (!force) {
// Determine if there are any uncommited changes:
@@ -88,12 +92,21 @@ 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
}
}
- yield DeinitUtil.deinit(repo, name);
- }));
- yield closers;
+ return true; // RETURN
+ });
+ yield SubmoduleConfigUtil.deinit(repo, subsClosedSuccessfully);
+
+ // Write out the meta index to update SKIP_WORKTREE flags for closed
+ // submodules.
+
+ 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);
if ("" !== errorMessage) {
throw new UserError(errorMessage);
}
diff --git a/node/lib/util/commit.js b/node/lib/util/commit.js
index 9c0b41a27..92e92844e 100644
--- a/node/lib/util/commit.js
+++ b/node/lib/util/commit.js
@@ -40,27 +40,47 @@
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 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 Mutex = require("async-mutex").Mutex;
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");
+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");
+const getSubmodulesFromCommit = SubmoduleConfigUtil.getSubmodulesFromCommit;
+
/**
* If the specified `message` does not end with '\n', return the result of
* appending '\n' to 'message'; otherwise, return 'message'.
*
* @param {String} message
*/
-function ensureEolOnLastLine(message) {
+exports.ensureEolOnLastLine = function (message) {
+ // TODO: test independently
return message.endsWith("\n") ? message : (message + "\n");
+};
+
+function abortIfNoMessage(message) {
+ if (message === "") {
+ throw new UserError("Aborting commit due to empty commit message.");
+ }
}
/**
@@ -82,7 +102,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
@@ -92,7 +117,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.
@@ -100,7 +130,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 {
/**
@@ -185,54 +215,73 @@ exports.stageChange = co.wrap(function *(index, path, change) {
}
});
+const mutex = new Mutex();
+
+const runPreCommitHook = 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);
+
+ let release = null;
+ try {
+ if (yield 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);
+
+ if (!isOk) {
+ // hooks are responsible for printing their own message
+ throw new UserError("");
+ }
+ }
+ } finally {
+ if (release !== null) {
+ release();
+ }
+ 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}
+ * @param {Boolean} noVerify
*/
-const commitRepo = co.wrap(function *(repo,
- changes,
- doAll,
- message,
- force,
- signature) {
+const prepareIndexAndRunHooks = co.wrap(function *(repo,
+ changes,
+ doAll,
+ noVerify) {
assert.instanceOf(repo, NodeGit.Repository);
assert.isObject(changes);
assert.isBoolean(doAll);
- assert.isString(message);
- assert.isBoolean(force);
- assert.instanceOf(signature, NodeGit.Signature);
+ assert.isBoolean(noVerify);
- 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();
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
}
- if (doCommit) {
- return yield repo.createCommitOnHead([],
- signature,
- signature,
- ensureEolOnLastLine(message));
+
+ if (!noVerify) {
+ yield runPreCommitHook(repo, index);
}
- return null;
});
const editorMessagePrefix = `\
@@ -348,18 +397,51 @@ 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.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) {
- yield Object.keys(submodules).map(co.wrap(function *(name) {
+const stageOpenSubmodules = co.wrap(function *(repo, index, submodules) {
+ 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;
}
}));
- yield index.write();
+ for (const entry of entries.filter(x => !!x)) {
+ index.add(entry);
+ }
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
});
/**
@@ -427,6 +509,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
@@ -439,7 +537,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
@@ -447,6 +545,10 @@ exports.shouldCommit = function (status, skipMeta, subMessages) {
* @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
@@ -454,31 +556,67 @@ exports.shouldCommit = function (status, skipMeta, subMessages) {
exports.commit = co.wrap(function *(metaRepo,
all,
metaStatus,
- message,
- subMessages) {
+ 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;
+ if (mergeParent) {
+ assert.instanceOf(mergeParent, NodeGit.Commit);
+ mergeTree = yield mergeParent.getTree();
+ }
- 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
// a commit in the meta-repo whether or not the meta-repo has its own
// 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
@@ -490,24 +628,49 @@ 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);
- const commit = yield commitRepo(subRepo,
- repoStatus.staged,
- all,
- subMessage,
- false,
- signature);
- subCommits[name] = commit.tostrS();
+
+ 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());
+ }
+ }
+
+ 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 subCommitters = Object.keys(submodules).map(commitSubmodule);
- yield subCommitters;
+ yield DoWorkQueue.doInParallel(Object.keys(subStageData), commitSubmodule);
+
+ const index = yield metaRepo.index();
+ 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,
@@ -517,36 +680,74 @@ exports.commit = co.wrap(function *(metaRepo,
return result; // RETURN
}
- const index = yield metaRepo.index();
- yield stageOpenSubmodules(index, submodules);
+ yield stageOpenSubmodules(metaRepo, index, submodules);
+ if (!noVerify) {
+ yield runPreCommitHook(metaRepo, index);
+ }
+
+ const tree = yield index.writeTree();
+ const headCommit = yield metaRepo.getHeadCommit();
+ const parents = [headCommit];
+ if (mergeParent) {
+ assert(mergeParent !== headCommit);
+ parents.push(mergeParent);
+ }
- result.metaCommit = yield commitRepo(metaRepo,
- metaStatus.staged,
- all,
- message,
- true,
- signature);
+ result.metaCommit = yield metaRepo.createCommit(
+ "HEAD",
+ signature,
+ signature,
+ exports.ensureEolOnLastLine(message),
+ tree,
+ parents);
+ if (mergeParent) {
+ yield SequencerStateUtil.cleanSequencerState(metaRepo.path());
+ }
return result;
});
/**
- * 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.
+ * 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;
+ }
+});
+
+/**
+ * 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}
*/
-exports.writeRepoPaths = co.wrap(function *(repo, status, message) {
+const writeSubmoduleIndex = co.wrap(function *(repo, status, noVerify) {
assert.instanceOf(repo, NodeGit.Repository);
assert.instanceOf(status, RepoStatus);
- assert.isString(message);
const headCommit = yield repo.getHeadCommit();
const changes = {};
@@ -559,18 +760,128 @@ 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.
+ // 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 = TreeUtil.hashFile(repo, filename);
- changes[filename] = new Change(blobId, FILEMODE.BLOB);
+ 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
+ * `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}
+ */
+exports.writeRepoPaths = co.wrap(function *(repo, status, message, 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());
+
+ // First, 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);
}
}
@@ -591,44 +902,59 @@ 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 index.write();
+ if (!noVerify) {
+ yield runPreCommitHook(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.
- const sig = repo.defaultSignature();
+ const sig = yield ConfigUtil.defaultSignature(repo);
const parents = [headCommit];
- const commitId = yield NodeGit.Commit.create(repo,
- 0,
- sig,
- sig,
- 0,
- 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
- // do a SOFT reset because we don't want to affect index changes for paths
- // you didn't touch.
-
- const commit = yield repo.getCommit(commitId);
- yield NodeGit.Reset.reset(repo, commit, NodeGit.Reset.TYPE.SOFT);
- return commitId.tostrS();
-});
+ const commitId = yield NodeGit.Commit.create(
+ repo,
+ 0,
+ sig,
+ sig,
+ 0,
+ exports.ensureEolOnLastLine(message),
+ tree,
+ parents.length,
+ parents);
+
+ //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());
+ }
+ }
+ return commitId;
+});
/**
* Commit changes to the files indicated as staged by the specified `status`
@@ -644,17 +970,20 @@ 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, 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
const committedSubs = {}; // map from name to RepoAST.Submodule
const subs = status.submodules;
- yield Object.keys(subs).map(co.wrap(function *(subName) {
+ const subStageData = {};
+
+ const writeSubIndexes = co.wrap(function*(subName) {
const sub = subs[subName];
const workdir = sub.workdir;
@@ -674,7 +1003,27 @@ 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 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;
const Submodule = RepoStatus.Submodule;
@@ -686,7 +1035,15 @@ exports.commitPaths = co.wrap(function *(repo, status, message) {
headCommit: sha,
}), Submodule.COMMIT_RELATION.SAME)
});
- }));
+ committedSubs[subName].repo = subRepo;
+ });
+
+ yield DoWorkQueue.doInParallel(Object.keys(subs), writeSubmoduleCommits);
+
+ 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`.
@@ -694,8 +1051,10 @@ exports.commitPaths = co.wrap(function *(repo, status, message) {
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,
@@ -720,7 +1079,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 +1088,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;
@@ -771,7 +1134,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();
@@ -803,21 +1166,75 @@ 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
* 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,64 +1262,100 @@ 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();
- // If we're including the meta-repo, load its changes.
+ const newUrls = yield getSubmodulesFromCommit(repo, head);
- let metaStaged = {};
- let metaWorkdir = {};
- if (includeMeta) {
- const changes = yield getAmendStatusForRepo(repo, all);
- metaStaged = changes.staged;
- metaWorkdir = changes.workdir;
- }
-
- // Read the state of the commits in the commit before the one to be
- // amended.
-
- let oldSubs = {};
- 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);
- }
+ const amendChanges = yield computeAmendSubmoduleChanges(repo, head);
+ const changes = amendChanges.changes;
+ const merged = amendChanges.merged;
+ const oldUrls = amendChanges.oldUrls;
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.
+
+ const subsToInspect = Array.from(new Set(
+ Object.keys(submodules).concat(Object.keys(changes))));
+
+ const inspectSub = co.wrap(function *(name) {
+ const change = changes[name];
+ let currentSub = submodules[name];
+ let old = null;
+ 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.
+
+ if (null === change.newSha) {
+ return; // RETURN
+ }
+ // This submodule was affected by the commit; record its 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.
- yield Object.keys(submodules).map(co.wrap(function *(name) {
- const currentSub = submodules[name];
- const old = oldSubs[name] || null;
- const getRepo = () => opener.getSubrepo(name);
+ const commit = currentSub.commit;
+ if (null !== commit) {
+ old = new Submodule(commit.url, commit.sha);
+ }
+ }
+ const getRepo =
+ () => opener.getSubrepo(name,
+ Open.SUB_OPEN_OPTION.FORCE_OPEN);
const result = yield exports.getSubmoduleAmendStatus(currentSub,
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 {
@@ -915,28 +1368,26 @@ 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.
- 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,
});
@@ -947,14 +1398,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);
@@ -962,11 +1414,18 @@ 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 id = yield head.amend("HEAD", null, null, null, termedMessage, tree);
- return id.tostrS();
+ const termedMessage = exports.ensureEolOnLastLine(message);
+ 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`
@@ -985,6 +1444,7 @@ exports.amendRepo = 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
@@ -994,7 +1454,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);
@@ -1021,9 +1482,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.
@@ -1035,30 +1496,41 @@ exports.amendMetaRepo = co.wrap(function *(repo,
return; // RETURN
}
}
+ subMessage = exports.ensureEolOnLastLine(subMessage);
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);
+ 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 (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) {
- const head = yield subRepo.getHeadCommit();
+ if (0 === numStaged) {
const parent = yield GitUtil.getParentCommit(subRepo, head);
const TYPE = NodeGit.Reset.TYPE;
const type = all ? TYPE.HARD : TYPE.MIXED;
@@ -1066,30 +1538,80 @@ exports.amendMetaRepo = co.wrap(function *(repo,
return; // RETURN
}
- const subIndex = yield subRepo.index();
- yield stageFiles(subRepo, staged, subIndex);
- subCommits[subName] = yield exports.amendRepo(subRepo, subMessage);
+ if (all) {
+ 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 examine 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);
+ }
+ }
+ }
+ if (!noVerify) {
+ yield runPreCommitHook(subRepo, subIndex);
+ }
+ subCommits[subName] = yield exports.createAmendCommit(subRepo,
+ subMessage);
return; // RETURN
}
- const commit = yield commitRepo(subRepo,
- repoStatus.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);
+ }
+ }
+ }
+
+ if (!noVerify) {
+ yield runPreCommitHook(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(index, subs);
+ 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,
@@ -1261,19 +1783,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,
@@ -1310,7 +1829,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.
@@ -1471,12 +1990,10 @@ 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
* @param {Object} [options]
@@ -1523,17 +2040,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);
@@ -1546,6 +2057,7 @@ exports.getCommitStatus = co.wrap(function *(repo, cwd, options) {
const workdirStatus = yield StatusUtil.getRepoStatus(repo, {
showMetaChanges: options.showMetaChanges,
ignoreIndex: true,
+ untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL,
});
return exports.calculateAllRepoStatus(baseStatus, workdirStatus);
}
@@ -1628,7 +2140,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) {
@@ -1798,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
@@ -1811,8 +2320,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`.
*
@@ -1823,7 +2331,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
@@ -1831,26 +2340,25 @@ function abortForNoMessage() {
exports.doCommitCommand = co.wrap(function *(repo,
cwd,
message,
- meta,
all,
paths,
interactive,
+ noVerify,
editMessage) {
assert.instanceOf(repo, NodeGit.Repository);
assert.isString(cwd);
if (null !== message) {
assert.isString(message);
}
- assert.isBoolean(meta);
assert.isBoolean(all);
assert.isArray(paths);
assert.isBoolean(interactive);
- assert.isFunction(editMessage);
+ assert.isBoolean(noVerify);
+ assert.isBoolean(editMessage);
const workdir = repo.workdir();
const relCwd = path.relative(workdir, cwd);
const repoStatus = yield exports.getCommitStatus(repo, cwd, {
- showMetaChanges: meta,
all: all,
paths: paths,
});
@@ -1884,6 +2392,20 @@ exports.doCommitCommand = co.wrap(function *(repo,
if (usingPaths) {
checkForPathIncompatibleSubmodules(repoStatus, relCwd);
}
+ 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;
+ throw new UserError(msg);
+ }
+ }
+
+ 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.
@@ -1894,19 +2416,21 @@ exports.doCommitCommand = co.wrap(function *(repo,
}
let subMessages;
+ let messageFunc;
if (interactive) {
// 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,
{});
- const userText = yield editMessage(repo, prompt);
+ 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.
@@ -1915,31 +2439,40 @@ 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);
- const rawMessage = yield editMessage(repo, initialMessage);
- message = GitUtil.stripMessage(rawMessage);
- }
+ } 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,
- subMessages);
+ messageFunc,
+ noVerify);
}
else {
return yield exports.commit(repo,
all,
repoStatus,
- message,
- subMessages);
+ messageFunc,
+ subMessages,
+ noVerify,
+ mergeParent);
}
});
@@ -1952,43 +2485,40 @@ 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
- * behavior is undefined if `null !== message && true === interactive`.
+ * `!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.
*
* @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
+ * @param {Boolean} noVerify
+ * @param {Boolean} 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,
cwd,
message,
- meta,
all,
interactive,
+ noVerify,
editMessage) {
assert.instanceOf(repo, NodeGit.Repository);
assert.isString(cwd);
if (null !== message) {
assert.isString(message);
}
- assert.isBoolean(meta);
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);
const amendStatus = yield exports.getAmendStatus(repo, {
- includeMeta: meta,
all: all,
cwd: relCwd,
});
@@ -1997,7 +2527,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) {
@@ -2008,7 +2538,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;
@@ -2020,40 +2551,46 @@ 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);
}
-
- 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);
}
}
- if ("" === message) {
- abortForNoMessage();
- }
+ abortIfNoMessage(message);
+ 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,
@@ -2061,6 +2598,7 @@ You can make this commit using the interactive ('-i') commit option.`;
Object.keys(subsToAmend),
all,
message,
- subMessages);
-
+ subMessages,
+ noVerify);
});
+
diff --git a/node/lib/util/config_util.js b/node/lib/util/config_util.js
new file mode 100644
index 000000000..8e1cf9ed2
--- /dev/null
+++ b/node/lib/util/config_util.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 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.
+ */
+
+/**
+ * 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
+ * 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);
+
+ 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";
+});
+
+
+/**
+ * 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();
+ // 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/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/deinit_util.js b/node/lib/util/deinit_util.js
deleted file mode 100644
index da5eb5e30..000000000
--- a/node/lib/util/deinit_util.js
+++ /dev/null
@@ -1,123 +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");
-
-/**
- * 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);
-
- // 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;
-
- // 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/destitch_util.js b/node/lib/util/destitch_util.js
new file mode 100644
index 000000000..c6b3596b8
--- /dev/null
+++ b/node/lib/util/destitch_util.js
@@ -0,0 +1,517 @@
+/*
+ * 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 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");
+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,
+ ForcePushSpec.Force,
+ 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 BulkNotesUtil.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/lib/util/diff_util.js b/node/lib/util/diff_util.js
index 3c20a993c..3674e98b0 100644
--- a/node/lib/util/diff_util.js
+++ b/node/lib/util/diff_util.js
@@ -35,7 +35,14 @@ const co = require("co");
const NodeGit = require("nodegit");
const RepoStatus = require("./repo_status");
-const SubmoduleConfigUtil = require("./submodule_config_util");
+
+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
@@ -46,13 +53,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 +67,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,17 +77,25 @@ 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() :
delta.newFile();
const path = file.path();
- // Skip the .gitmodules file and all submodule changes; they're handled
- // separately.
+ 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;
+ }
- 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;
}
}
@@ -90,22 +103,27 @@ 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
- * 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 `{}`.
+ * 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
@@ -114,30 +132,44 @@ 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.EXCLUDE_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);
+ yield NodeGit.Diff.treeToWorkdirWithIndex(repo,
+ tree,
+ options);
const workdirToTreeStatus = readDiff(workdirToTreeDiff);
return {
staged: {},
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/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
old mode 100644
new mode 100755
index d92daaffc..638f7ed4b
--- 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;
@@ -42,31 +43,14 @@ const fs = require("fs-promise");
const NodeGit = require("nodegit");
const path = require("path");
-const UserError = require("../util/user_error");
+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");
-/**
- * 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 gitDir = path.join(dir, ".git");
- if (fs.existsSync(gitDir) && fs.statSync(gitDir).isDirectory()) {
- return dir; // RETURN
- }
-
- const base = path.dirname(dir);
-
- if ("" === base || "/" === base) {
- return null; // RETURN
- }
-
- return getContainingGitDir(base);
-}
+const EXEC_BUFFER = 1024*1024*100;
/**
* Create a branch having the specified `branchName` in the specified `repo`
@@ -84,9 +68,7 @@ exports.createBranchFromHead = co.wrap(function *(repo, branchName) {
const head = yield repo.getHeadCommit();
return yield repo.createBranch(branchName,
head,
- 0,
- repo.defaultSignature(),
- "git-meta branch");
+ 0);
});
/**
@@ -114,13 +96,19 @@ exports.findBranch = co.wrap(function *(repo, branchName) {
* 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);
+ if (!branch.isBranch()) {
+ return null;
+ }
let upstream;
try {
upstream = yield NodeGit.Branch.upstream(branch);
@@ -131,19 +119,58 @@ 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 ConfigUtil.getConfigString(
+ config,
+ `branch.${branch.shorthand()}.pushRemote`);
+
+ // If no 'pushRemote', try to read a 'pushDefault' for the repo.
+
+ if (null === pushRemote) {
+ pushRemote = yield ConfigUtil.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,
};
});
+/**
+ * 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`.
@@ -155,7 +182,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;
}
@@ -183,7 +210,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;
});
@@ -273,17 +300,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
@@ -293,7 +309,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())}.`);
@@ -306,14 +322,14 @@ 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
* @param {String} remote
* @param {String} source
* @param {String} target
- * @param {String} force
+ * @param {ForcePushSpec} force
* @param {Boolean} [quiet]
* @return {String} [return]
*/
@@ -326,7 +342,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;
@@ -335,20 +351,52 @@ 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()) {
+ 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.workdir()}' push ${forceStr} ${remote} ${source}:${target}`;
try {
- const result = yield ChildProcess.exec(execString);
- if (result.stdout && !quiet) {
- console.log(result.stdout);
+ 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,
+ maxBuffer: EXEC_BUFFER
+ });
}
- 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;
}
@@ -367,7 +415,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;
@@ -446,7 +504,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);
@@ -475,7 +535,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);
@@ -484,132 +546,61 @@ 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) {
+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.
try {
yield repo.getCommit(sha);
- return; // RETURN
+ return false; // RETURN
}
catch (e) {
}
- const execString = `git -C '${repo.path()}' fetch -q '${url}' ${sha}`;
+ 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 {
- return yield ChildProcess.exec(execString);
+ yield ChildProcess.exec(execString, {
+ maxBuffer: EXEC_BUFFER
+ });
+ yield repo.getCommit(sha);
}
catch (e) {
- throw new UserError(e.message);
+ throw new UserError(e.message, UserError.CODES.FETCH_ERROR);
}
-});
-
-
-
-/**
- * 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;
- }
- });
-
- const refCheckers = refs.map(checkRef);
-
- yield refCheckers;
-
- // 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;
});
/**
@@ -646,10 +637,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"]);
});
/**
@@ -726,7 +727,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
* `""`.
*
@@ -735,24 +736,21 @@ 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.`);
+ if (filename === "") {
+ throw new UserError(`Empty path`);
}
+ const absPath = path.resolve(cwd, filename);
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`.
@@ -767,7 +765,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];
});
@@ -782,19 +781,39 @@ 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.
+
+ 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.`);
+ }
+
+ }
+
+ if (runHooks && (yield 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");
});
@@ -826,7 +845,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}
@@ -895,17 +914,152 @@ exports.getParentCommit = co.wrap(function *(repo, commit) {
});
/**
- * Returns whether a config variable is, according to git's reckoning,
- * true. That is, it's set to 'true', 'yes', or '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 {NodeGit.Commit} configVar
- * @return boolean
- * @throws if the configuration variable doesn't exist
+ * @param {String} remoteName
+ * @return String
+ * @throws if there's no such named remote
*/
-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");
+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);
+ }
});
+
+/**
+ * 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);
+});
+
+/*
+ * 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);
+ }
+});
+
+/*
+ * 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;
+});
+
+/**
+ * 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, {
+ maxBuffer: EXEC_BUFFER
+ });
+ if (result.error) {
+ throw new UserError("Couldn't run git merge-base: " +
+ result.stderr);
+ }
+ 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;
+});
+
+/**
+ * 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/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/lib/util/hook.js b/node/lib/util/hook.js
new file mode 100644
index 000000000..d3e5a71b5
--- /dev/null
+++ b/node/lib/util/hook.js
@@ -0,0 +1,115 @@
+/*
+ * 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 spawn = require("child-process-promise").spawn;
+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 = co.wrap(function*(repo, name) {
+ assert.isString(name);
+ const hookPath = yield getHooksPath(repo);
+ const absPath = path.resolve(hookPath, name);
+
+ return fs.existsSync(absPath);
+});
+
+/**
+ * Run git-meta hook with given hook name.
+ * Return true on success or hook does not exist, false otherwise.
+ * @async
+ * @param {String} name
+ * @param {String[]} args
+ * @return {Boolean}
+ */
+exports.execHook = co.wrap(function*(repo, name, args=[], env={}) {
+ assert.isString(name);
+
+ const hookPath = yield getHooksPath(repo);
+ const absPath = path.resolve(hookPath, name);
+
+ if (!fs.existsSync(absPath)) {
+ return true;
+ }
+
+ try {
+ const subEnv = {};
+ Object.assign(subEnv, process.env);
+ Object.assign(subEnv, env);
+ yield spawn(absPath, args, { stdio: "inherit", env: subEnv,
+ cwd: repo.workdir()});
+ return true;
+ } catch (e) {
+ if (e.code === "EACCES") {
+ console.log("EACCES: Cannot execute: " + absPath);
+ } else if (e.stdout) {
+ console.log(e.stdout);
+ }
+ return false;
+ }
+});
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);
diff --git a/node/lib/util/merge.js b/node/lib/util/merge.js
deleted file mode 100644
index a7f7fb560..000000000
--- a/node/lib/util/merge.js
+++ /dev/null
@@ -1,362 +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 GitUtil = require("./git_util");
-const Open = require("./open");
-const RepoStatus = require("./repo_status");
-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;
-
-/**
- * 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.
- *
- * @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
- */
-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;
- }
-
- 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)}.`);
- }
-
- 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 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 subs)) {
- return; // RETURN
- }
-
- // Otherwise, we have a submodule that needs to be merged.
-
- const subSha = entry.id.tostrS();
- const subCommitId = NodeGit.Oid.fromString(subSha);
- const sub = subs[path];
- const subHeadSha = sub.commit.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
- }
-
- 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);
- }
-
- // 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);
- }
-
- // 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,
- };
- }
-
- 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,
- };
-});
diff --git a/node/lib/util/merge_common.js b/node/lib/util/merge_common.js
new file mode 100755
index 000000000..26c005b38
--- /dev/null
+++ b/node/lib/util/merge_common.js
@@ -0,0 +1,371 @@
+/*
+ * 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]} doNotRecurse
+ * @param {String|null} commitMessage
+ * @param {() -> Promise(String)} editMessage
+ */
+ constructor(metaRepo,
+ ourCommit,
+ theirCommit,
+ mode,
+ openOption,
+ doNotRecurse,
+ commitMessage,
+ editMessage,
+ authorName,
+ authorEmail,
+ committerName,
+ committerEmail) {
+ 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_doNotRecurse = doNotRecurse;
+ this.d_commitMessage = commitMessage;
+ this.d_editMessage = editMessage;
+ this.d_opener = new Open.Opener(metaRepo, ourCommit);
+ 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;
+ }
+
+ /**
+ * @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 {[String]}
+ */
+ get doNotRecurse() {
+ return this.d_doNotRecurse;
+ }
+
+ /**
+ * @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,
+ this.d_doNotRecurse);
+ }
+ 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 {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}
+ */
+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
new file mode 100644
index 000000000..27775a1ad
--- /dev/null
+++ b/node/lib/util/merge_util.js
@@ -0,0 +1,865 @@
+/*
+ * 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 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");
+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 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;
+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,
+ * 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;
+});
+
+/**
+ * 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 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) {
+ 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}' itself
+(e.g. delete/modify or add/modify)\n`;
+ }
+ }
+ errorMessage += "\nAutomatic merge failed\n";
+ return errorMessage;
+};
+
+
+/**
+ * Perform a fast-forward merge in the specified `repo` to the
+ * specified `commit`. The behavior is undefined unless `commit`
+ * is different from but descendant of the HEAD commit in `repo`.
+ *
+ * @param {NodeGit.Repository} repo
+ * @param {MergeCommon.MODE} mode
+ * @param {NodeGit.Commit} commit
+ */
+exports.fastForwardMerge = co.wrap(function *(repo, commit) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(commit, NodeGit.Commit);
+
+ // Remember the current branch; the checkoutCommit function will move it.
+
+ const branch = yield repo.getCurrentBranch();
+
+ yield Checkout.checkoutCommit(repo, commit, false);
+
+ // If we were on a branch, make it current again.
+
+ if (branch.isBranch()) {
+ yield branch.setTarget(commit, "ffwd merge");
+ yield repo.setHead(branch.name());
+ }
+});
+
+/**
+ * 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
+ * @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
+ * is required, this is the merge commit.
+ */
+exports.makeMetaCommit = co.wrap(function *(repo,
+ indexToWrite,
+ ourCommit,
+ theirCommit,
+ commitMessage,
+ refToUpdate,
+ author,
+ committer) {
+ const id = yield indexToWrite.writeTreeTo(repo);
+ const metaCommit = yield repo.createCommit(refToUpdate,
+ author,
+ committer,
+ commitMessage,
+ id,
+ [ourCommit, theirCommit]);
+ const commitSha = metaCommit.tostrS();
+ return {
+ metaCommit: commitSha,
+ infoMessage: `Merge commit created at ` +
+ `${colors.green(commitSha)}.`,
+ };
+});
+
+/**
+ * Merge the specified `subName` and update the in memory `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} 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}
+ * @return {String|null} return.mergeSha
+ * @return {String|null} return.conflictSha
+ * @return {String []} return.conflictPaths
+ */
+exports.mergeSubmodule = co.wrap(function *(metaIndex,
+ subName,
+ change,
+ message,
+ opener,
+ fetcher,
+ author,
+ committer,
+ 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(author, NodeGit.Signature);
+ assert.instanceOf(committer, 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;
+ 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);
+
+ const ourSha = change.ourSha;
+ const ourCommit = yield subRepo.getCommit(ourSha);
+
+ const result = {
+ mergeSha: null,
+ conflictSha: null,
+ conflictPaths: [],
+ };
+
+ // See if up-to-date
+ if (yield NodeGit.Graph.descendantOf(subRepo, ourSha, theirSha)) {
+ yield CherryPickUtil.addSubmoduleCommit(metaIndex, subName, ourSha);
+ 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.error(`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,
+ });
+ }
+
+ // 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()) {
+ result.conflictPaths =
+ Object.keys(StatusUtil.readConflicts(subIndex, []));
+ 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,
+ });
+ }
+ 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.
+ if (!isHalfOpened) {
+ subIndex = yield subRepo.index();
+ }
+
+ const refToUpdate = isHalfOpened ? null : "HEAD";
+ const treeId = yield subIndex.writeTreeTo(subRepo);
+ const mergeCommit = yield subRepo.createCommit(refToUpdate,
+ author,
+ committer,
+ 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 metaIndex.conflictRemove(subName);
+ }
+ return result;
+});
+
+/**
+ * Perform preparation work before merge, including
+ * 1. locate merge base
+ * 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.
+ *
+ * @async
+ * @param {MergeContext} context
+ * @return {MergeStepResult}
+ */
+const mergeStepPrepare = co.wrap(function *(context) {
+ assert.instanceOf(context, MergeContext);
+
+ 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) {
+ errorMessage = "No commits in common with" +
+ `${colors.red(GitUtil.shortSha(ourCommitSha))} and ` +
+ `${colors.red(GitUtil.shortSha(theirCommitSha))}`;
+ return MergeStepResult.error(errorMessage); // RETURN
+ }
+
+ 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
+ }
+ }
+
+ if (ourCommitSha === theirCommitSha) {
+ infoMessage = "Nothing to do.";
+ return MergeStepResult.justMeta(infoMessage, theirCommit); // RETURN
+ }
+
+ const upToDate = yield NodeGit.Graph.descendantOf(metaRepo,
+ ourCommitSha,
+ theirCommitSha);
+
+ if (upToDate) {
+ return MergeStepResult.justMeta(infoMessage, ourCommitSha); // RETURN
+ }
+ return MergeStepResult.empty();
+});
+
+/**
+ * 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();
+});
+
+/**
+ * @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 author = yield context.getAuthor();
+ 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 FORCE_BARE mode
+ if (forceBare && Object.keys(changes.conflicts).length > 0) {
+ conflictMessage = getBareMergeConflictsMessage(changes.conflicts);
+ return MergeStepResult.error(conflictMessage); // RETURN
+ }
+
+ // deal with simple changes
+ if (forceBare) {
+ // 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.urls);
+ }
+
+ const message = yield context.getCommitMessage();
+ if ("" === message) {
+ return MergeStepResult.empty();
+ }
+
+ const merges = {
+ conflicts: {},
+ conflictPaths: {},
+ 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,
+ changes.changes[subName],
+ message,
+ opener,
+ fetcher,
+ author,
+ committer,
+ openOption);
+ if (null !== subResult.mergeSha) {
+ merges.commits[subName] = subResult.mergeSha;
+ }
+ 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 = getBareMergeConflictsMessage(merges.conflictPaths);
+ } else {
+ conflictMessage =
+ yield CherryPickUtil.writeConflicts(repo,
+ index,
+ changes.conflicts);
+ ///
+ Object.keys(merges.conflicts).sort().forEach(name => {
+ conflictMessage +=
+ SubmoduleRebaseUtil.subConflictErrorMessage(name);
+ });
+ }
+
+ // finishing merge for interactive merges
+ // 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) {
+ yield CherryPickUtil.closeSubs(opener, merges);
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+ }
+
+ 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);
+ }
+
+ let infoMessage = `Merging meta-repo commits ` +
+ `${colors.green(ourCommitSha)} and ` +
+ `${colors.green(theirCommitSha)}`;
+ const metaCommitRet = yield exports.makeMetaCommit(repo,
+ index,
+ ourCommit,
+ theirCommit,
+ message,
+ refToUpdate,
+ author,
+ committer);
+ infoMessage += "\n" + metaCommitRet.infoMessage;
+ return new MergeStepResult(infoMessage,
+ null,
+ metaCommitRet.metaCommit,
+ merges.commits);
+});
+
+/**
+ * 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,
+ doNotRecurse,
+ commitMessage,
+ editMessage) {
+ // pack and validate merging objects
+ const context = new MergeContext(repo,
+ ourCommit,
+ theirCommit,
+ mode,
+ openOption,
+ doNotRecurse,
+ commitMessage,
+ 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,
+ submoduleCommits: {},
+ errorMessage: null,
+ };
+ const mergeAsyncSteps = [
+ mergeStepPrepare,
+ mergeStepFF,
+ mergeStepMergeSubmodules,
+ ];
+
+ for (const asyncStep of mergeAsyncSteps) {
+ const ret = yield asyncStep(context);
+ if (null !== ret.infoMessage) {
+ console.error(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;
+});
+
+/**
+ * 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 = NodeGit.Index.entryStage(entry);
+ if (RepoStatus.STAGE.OURS === stage &&
+ 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 seq = yield checkForMerge(repo.path());
+ 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(seq.target.sha)}.`);
+
+ 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 = 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
+ // changes we need to make a commit.
+
+ const status = yield StatusUtil.getRepoStatus(subRepo, {
+ showMetaChanges: true,
+ });
+ if (!status.isIndexClean()) {
+ const id = yield subRepo.createCommitOnHead([],
+ sig,
+ sig,
+ seq.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(subSeq.target.sha);
+ const treeId = yield subIndex.writeTreeTo(subRepo);
+ const id = yield subRepo.createCommit("HEAD",
+ sig,
+ sig,
+ subSeq.message,
+ treeId,
+ [head, mergeHead]);
+ yield SequencerStateUtil.cleanSequencerState(subRepo.path());
+ result.submoduleCommits[subPath] = id.tostrS();
+ }
+ yield index.addByPath(subPath);
+ yield index.conflictRemove(subPath);
+ });
+ const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo);
+ yield DoWorkQueue.doInParallel(openSubs,
+ continueSub,
+ {failMsg: "Merge in submodule failed."});
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+
+ if ("" !== errorMessage) {
+ throw new UserError(errorMessage);
+ }
+ const treeId = yield index.writeTreeTo(repo);
+
+ 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",
+ sig,
+ sig,
+ seq.message,
+ treeId,
+ [head, mergeHead]);
+ console.log(
+ `Finished with merge commit ${colors.green(metaCommit.tostrS())}`);
+ yield SequencerStateUtil.cleanSequencerState(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, {
+ maxBuffer: 1024*1024*100
+ });
+});
+
+/**
+ * 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);
+
+ yield checkForMerge(repo.path());
+
+ 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 === 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,
+ commit,
+ NodeGit.Reset.TYPE.SOFT);
+ yield resetMerge(subRepo);
+ }
+ yield SequencerStateUtil.cleanSequencerState(subRepo.path());
+ yield index.addByPath(subName);
+ });
+ yield DoWorkQueue.doInParallel(openSubs, abortSub);
+ yield index.conflictCleanup();
+ 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 0d21492b6..6e2d3663d 100644
--- a/node/lib/util/open.js
+++ b/node/lib/util/open.js
@@ -35,14 +35,58 @@
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");
-const DeinitUtil = require("./deinit_util");
+const Hook = require("../util/hook");
+const SparseCheckoutUtil = require("./sparse_checkout_util");
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;
+
+/**
+ * @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`
@@ -50,27 +94,39 @@ 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.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
* @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);
}
+
const metaRepoUrl = yield fetcher.getMetaOriginUrl();
const metaRepo = fetcher.repo;
const submoduleUrl = yield fetcher.getSubmoduleUrl(submoduleName);
+
+ const wasOpen = new Opener(metaRepo, null).isOpen(submoduleName);
+
// Set up the submodule.
const submoduleRepo = yield SubmoduleConfigUtil.initSubmoduleAndRepo(
@@ -78,8 +134,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.
@@ -88,7 +152,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;
}
@@ -97,48 +161,44 @@ exports.openOnCommit = co.wrap(function *(fetcher,
const commit = yield submoduleRepo.getCommit(submoduleSha);
yield GitUtil.setHeadHard(submoduleRepo, commit);
- return submoduleRepo;
-});
+ // If we're in sparse mode, we need to add a submodule to the
+ // `.git/info/sparse-checkout` file so that it's "visible".
-/**
- * @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 (yield SparseCheckoutUtil.inSparseMode(metaRepo)) {
+ yield SparseCheckoutUtil.addToSparseCheckoutFile(metaRepo,
+ submoduleName);
}
- /**
- * @property {NodeGit.Repository} the repo associated with this object
- */
- get repo() {
- return this.d_repo;
+ if (!wasOpen) {
+ // Run post-open-submodule hook with successfully-opened submodules
+ yield Hook.execHook(metaRepo, "post-open-submodule", [submoduleName]);
}
-}
+
+ return submoduleRepo;
+});
Opener.prototype._initialize = co.wrap(function *() {
if (null === this.d_commit) {
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);
+
+ // 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);
+ 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();
@@ -173,53 +233,166 @@ 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}
+ */
+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 true if the submodule is opened nor half opened.
*
+ * @async
+ * @param {String} subName
+ * @return {Boolean}
+ */
+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_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.isOpen = co.wrap(function *(subName) {
+Opener.prototype.isHalfOpened = co.wrap(function *(subName) {
if (!this.d_initialized) {
yield this._initialize();
}
- return this.d_openSubs.has(subName) || (subName in this.d_subRepos);
+ 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
*
- * @param {String} subName
+ * Note that after opening one or more submodules,
+ * `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
* @return {NodeGit.Repository}
*/
-Opener.prototype.getSubrepo = co.wrap(function *(subName) {
+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 (this.d_openSubs.has(subName)) {
- subRepo = yield SubmoduleUtil.getRepo(this.d_repo, subName);
+ if (SUB_OPEN_OPTION.FORCE_OPEN !== openOption) {
+ subRepo = this.d_cachedAbsorbedSubs[subName];
+ if (undefined !== subRepo) {
+ return subRepo;
+ }
}
- 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);
+ const openable = 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/print_status_util.js b/node/lib/util/print_status_util.js
index a6c9c258e..157583ebb 100644
--- a/node/lib/util/print_status_util.js
+++ b/node/lib/util/print_status_util.js
@@ -40,9 +40,10 @@ 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 SequencerState = require("./sequencer_state");
+const RepoStatus = require("./repo_status");
+const TextUtil = require("./text_util");
/**
* This value-semantic class describes a line entry to be printed in a status
@@ -50,9 +51,9 @@ const RepoStatus = require("../util/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;
@@ -71,27 +72,34 @@ 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: ";
}
- result += path.relative(cwd, this.path);
+ 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) {
+ filename = ".";
+ }
+ result += filename;
result = color(result);
if ("" !== this.detail) {
result += ` (${this.detail})`;
@@ -109,11 +117,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));
};
/**
@@ -340,19 +344,35 @@ exports.accumulateStatus = function (status) {
};
/**
- * Return a message describing the specified `rebase`.
+ * Return the command to which the specified sequencer `type` corresponds.
*
- * @param {Rebase}
+ * @param {SequencerState.TYPE} type
* @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}'.
- (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)
+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, `unhandled sequencer type: ${type}`);
+ }
+};
+
+/**
+ * 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)
`;
};
@@ -367,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`;
};
@@ -385,12 +408,12 @@ exports.printRepoStatus = function (status, cwd) {
let result = "";
- if (null !== status.rebase) {
- result += exports.printRebase(status.rebase);
- }
-
result += exports.printCurrentBranch(status);
+ if (null !== status.sequencerState) {
+ result += exports.printSequencer(status.sequencerState);
+ }
+
let changes = "";
const fileStatuses = exports.accumulateStatus(status);
const staged = fileStatuses.staged;
@@ -437,34 +460,102 @@ 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`.
+ * 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
+ * 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/pull.js b/node/lib/util/pull.js
index 443cead5f..28c11f39d 100644
--- a/node/lib/util/pull.js
+++ b/node/lib/util/pull.js
@@ -32,13 +32,13 @@
const assert = require("chai").assert;
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
@@ -54,40 +54,26 @@ 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
// 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());
- yield RebaseUtil.rebase(metaRepo, remoteCommit, status);
+ const result = yield RebaseUtil.rebase(metaRepo, remoteCommit, status);
+ if (null !== result.errorMessage) {
+ throw new UserError(result.errorMessage);
+ }
});
/**
- * 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
@@ -97,21 +83,14 @@ ${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;
}
- 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/push.js b/node/lib/util/push.js
index b4c438386..04ea6fe1f 100644
--- a/node/lib/util/push.js
+++ b/node/lib/util/push.js
@@ -34,18 +34,155 @@
* 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 ForcePushSpec = require("./force_push_spec");
const GitUtil = require("./git_util");
const SubmoduleUtil = require("./submodule_util");
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, 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
+ * 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 {NodeGit.Commit} commit
+ */
+exports.getClosePushedCommit = co.wrap(function*(repo, commit) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ 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. 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);
+ }
+ }
+ if (excludeRefs.length === 0) {
+ return null;
+ }
+
+ const args = ["-C", repo.workdir(), "rev-list", "--reverse",
+ "--topo-order", commit, ...excludeRefs];
+ let result = yield ChildProcess.execFile("git", args, {
+ maxBuffer: 1024*1024*100
+ });
+ 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);
+
+ // 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.
+ *
+ * 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
+ * @param {String} source
+ * @param {NodeGit.Commit} commit
+ */
+exports.getPushMap = co.wrap(function*(repo, source, commit) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.isString(source);
+ assert.instanceOf(commit, NodeGit.Commit);
+
+ const baseCommit = yield exports.getClosePushedCommit(repo, commit);
+ let baseTree;
+ if (null !== baseCommit) {
+ baseTree = yield baseCommit.getTree();
+ } else {
+ baseTree = EMPTY_TREE;
+ }
+
+ const tree = yield commit.getTree();
+ const diff = yield NodeGit.Diff.treeToTree(repo, baseTree, tree, null);
+ const changes = SubmoduleUtil.getSubmoduleChangesFromDiff(diff, true);
+
+ const pushMap = {};
+ for (const path of Object.keys(changes)) {
+ const change = changes[path];
+ if (!change.deleted) {
+ pushMap[path] = change.newSha;
+ }
+ }
+
+ 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]);
+ } catch (e) {
+ 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.
@@ -79,59 +216,60 @@ 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 remote;
- try {
- remote = yield repo.getRemote(remoteName);
- }
- catch (e) {
- throw new UserError(`No remote named ${colors.red(remoteName)}.`);
+ 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 remoteUrl = yield GitUtil.getRemoteUrl(repo, remote);
+ const sha = annotatedCommit.id();
+ const commit = yield repo.getCommit(sha);
// First, push the submodules.
+ const pushMap = yield exports.getPushMap(repo, source, commit);
let errorMessage = "";
- const shas = yield SubmoduleUtil.getSubmoduleShasForBranch(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.
+ if (!(subName in urls)) {
+ throw new UserError(
+ `The submodule ${subName} doesn't have an entry in .gitmodules`
+ );
+ }
const subUrl = SubmoduleConfigUtil.resolveSubmoduleUrl(remoteUrl,
urls[subName]);
// 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,
+ ForcePushSpec.Force,
true);
if (null !== pushResult) {
errorMessage +=
`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/read_repo_ast_util.js b/node/lib/util/read_repo_ast_util.js
index c9c214835..7cdc1fdbd 100644
--- a/node/lib/util/read_repo_ast_util.js
+++ b/node/lib/util/read_repo_ast_util.js
@@ -44,8 +44,13 @@ 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");
+const FILEMODE = NodeGit.TreeEntry.FILEMODE;
+const File = RepoAST.File;
+
/**
* Load the submodules objects from the specified `repo` on the specified
* `commitId`.
@@ -81,16 +86,49 @@ 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
+ }
+ let isExecutable = false;
+ try {
+ yield fs.access(absPath, fs.constants.X_OK);
+ isExecutable = true;
+ } catch (e) {
+ // cannot execute
+ }
+ if (undefined !== data) {
+ workdir[filePath] = new File(data, isExecutable);
+ }
+ });
+
+ const readEntryFile = co.wrap(function *(entry) {
+ if (undefined === entry) {
+ return null; // RETURN
+ }
+ const oid = entry.id;
+ if (FILEMODE.COMMIT === entry.mode) {
+ return new RepoAST.Submodule("", oid.tostrS());
+ }
+ const isExecutable = FILEMODE.EXECUTABLE === entry.mode;
+ const blob = yield repo.getBlob(oid);
+ return new File(blob.toString(), isExecutable);
+ });
+
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.
@@ -107,6 +145,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;
}
@@ -125,12 +180,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);
}
}
@@ -141,11 +192,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);
}
}
}
@@ -184,19 +231,34 @@ 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);
- const branches = yield repo.getReferences(NodeGit.Reference.TYPE.LISTALL);
+ if (includeRefsCommits === undefined) {
+ includeRefsCommits = false;
+ }
+ assert.instanceOf(repo, NodeGit.Repository);
+ const branches = yield repo.getReferences();
let commits = {};
let branchTargets = {};
let refTargets = {};
@@ -269,8 +331,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);
}
}
@@ -350,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
@@ -390,10 +459,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.
@@ -440,12 +516,15 @@ 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);
- const WD_UNINITIALIZED = (1 << 7); // means "closed"
- if (!(status & WD_UNINITIALIZED)) {
+ 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);
@@ -503,6 +582,15 @@ exports.readRAST = co.wrap(function *(repo) {
yield loadCommit(NodeGit.Oid.fromString(rebase.onto));
}
+ 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,
@@ -515,6 +603,8 @@ exports.readRAST = co.wrap(function *(repo) {
workdir: workdir,
openSubmodules: openSubmodules,
rebase: rebase,
+ sequencerState: sequencer,
bare: bare,
+ sparse: sparse,
});
});
diff --git a/node/lib/util/rebase_util.js b/node/lib/util/rebase_util.js
index b58bcad53..05e25653b 100644
--- a/node/lib/util/rebase_util.js
+++ b/node/lib/util/rebase_util.js
@@ -33,681 +33,367 @@
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");
-const DeinitUtil = require("./deinit_util");
-const Open = require("./open");
+const Checkout = require("./checkout");
+const CherryPickUtil = require("./cherry_pick_util");
+const Reset = require("./reset");
const GitUtil = require("./git_util");
-const RepoStatus = require("./repo_status");
-const RebaseFileUtil = require("./rebase_file_util");
-const SubmoduleConfigUtil = require("./submodule_config_util");
-const SubmoduleUtil = require("./submodule_util");
+const Hook = require("./hook");
+const SequencerState = require("./sequencer_state");
+const SequencerStateUtil = require("./sequencer_state_util");
+const StatusUtil = require("./status_util");
+const SubmoduleRebaseUtil = require("./submodule_rebase_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);
-});
+const CommitAndRef = SequencerState.CommitAndRef;
/**
- * Call `next` on the specified `rebase`; return the rebase operation for the
- * rebase or null if there is no further operation.
+ * Accumulate specfied `intermediate` result into `result`, gathering new and
+ * rewritten submodule commits generated from a single rebase operation.
*
- * @async
- * @private
- * @param {NodeGit.Rebase} rebase
- * @return {RebaseOperation|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 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);
- }));
+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;
}
-});
+ result.errorMessage = intermediate.errorMessage;
+}
/**
- * 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.
+ * If the specified `seq` has a non-null ref that is a branch, make it the
+ * current branch in the specified `repo`.
*
* @param {NodeGit.Repository} repo
- * @param {NodeGit.Rebase} rebase
+ * @param {SequencerState} seq
*/
-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 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.
- *
- * @param {NodeGit.Repository} rep
- * @param {String} name
- * @param {NodeGit.Rebase} rebase
- * @param {NodeGit.RebaseOperation} op
- * @return {Object}
- * @return {Object} return.commits
- * @return {String|null} return.error
- */
-const processSubmoduleRebase = co.wrap(function *(repo,
- name,
- rebase,
- op) {
- const result = {
- commits: {},
- error: 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
+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);
}
- 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);
}
- return result;
});
/**
- * 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.error failure message if non-null
+ * Throw a `UserError` unlessn the specified `seq` is non-null and has type
+ * `REBASE`.
+ * @param {SequencerState} seq
*/
-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())}.`);
-
- let op = yield callNext(rebase);
- return yield processSubmoduleRebase(repo, name, rebase, op);
-});
+function ensureRebaseInProgress(seq) {
+ if (null !== seq) {
+ assert.instanceOf(seq, SequencerState);
+ }
-/**
- * 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;
+ if (null === seq || SequencerState.TYPE.REBASE !== seq.type) {
+ throw new UserError("Error: no rebase in progress");
}
- return false;
-});
+ return seq;
+}
/**
- * 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.
+ * 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.
*
- * @return {Object}
- * @return {String|null} return.error
- * @return {String|null} return.subToRebase
- * @return {String|undefined} return.rebaseFrom only if `subToRebase !== null`
+ * @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 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 = RepoStatus.getStage(entry.flags);
-
+exports.runRebase = co.wrap(function *(repo, seq) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(seq, SequencerState);
const result = {
- error: null,
- subToRebase: null,
+ metaCommits: {},
+ submoduleCommits: {},
+ errorMessage: 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);
- }
+ 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,
+ "rebase");
+ if (null !== cherryResult.newMetaCommit) {
+ result.metaCommits[cherryResult.newMetaCommit] = sha;
}
- break;
- case RepoStatus.STAGE.OURS:
- if (isSubmodule) {
- result.subToRebase = entry.path;
- result.rebaseFrom = id.tostrS();
+ accumulateRebaseResult(result, cherryResult);
+ if (null !== result.errorMessage) {
+ return result;
}
- else {
- if (SubmoduleConfigUtil.modulesFileName === entry.path) {
- const succeeded = yield mergeModulesFile(metaRepo,
- index,
- fromCommit,
- ontoCommit);
- if (succeeded) {
- break; // BREAK
- }
- }
- result.error =
- `There is a conflict in ${colors.red(entry.path)}.\n`;
- }
- break;
}
- return result;
-});
-/**
- * Close the submodules opened by the specified `opener` that have no entry in
- * the specified `subCommits` map or the specified `conflicted` set.
- *
- * @param {Open.Opener} opener
- * @param {Object} subCommits sub name to commit map
- * @param {Set} conflicted name of subs with conflicts.
- */
-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);
- }
- }));
+ yield restoreHeadBranch(repo, seq);
+ yield SequencerStateUtil.cleanSequencerState(repo.path());
+ console.log("Finished rebase.");
+ yield Hook.execHook(repo, "post-rewrite", ["rebase"]);
+ return result;
});
/**
- * 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`.
+ * 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.
*
- * @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
+ * @async
+ * @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
*/
-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) {
- console.log("E:", ret.error);
- errorMessage += ret.error + "\n";
- }
- else if (null !== ret.subToRebase) {
- subsToRebase[ret.subToRebase] = ret.rebaseFrom;
- }
- }
+exports.rebase = co.wrap(function *(repo, onto) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(onto, NodeGit.Commit);
- // Clean up conflicts unless we found one in the meta-repo that was not
- // a submodule change.
+ // First, make sure we're in a state in which we can run a rebase.
- if ("" === errorMessage) {
- yield index.conflictCleanup();
+ 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 conflicted = new Set();
+ const result = {
+ metaCommits: {},
+ submoduleCommits: {},
+ errorMessage: null,
+ };
- // Process submodule rebases.
+ const headCommit = yield repo.getHeadCommit();
+ const headRef = yield repo.head();
+ const headSha = headCommit.id().tostrS();
+ const ontoSha = onto.id().tostrS();
- 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;
- }
+ // First, see if 'commit' already exists in the current history. If so, we
+ // can exit immediately.
- if (null !== ret.error) {
- errorMessage += ret.error + "\n";
- conflicted.add(name);
- }
+ if (yield GitUtil.isUpToDate(repo, headSha, ontoSha)) {
+ const name = headRef.shorthand();
+ console.log(`${colors.green(name)} is up-to-date.`);
+ return result; // RETURN
}
- yield closeAutoOpenedSubmodules(opener,
- result.submoduleCommits,
- conflicted);
-
- if ("" !== errorMessage) {
- throw new UserError(errorMessage);
+ 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)}`);
+ yield Hook.execHook(repo, "post-checkout", [headSha, ontoSha, "1"]);
+ return result; // RETURN
}
- // Write the index and new commit, recording a mapping from the
- // original commit ID to the new one.
+ console.log("First, rewinding head to replay your work on top of it...");
+
+ yield Checkout.checkoutCommit(repo, onto, true);
- 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 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,
});
- const newCommitSha = newCommit.tostrS();
- const originalSha = op.id().tostrS();
- if (originalSha !== newCommitSha) {
- result.metaCommits[newCommitSha] = originalSha;
- }
+ return yield exports.runRebase(repo, 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.
+ * Abort the rebase in progress on the specified `repo` and all open
+ * submodules, returning them to their previous heads and checking them out.
+ * The behavior is undefined unless the specified `repo` has a rebase in
+ * progress.
*
- * @param {NodeGit.Repository} metaRepo
- * @param {(openSubs, getSubmoduleRebaser) => NodeGit.Rebase} initializer
- * @param {NodeGit.Commit} fromCommit
- * @param {NodeGit.Commit} ontoCommit
+ * @param {NodeGit.Repository} repo
*/
-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;
- const result = {
- metaCommits: {},
- submoduleCommits: init.submoduleCommits,
- };
-
- // 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 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);
- yield openSubs.map(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);
- }
- }));
- }
+exports.abort = co.wrap(function *(repo) {
+ assert.instanceOf(repo, NodeGit.Repository);
- yield callFinish(metaRepo, rebase);
- return result;
+ const seq = yield SequencerStateUtil.readSequencerState(repo.path());
+ ensureRebaseInProgress(seq);
+ const commit = yield repo.getCommit(seq.originalHead.sha);
+ yield Reset.reset(repo, commit, Reset.TYPE.MERGE);
+ yield restoreHeadBranch(repo, seq);
+ yield SequencerStateUtil.cleanSequencerState(repo.path());
});
/**
- * 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.
+ * 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.
*
- * @async
- * @param {NodeGit.Repository} metaRepo
- * @param {NodeGit.Commit} commit
+ * @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.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.continue = co.wrap(function *(repo) {
+ assert.instanceOf(repo, NodeGit.Repository);
+
+ const status = yield StatusUtil.getRepoStatus(repo);
+ const seq = status.sequencerState;
+ ensureRebaseInProgress(seq);
+
+ if (status.isConflicted()) {
+ throw new UserError("Cannot continue rebase due to conflicts.");
}
- const currentBranch = yield metaRepo.getBranch(currentBranchName);
- const fromCommitId = currentBranch.target();
- const fromCommit = yield metaRepo.getCommit(fromCommitId);
- const commitId = commit.id();
+ const currentSha = seq.commits[seq.currentCommit];
+ const currentCommit = yield repo.getCommit(currentSha);
+ const index = yield repo.index();
- // First, see if 'commit' already exists in the current history. If so, we
- // can exit immediately.
+ // 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.
- if (yield GitUtil.isUpToDate(metaRepo,
- fromCommitId.tostrS(),
- commitId.tostrS())) {
- console.log(`${colors.green(currentBranch.shorthand())} is \
-up-to-date.`);
- return {
- metaCommits: {},
- submoduleCommits: {},
- };
+ 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 callNext(rebase);
- return {
- rebase: rebase,
- submoduleCommits: {},
- };
- }));
+ // Then, call back to `runRebase` to complete any remaining commits.
+
+ const nextSeq = seq.copy({
+ currentCommit: seq.currentCommit + 1,
});
- return yield driveRebase(metaRepo, initialize, fromCommit, commit);
+ const nextResult = yield exports.runRebase(repo, nextSeq);
+ Object.assign(result.metaCommits, nextResult.metaCommits);
+ accumulateRebaseResult(result, nextResult);
+ return result;
});
/**
- * Abort the rebase in progress on the specified `repo` and all open
- * submodules, returning them to their previous heads and checking them out.
- * The behavior is undefined unless the specified `repo` has a rebase in
- * progress.
+ * 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.abort = co.wrap(function *(repo) {
+exports.listRebaseCommits = co.wrap(function *(repo, from, onto) {
assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(from, NodeGit.Commit);
+ assert.instanceOf(onto, NodeGit.Commit);
- yield SubmoduleUtil.cacheSubmodules(repo, co.wrap(function*() {
- const rebase = yield NodeGit.Rebase.open(repo);
- rebase.abort();
- }));
+ const ontoSha = onto.id().tostrS();
+ const seen = new Set([ontoSha]); // shas that stop traversal
+ const result = [];
+ const todo = []; // { sha: String | null, parents: [Commit]}
- const head = yield repo.head();
- console.log(`Set HEAD back to ${colors.green(head.target().tostrS())}.`);
+ // 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.
- // 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 enqueue = co.wrap(function *(commit) {
+ const sha = commit.id().tostrS();
- const openSubs = yield SubmoduleUtil.listOpenSubmodules(repo);
- yield openSubs.map(co.wrap(function *(name) {
- const subRepo = yield SubmoduleUtil.getRepo(repo, name);
- if (!subRepo.isRebasing()) {
+ // 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
}
- 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)}.`);
- }));
-});
+ seen.add(sha);
-/**
- * Continue the rebase in progress on the specified `repo`.
- *
- * @param {NodeGit.Repository} repo
- */
-exports.continue = co.wrap(function *(repo) {
- assert.instanceOf(repo, NodeGit.Repository);
+ // Skip this commit if it's an ancestor of `onto`.
- const rebaseInfo = yield RebaseFileUtil.readRebase(repo.path());
- 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);
+ const inHistory = yield NodeGit.Graph.descendantOf(repo, ontoSha, sha);
+ if (inHistory) {
+ return; // RETURN
+ }
+ const parents = yield commit.getParents();
- let errorMessage = "";
+ // 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.
- const initializer = co.wrap(function *(metaRepo) {
- 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);
+ todo.push({
+ sha: 1 >= parents.length ? sha : null,
+ parents: parents.reverse(),
});
- 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);
+ });
+
+ 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);
}
- if ("" !== errorMessage) {
- throw new UserError(errorMessage);
- }
- return {
- rebase: rebase,
- submoduleCommits: subCommits,
- };
- });
- return yield driveRebase(repo, initializer, fromCommit, ontoCommit);
+ }
+ return result;
});
diff --git a/node/lib/util/repo_ast.js b/node/lib/util/repo_ast.js
index ee0e97fe2..f58739d0a 100644
--- a/node/lib/util/repo_ast.js
+++ b/node/lib/util/repo_ast.js
@@ -37,7 +37,63 @@ const assert = require("chai").assert;
const deeper = require("deeper");
const deepCopy = require("deepcopy");
-const Rebase = require("./rebase");
+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}
@@ -114,12 +170,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 ||
+ ancestor instanceof File ||
+ ancestor instanceof Submodule, ancestor);
+ assert(null === our ||
+ our instanceof File ||
+ our instanceof Submodule, our);
+ assert(null === their ||
+ their instanceof File ||
+ 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
@@ -159,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 = "";
@@ -290,20 +432,23 @@ 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]
- * @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 {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 {SequencerState} [args.sequencerState]
+ * @param {Boolean} [args.sparse]
*/
constructor(args) {
if (undefined === args) {
@@ -469,6 +614,21 @@ 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.
@@ -476,22 +636,6 @@ in commit ${id}.`);
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 = {};
@@ -517,16 +661,36 @@ 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) {
- assert.isString(change);
+ assert.instanceOf(change,
+ File,
+ `workdir change at ${path}`);
}
this.d_workdir[path] = change;
}
}
+ // 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 ||
+ change instanceof File ||
+ 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.
@@ -549,6 +713,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);
}
@@ -640,6 +811,20 @@ in commit ${id}.`);
return this.d_rebase;
}
+ /**
+ * @property {SequencerState} null unless a sequence operation is ongoing
+ */
+ get sequencerState() {
+ 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
@@ -699,7 +884,10 @@ in commit ${id}.`);
openSubmodules: ("openSubmodules" in args) ?
args.openSubmodules : this.d_openSubmodules,
rebase: ("rebase" in args) ? args.rebase : this.d_rebase,
+ 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,
});
}
@@ -764,16 +952,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.File = File;
AST.Rebase = Rebase;
AST.Remote = Remote;
+AST.SequencerState = SequencerState;
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..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.
@@ -282,9 +284,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 +296,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 +329,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;
@@ -339,15 +346,71 @@ 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,
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);
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 7d7d2449f..a330aca2f 100644
--- a/node/lib/util/repo_ast_util.js
+++ b/node/lib/util/repo_ast_util.js
@@ -40,6 +40,10 @@ 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;
// Begin module-local methods
@@ -131,12 +135,26 @@ function diffChanges(actual, expected) {
function compare(path) {
const actualChange = actual[path];
const expectedChange = expected[path];
- let different = actualChange !== expectedChange;
- if (different &&
- actualChange instanceof RepoAST.Submodule &&
- expectedChange instanceof RepoAST.Submodule) {
- different = actualChange.url !== expectedChange.url ||
- actualChange.sha !== expectedChange.sha;
+ let different;
+ 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 (actualChange instanceof RepoAST.Conflict &&
+ expectedChange instanceof RepoAST.Conflict) {
+ different = !actualChange.equal(expectedChange);
+ } else {
+ different = actualChange !== null || expectedChange !== null;
}
if (different) {
result.push(`\
@@ -195,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])}`
);
@@ -227,7 +245,6 @@ ${colorAct(actual.branches[branch])}`
*/
function diffASTs(actual, expected) {
let result = [];
- const indent = "".repeat(4);
// First, check the commits
@@ -241,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,
@@ -265,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,
@@ -408,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.
@@ -418,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
@@ -436,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,
@@ -472,6 +479,33 @@ ${colorExp(expected.rebase.onto)} but got ${colorAct(actual.rebase.onto)}.`);
}
}
+ // 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}`);
+ }
+
+ // 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;
}
@@ -537,7 +571,6 @@ exports.assertEqualRepoMaps = function (actual, expected, message) {
assert.isString(message);
}
let result = [];
- const indent = "".repeat(4);
// First, check the commits
@@ -553,9 +586,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,
@@ -575,7 +606,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
@@ -588,10 +620,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) {
@@ -600,18 +630,29 @@ 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);
}
+ 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;
}
@@ -628,6 +669,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.
@@ -697,6 +742,21 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) {
mapCommitId(rebase.onto));
}
+ 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,
+ message: sequencer.message,
+ });
+ }
+
return ast.copy({
commits: commits,
branches: branches,
@@ -709,6 +769,7 @@ exports.mapCommitsAndUrls = function (ast, commitMap, urlMap) {
workdir: mapChanges(ast.workdir),
openSubmodules: openSubmodules,
rebase: rebase,
+ sequencerState: sequencer,
});
};
@@ -796,3 +857,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/repo_status.js b/node/lib/util/repo_status.js
index fe54eae84..f3e40f6e8 100644
--- a/node/lib/util/repo_status.js
+++ b/node/lib/util/repo_status.js
@@ -32,12 +32,62 @@
const assert = require("chai").assert;
const Rebase = require("./rebase");
+const SequencerState = require("./sequencer_state");
/**
* 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
@@ -46,6 +96,7 @@ const Rebase = require("./rebase");
// 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
};
@@ -59,7 +110,6 @@ const FILESTATUS = {
MODIFIED: 0,
ADDED: 1,
REMOVED: 2,
- CONFLICTED: 3,
RENAMED: 4,
TYPECHANGED: 5
};
@@ -315,14 +365,15 @@ 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}
*/
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()));
@@ -384,29 +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 {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 {SequencerState}
+ * [args.sequencerState] state of sequencer
*/
constructor(args) {
if (undefined === args) {
@@ -421,6 +462,7 @@ class RepoStatus {
this.d_workdir = {};
this.d_submodules = {};
this.d_rebase = null;
+ this.d_sequencerState = null;
if ("currentBranchName" in args) {
if (null !== args.currentBranchName) {
@@ -439,7 +481,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;
}
}
@@ -463,6 +507,14 @@ class RepoStatus {
}
this.d_rebase = rebase;
}
+
+ if ("sequencerState" in args) {
+ const sequencerState = args.sequencerState;
+ if (null !== sequencerState) {
+ assert.instanceOf(sequencerState, SequencerState);
+ }
+ this.d_sequencerState = sequencerState;
+ }
Object.freeze(this);
}
@@ -566,6 +618,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
/**
@@ -607,12 +680,18 @@ 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 {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`.
@@ -637,11 +716,14 @@ 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,
+ sequencerState: ("sequencerState" in args) ?
+ args.sequencerState : this.d_sequencerState,
});
}
}
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 120b43235..a559be6b2 100644
--- a/node/lib/util/reset.js
+++ b/node/lib/util/reset.js
@@ -32,18 +32,26 @@
const assert = require("chai").assert;
const co = require("co");
+const fs = require("fs-promise");
+const mkdirp = require("mkdirp");
const NodeGit = require("nodegit");
+const path = require("path");
-const GitUtil = require("./git_util");
-const StatusUtil = require("./status_util");
-const SubmoduleFetcher = require("./submodule_fetcher");
-const SubmoduleUtil = require("./submodule_util");
-const UserError = require("./user_error");
+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",
+ SOFT: "SOFT",
+ MIXED: "MIXED",
+ HARD: "HARD",
+ MERGE: "MERGE",
};
Object.freeze(TYPE);
exports.TYPE = TYPE;
@@ -54,14 +62,96 @@ 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;
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}`);
}
+/**
+ * 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 {NodeGit.Repository} repo
+ * @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,
+ mixed) {
+ 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);
+
+ if (mixed) {
+ return;
+ }
+
+ // 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;
+ }
+ }
+ }
+
+ // 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.
+ }
+ }
+ });
+ yield DoWorkQueue.doInParallel(Object.keys(changes), tidySub);
+ }
+});
+
/**
* 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
@@ -78,77 +168,115 @@ 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 changedSubs = yield SubmoduleUtil.getSubmoduleChanges(repo,
+ commit,
+ head,
+ false);
+
+ // 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 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,
+ TYPE.MIXED === type);
+ }
const resetType = getType(type);
- // First, reset the meta-repo.
+ const removedSubmodules = [];
- yield SubmoduleUtil.cacheSubmodules(repo, () => {
- return NodeGit.Reset.reset(repo, commit, resetType);
- });
+ const resetSubmodule = co.wrap(function *(name) {
+ const change = changedSubs[name];
- // Then, all open subs.
+ // Nothing to do if the change was an addition or deletion.
- const openNames = yield SubmoduleUtil.listOpenSubmodules(repo);
- const index = yield repo.index();
- const shas = yield SubmoduleUtil.getCurrentSubmoduleShas(index, openNames);
- const fetcher = new SubmoduleFetcher(repo, commit);
+ if (undefined !== change &&
+ (null === change.oldSha || null === change.newSha)) {
+ return; // RETURN
+ }
+
+ // 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 || TYPE.MERGE === type) &&
+ !openSubsSet.has(name)) {
+ return; // RETURN
+ }
- yield openNames.map(co.wrap(function *(name, index) {
- const sha = shas[index];
- const subRepo = yield SubmoduleUtil.getRepo(repo, name);
+ // Open the submodule and fetch the sha of the commit to which we're
+ // resetting in case we don't have it.
- // Fetch the sha in case we don't already have it.
+ const subRepo =
+ yield opener.getSubrepo(name,
+ Open.SUB_OPEN_OPTION.FORCE_OPEN);
- yield fetcher.fetchSha(subRepo, name, sha);
+ let subCommitSha;
+
+ if (undefined === change) {
+ // 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;
+ }
+
+ 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.
- const subCommit = yield subRepo.getCommit(sha);
yield NodeGit.Reset.reset(subRepo, subCommit, resetType);
- }));
-});
-/**
- * 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`.
+ // 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.
- const status = yield StatusUtil.getRepoStatus(repo, {
- showMetaChanges: true,
- paths: resolvedPaths,
+ yield index.addByPath(name);
});
- // Reset the meta-repo.
+ // 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);
- const metaStaged = Object.keys(status.staged);
- if (0 !== metaStaged.length) {
- yield NodeGit.Reset.default(repo, commit, metaStaged);
+ // remove added submodules from .gitmodules
+ const modules = yield SubmoduleConfigUtil.getSubmodulesFromIndex(repo,
+ index);
+ for (const file of removedSubmodules) {
+ delete modules[file];
}
- const subs = status.submodules;
- const fetcher = new SubmoduleFetcher(repo, commit);
- const subNames = Object.keys(subs);
- const shas =
- yield SubmoduleUtil.getSubmoduleShasForCommit(repo, subNames, commit);
+ yield SubmoduleConfigUtil.writeUrls(repo, index, modules,
+ type === TYPE.MIXED);
- 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.
+ // Write the index in case we've had to stage submodule changes.
- 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);
- }
- }
- }));
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
});
/**
@@ -179,10 +307,37 @@ 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, () => {
- 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/rm.js b/node/lib/util/rm.js
new file mode 100644
index 000000000..b27860337
--- /dev/null
+++ b/node/lib/util/rm.js
@@ -0,0 +1,498 @@
+/*
+ * 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 SparseCheckoutUtil = require("./sparse_checkout_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);
+ }
+ 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
+ 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);
+
+ // TODO: consider changing resolvePaths to support root-level items
+ // items, which would enable removal of checkAllPathsResolved
+ 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 SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+ }
+
+ // 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()) {
+ 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);
+ }
+ }
+ 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);
+ yield index.write();
+});
diff --git a/node/lib/util/sequencer_state.js b/node/lib/util/sequencer_state.js
new file mode 100644
index 000000000..ed4e845b1
--- /dev/null
+++ b/node/lib/util/sequencer_state.js
@@ -0,0 +1,265 @@
+/*
+ * 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 <= 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
+ * @param {CommitAndRef} properties.originalHead
+ * @param {CommitAndRef} properties.target
+ * @param {[String]} properties.commits
+ * @param {Number} properties.currentCommit
+ * @param {String|null} [properties.message]
+ */
+ 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_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;
+ 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;
+ }
+
+ /**
+ * @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`
+ * 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 &&
+ 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 () {
+ return `\
+SequencerState(type=${this.d_type}, originalHead=${this.d_originalHead}, \
+target=${this.d_target}, commits=${JSON.stringify(this.d_commits)}, \
+currentCommit=${this.d_currentCommit}, msg=${this.d_message})`;
+};
+
+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..0f8c77c59
--- /dev/null
+++ b/node/lib/util/sequencer_state_util.js
@@ -0,0 +1,303 @@
+/*
+ * 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.
+ * /MESSAGE -- commit message file, if any
+ */
+
+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";
+const MESSAGE_FILE = "MESSAGE";
+
+/**
+ * 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
+ }
+ const message = yield exports.readFile(gitDir, MESSAGE_FILE);
+ return new SequencerState({
+ type: type,
+ originalHead: original,
+ target: target,
+ commits: commits,
+ currentCommit: currentCommit,
+ message: message,
+ });
+});
+
+/**
+ * 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);
+ if (null !== state.message) {
+ yield fs.writeFile(path.join(root, MESSAGE_FILE), state.message);
+ }
+});
+
+/**
+ * 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,
+ message: sequencer.message,
+ });
+};
diff --git a/node/lib/util/shorthand_parser_util.js b/node/lib/util/shorthand_parser_util.js
index a5e2f2dcd..c5799d3fb 100644
--- a/node/lib/util/shorthand_parser_util.js
+++ b/node/lib/util/shorthand_parser_util.js
@@ -31,9 +31,13 @@
"use strict";
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;
+
/**
* @module {ShorthandParserUtil}
*
@@ -53,11 +57,11 @@ const RepoASTUtil = require("../util/repo_ast_util");
*
* The shorthand syntax for describing a repository:
*
- * shorthand = [':'(';\s*')*]
+ * shorthand = ['%'] [':'(';\s*')*]
* base repo type = 'S' | 'B' | ('C') | 'A' | 'N'
* override = | | | |
* | | | |
- * |
+ * | |
* head = 'H='| nothing means detached
* nothing =
* commit = +
@@ -67,14 +71,21 @@ const RepoASTUtil = require("../util/repo_ast_util");
* current branch = '*='
* new commit = 'C'[message#]['-'(,)*]
* [' '(',\s*'*)]
- * change = path ['=' | "~" | ]
+ * change = path ['=' | "~" | ] |
+ * *path='*''*'
+ * conflict item = |
* path = (|'/')+
* submodule = Surl:[]
+ * file = (['^'] | ['+'])
* data = ('0-9'|'a-z'|'A-Z'|' ')* basically non-delimiter ascii
* remote = R=[]
* [' '=[](',\s*'=[])*]
* note = N [ =message
* rebase = E,,
+ * sequencer = Q[message'#']' '' '
+ * ' '' '(',')*
+ * sequencer type = C | M | R
+ * commit and ref = commit':'[]
* index = I [,]*
* workdir = W [,]*
* open submodule = 'O'[' '('!')*]
@@ -90,7 +101,7 @@ const RepoASTUtil = require("../util/repo_ast_util");
* "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:
@@ -120,10 +131,14 @@ const RepoASTUtil = require("../util/repo_ast_util");
* 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.
*
@@ -140,9 +155,19 @@ 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`.
+ *
+ * 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
@@ -156,13 +181,15 @@ const RepoASTUtil = require("../util/repo_ast_util");
* 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
* 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
@@ -171,6 +198,24 @@ 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: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'.
+ * 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
+ * -- 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".
+ * 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
@@ -242,6 +287,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
@@ -257,22 +318,28 @@ 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
}
-
- // 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;
}
/**
@@ -295,6 +362,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
@@ -320,7 +405,9 @@ function prepareASTArguments(baseAST, rawRepo) {
workdir: baseAST.workdir,
openSubmodules: baseAST.openSubmodules,
rebase: baseAST.rebase,
+ sequencerState: baseAST.sequencerState,
bare: baseAST.bare,
+ sparse: baseAST.sparse,
};
// Process HEAD.
@@ -432,6 +519,14 @@ function prepareASTArguments(baseAST, rawRepo) {
resultArgs.rebase = rawRepo.rebase;
}
+ if ("sequencerState" in rawRepo) {
+ resultArgs.sequencerState = rawRepo.sequencerState;
+ }
+
+ if ("sparse" in rawRepo) {
+ resultArgs.sparse = rawRepo.sparse;
+ }
+
return resultArgs;
}
@@ -509,6 +604,7 @@ function parseOverrides(shorthand, begin, end, delimiter) {
let notes = {};
let openSubmodules = {};
let rebase;
+ let sequencer;
/**
* Parse a set of changes from the specified `begin` character to the
@@ -516,6 +612,7 @@ function parseOverrides(shorthand, begin, end, delimiter) {
*
* @param {Number} begin
* @param {Number} end
+ * @param {Bool} allowConflicts
*/
function parseChanges(begin, end) {
let changes = {};
@@ -525,16 +622,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;
}
@@ -703,7 +813,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,
@@ -861,6 +971,53 @@ function parseOverrides(shorthand, begin, end, delimiter) {
rebase = new RepoAST.Rebase(parts[0], parts[1], parts[2]);
}
+ /**
+ * 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 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}`);
+ 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,
+ message: message,
+ });
+ }
/**
* Parse the override beginning at the specified `begin` and finishing at
* the specified `end`.
@@ -885,6 +1042,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;
@@ -926,6 +1084,9 @@ function parseOverrides(shorthand, begin, end, delimiter) {
if (undefined !== rebase) {
result.rebase = rebase;
}
+ if (undefined !== sequencer) {
+ result.sequencerState = sequencer;
+ }
return result;
}
@@ -952,17 +1113,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;
@@ -980,7 +1155,6 @@ exports.parseRepoShorthandRaw = function (shorthand) {
if (undefined !== typeData) {
result.typeData = typeData;
}
-
return result;
};
@@ -1001,7 +1175,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}`,
@@ -1100,10 +1274,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;
@@ -1181,6 +1356,11 @@ exports.parseMultiRepoShorthand = function (shorthand, existingRepos) {
includeCommit(resultArgs.rebase.originalHead);
includeCommit(resultArgs.rebase.onto);
}
+ 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) {
@@ -1365,7 +1545,7 @@ exports.RepoType = (() => {
commits: {
"1": new RepoAST.Commit({
changes: {
- "README.md": "hello world"
+ "README.md": new File("hello world", false),
},
message: "the first commit",
}),
@@ -1389,7 +1569,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/sparse_checkout_util.js b/node/lib/util/sparse_checkout_util.js
new file mode 100644
index 000000000..cae476417
--- /dev/null
+++ b/node/lib/util/sparse_checkout_util.js
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ *
+ * @param {NodeGit.Repository} repo
+ * @return {Bool}
+ */
+exports.inSparseMode = co.wrap(function *(repo) {
+ assert.instanceOf(repo, NodeGit.Repository);
+
+ 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. 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
+ */
+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");
+});
+
+/**
+ * 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
+ * @param {String|undefined} path if set, where to write the index
+ */
+exports.setSparseBitsAndWriteIndex = co.wrap(function *(repo, index,
+ path = undefined) {
+ 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);
+ }
+ }
+ }
+ // 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/lib/util/stash_util.js b/node/lib/util/stash_util.js
index 26e1fc1ee..ba5e75eb8 100644
--- a/node/lib/util/stash_util.js
+++ b/node/lib/util/stash_util.js
@@ -33,16 +33,32 @@
const assert = require("chai").assert;
const co = require("co");
const colors = require("colors");
+const fs = require("fs-promise");
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 CloseUtil = require("./close_util");
+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");
+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
@@ -69,7 +85,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);
@@ -101,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
@@ -115,21 +187,36 @@ 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
+ * @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
const subRepos = {}; // name to submodule open repo
- const sig = repo.defaultSignature();
+ 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
@@ -142,41 +229,217 @@ exports.save = co.wrap(function *(repo, status, includeUntracked) {
yield Object.keys(submodules).map(co.wrap(function *(name) {
const sub = submodules[name];
const wd = sub.workdir;
- if (null === wd ||
- (wd.status.isClean() &&
- (!includeUntracked ||
- 0 === Object.keys(wd.status.workdir).length))) {
- // Nothing to do for closed or clean subs
- return; // RETURN
+ let stashId;
+
+ 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;
}
- const subRepo = yield SubmoduleUtil.getRepo(repo, name);
- subRepos[name] = subRepo;
- const FLAGS = NodeGit.Stash.FLAGS;
- const flags = includeUntracked ?
+
+ 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.
+
+ 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;
- const stashId = yield NodeGit.Stash.save(subRepo, sig, "stash", flags);
- subResults[name] = stashId.tostrS();
+ 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.
- 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 NodeGit.Commit.create(repo,
- null,
- sig,
- sig,
- null,
- "stash",
- subsTree,
- 1,
- [head]);
+ const stashId = yield Commit.create(repo,
+ null,
+ sig,
+ sig,
+ null,
+ "stash",
+ subsTree,
+ parents.length,
+ parents);
const stashSha = stashId.tostrS();
@@ -194,7 +457,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,
@@ -226,8 +491,8 @@ 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.write();
+ log.append(id, yield ConfigUtil.defaultSignature(repo), message);
+ yield log.write();
}
});
@@ -270,13 +535,15 @@ 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);
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
@@ -284,10 +551,58 @@ 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 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 = {};
yield Object.keys(newSubs).map(co.wrap(function *(name) {
const stashSha = newSubs[name].sha;
if (baseSubs[name].sha === stashSha) {
@@ -295,14 +610,15 @@ exports.apply = co.wrap(function *(repo, id) {
return; // RETURN
}
- const subRepo = yield opener.getSubrepo(name);
-
- // Try to get the comit for the stash; if it's missing, fail.
+ let subRepo =
+ yield opener.getSubrepo(name,
+ Open.SUB_OPEN_OPTION.FORCE_OPEN);
+ // 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)}`);
@@ -310,6 +626,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);
@@ -317,10 +683,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) {
@@ -330,9 +698,48 @@ ${colors.red(name)}`);
result[name] = stashSha;
}
}));
+
+ 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;
});
+/**
+ * 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
@@ -346,29 +753,37 @@ ${colors.red(name)}`);
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();
+ yield log.write();
// We dropped the first element. We need to update `refs/meta-stash`
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 */);
+ yield log.write();
}
else {
NodeGit.Reference.remove(repo, metaStashRef);
}
}
+ const refText = `${metaStashRef}@{${index}}`;
+ console.log(`\
+Dropped ${colors.green(refText)} ${colors.blue(stashSha)}`);
});
/**
@@ -377,29 +792,15 @@ exports.removeStash = co.wrap(function *(repo, index) {
* 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, shouldDrop) {
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 applyResult = yield exports.apply(repo, stashSha);
+ const stashSha = yield getStashSha(repo, index);
+ const applyResult = yield exports.apply(repo, stashSha, reinstateIndex);
const status = yield StatusUtil.getRepoStatus(repo);
process.stdout.write(PrintStatusUtil.printRepoStatus(status, ""));
@@ -407,17 +808,17 @@ exports.pop = co.wrap(function *(repo, index) {
// If the application succeeded, remove it.
if (null !== applyResult) {
- yield exports.removeStash(repo, index);
- console.log(`\
-Dropped ${colors.green(metaStashRef + "@{0}")} ${colors.blue(stashSha)}`);
+ 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(`\
@@ -441,3 +842,187 @@ 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. 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} incrementTimestamp
+ * @param {Bool} includeUntracked
+ * @return {String}
+ */
+const makeShadowCommitForRepo = co.wrap(function *(repo,
+ status,
+ message,
+ incrementTimestamp,
+ 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 = yield ConfigUtil.defaultSignature(repo);
+ const subCommit = yield repo.createCommit(null, sig, sig,
+ message, tree, []);
+ return subCommit.tostrS();
+ }
+
+ const changes = yield TreeUtil.listWorkdirChanges(repo,
+ status,
+ includeUntracked);
+ const head = yield repo.getHeadCommit();
+ const parents = [];
+
+ 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 = yield ConfigUtil.defaultSignature(repo);
+ if (incrementTimestamp && null !== head) {
+ sig = NodeGit.Signature.create(sig.name(),
+ sig.email(),
+ head.time() + 1,
+ head.timeOffset());
+ }
+ const id = yield 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.
+ * 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.
+ *
+ * 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} useEpochTimestamp
+ * @param {Bool} includeMeta
+ * @param {Bool} includeUntracked
+ * @param {Object} includedSubrepos
+ * @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
+ */
+exports.makeShadowCommit = co.wrap(function *(repo,
+ message,
+ 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;
+ } else {
+ assert.isBoolean(indexOnly);
+ }
+
+ if (!message.endsWith("\n")) {
+ message += "\n";
+ }
+
+ const status = yield StatusUtil.getRepoStatus(repo, {
+ showMetaChanges: includeMeta,
+ untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL,
+ ignoreIndex: false,
+ paths: includedSubrepos,
+ });
+ 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 || ((subStatus.commit === null ||
+ wd.status.headCommit === subStatus.commit.sha) &&
+ wd.status.isClean(includeUntracked))) {
+ return; // RETURN
+ }
+ const subRepo = yield SubmoduleUtil.getRepo(repo, name);
+ const subSha = yield makeShadowCommitForRepo(subRepo,
+ wd.status,
+ message,
+ useEpochTimestamp,
+ includeUntracked,
+ indexOnly);
+ 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,
+ useEpochTimestamp,
+ includeUntracked,
+ false);
+ return {
+ metaCommit: metaCommit,
+ subCommits: subCommits,
+ };
+});
diff --git a/node/lib/util/status_util.js b/node/lib/util/status_util.js
index 124a222e2..9d9075666 100644
--- a/node/lib/util/status_util.js
+++ b/node/lib/util/status_util.js
@@ -42,11 +42,14 @@ const NodeGit = require("nodegit");
const DiffUtil = require("./diff_util");
const GitUtil = require("./git_util");
+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");
/**
* Return a new `RepoStatus.Submodule` object having the same value as the
@@ -97,6 +100,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);
@@ -148,6 +152,10 @@ exports.remapRepoStatus = function (status, commitMap, urlMap) {
workdir: status.workdir,
rebase: status.rebase === null ? null : remapRebase(status.rebase,
commitMap),
+ sequencerState: status.sequencerState === null ?
+ null :
+ SequencerStateUtil.mapCommits(status.sequencerState,
+ commitMap),
});
};
@@ -205,7 +213,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
@@ -222,6 +230,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;
@@ -234,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
@@ -282,23 +295,202 @@ exports.getSubmoduleStatus = co.wrap(function *(repo,
});
});
+/**
+ * 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, paths) {
+ assert.instanceOf(index, NodeGit.Index);
+ assert.isArray(paths);
+ paths = new Set(paths);
+ 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];
+ 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) {
+ result[name] = new RepoStatus.Conflict(c.ancestor, c.our, c.their);
+ }
+ }
+ 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] : [],
+ untrackedFilesOption: options.untrackedFilesOption,
+ 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),
- * 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 submobules. If the optionally specified `ignoreIndex` is specified,
- * calculate the status matching the workdir to the underlying commit rather
- * than against the index.
+ * 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
+ * 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]
- * @param {Boolean} [options.showAllUntracked]
+ * @param {String} [options.untrackedFilesOption]
* @param {String []} [options.paths]
+ * @param {String} [options.cwd]
* @param {Boolean} [options.showMetaChanges]
* @param {Boolean} [options.ignoreIndex]
* @return {RepoStatus}
@@ -314,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 = [];
@@ -327,7 +519,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 +530,14 @@ exports.getRepoStatus = co.wrap(function *(repo, options) {
else {
assert.isBoolean(options.ignoreIndex);
}
-
+ if (undefined !== options.cwd) {
+ assert.isString(options.cwd);
+ options.paths = options.paths.map(filename => {
+ return GitUtil.resolveRelativePath(repo.workdir(),
+ options.cwd,
+ filename);
+ });
+ }
const headCommit = yield repo.getHeadCommit();
let args = {
@@ -362,106 +561,98 @@ exports.getRepoStatus = co.wrap(function *(repo, options) {
args.rebase = rebase;
}
- if (options.showMetaChanges && !repo.isBare()) {
+ args.sequencerState =
+ yield SequencerStateUtil.readSequencerState(repo.path());
+
+ if (!repo.isBare()) {
const head = yield repo.getHeadCommit();
let tree = null;
if (null !== head) {
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);
- });
- args.staged = status.staged;
- args.workdir = status.workdir;
- }
-
- // 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.
-
- 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();
- 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
- // 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;
- if (filtering) {
- filterPaths = yield SubmoduleUtil.resolvePaths(repo.workdir(),
- options.paths,
- indexNames,
- 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)));
+ let paths = options.paths;
+ if (!options.showMetaChanges && (!paths || paths.length === 0)) {
+ paths = [SubmoduleConfigUtil.modulesFileName];
}
- 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 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;
+ args.workdir = status.workdir;
+ } else {
+ const gitmodules = SubmoduleConfigUtil.modulesFileName;
+ if (gitmodules in status.staged) {
+ args.staged[gitmodules] = status.staged[gitmodules];
}
- let indexSha = null;
- const entry = index.getByPath(name);
- if (entry) {
- indexSha = entry.id.tostrS();
+ if (gitmodules in status.workdir) {
+ args.workdir[gitmodules] = status.workdir[gitmodules];
}
- let subRepo = null;
- 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,
- });
- }
- return yield exports.getSubmoduleStatus(subRepo,
- status,
- indexUrls[name] || null,
- headUrls[name] || null,
- indexSha,
- headSha);
- }));
-
- const subStats = yield subStatMakers;
-
- // And copy them into the arguments.
+ }
+ }
- subsToList.forEach((name, i) => {
- args.submodules[name] = subStats[i];
- });
+ // Now do the submodules.
+ if (null !== headCommit) {
+ const {conflicts, submodules} =
+ yield getSubRepoStatus(repo, options, headCommit);
+ Object.assign(args.staged, conflicts);
+ args.submodules = submodules;
}
return new RepoStatus(args);
});
+
+/**
+ * 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.checkReadiness = function (status) {
+ assert.instanceOf(status, RepoStatus);
+
+ 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 rebase --continue') or abort it (by running
+'git rebase --abort').`);
+ }
+ if (status.isConflicted()) {
+ return (`\
+Please resolve outstanding conflicts before proceeding:
+${PrintStatusUtil.printRepoStatus(status, "")}`);
+ }
+ if (null !== status.sequencerState) {
+ const command =
+ PrintStatusUtil.getSequencerCommand(status.sequencerState.type);
+ 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/stitch_util.js b/node/lib/util/stitch_util.js
new file mode 100644
index 000000000..58d913242
--- /dev/null
+++ b/node/lib/util/stitch_util.js
@@ -0,0 +1,1102 @@
+/*
+ * 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 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");
+const SubmoduleUtil = require("./submodule_util");
+const TreeUtil = require("./tree_util");
+const UserError = require("./user_error");
+
+const FILEMODE = NodeGit.TreeEntry.FILEMODE;
+
+/**
+ * @property {String}
+ */
+exports.allowedToFailNoteRef = "refs/notes/stitched/allowed_to_fail";
+
+/**
+ * Return a set of meta-repo commits that are allowed to fail.
+ *
+ * @param {NodeGit.Repository} repo
+ * @return {Set}
+ */
+exports.readAllowedToFailList = co.wrap(function *(repo) {
+ assert.instanceOf(repo, NodeGit.Repository);
+
+ const notes =
+ yield BulkNotesUtil.readNotes(repo,
+ exports.allowedToFailNoteRef);
+ return new Set(Object.keys(notes));
+});
+
+/**
+ * The name of the note used to record conversion information.
+ *
+ * @property {String}
+ */
+exports.convertedNoteRef = "refs/notes/stitched/converted";
+
+/**
+ * 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 {String|null} stitchedSha
+ */
+exports.makeConvertedNoteContent = function (stitchedSha) {
+ if (null !== stitchedSha) {
+ assert.isString(stitchedSha);
+ }
+ return null === stitchedSha ? "" : stitchedSha;
+};
+
+/**
+ * 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();
+
+ // 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
+ // 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.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;
+ result += whenText;
+ result += messageText;
+ }
+ });
+ return result;
+};
+
+/**
+ * The name of the note used to record conversion information.
+ *
+ * @property {String}
+ */
+exports.referenceNoteRef = "refs/notes/stitched/reference";
+
+/**
+ * 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} metaRepoSha
+ * @param {Object} subCommits name to NodeGit.Commit
+ */
+exports.makeReferenceNoteContent = function (metaRepoSha, subCommits) {
+ assert.isString(metaRepoSha);
+ assert.isObject(subCommits);
+ const object = {
+ metaRepoCommit: metaRepoSha,
+ submoduleCommits: {},
+ };
+ Object.keys(subCommits).forEach(name => {
+ object.submoduleCommits[name] = subCommits[name].id().tostrS();
+ });
+ return JSON.stringify(object, null, 4);
+};
+
+/**
+ * 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 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);
+});
+
+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 BulkNotesUtil.writeNotes(repo, exports.changeCacheRef, cache);
+});
+
+/**
+ * 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 BulkNotesUtil.readNotes(repo, exports.changeCacheRef);
+ return BulkNotesUtil.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 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 {Object} commitChanges sha->name->SubmoduleChange
+ * @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,
+ commitChanges,
+ keepAsSubmodule,
+ adjustPath) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.isArray(toFetch);
+ assert.isObject(commitChanges);
+ assert.isFunction(keepAsSubmodule);
+ assert.isFunction(adjustPath);
+
+ 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 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;
+});
+
+/**
+ * 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
+ * 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.
+ *
+ * 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 the first parent (and an
+ * empty `subCommits` map), or null if there are no parents.
+ *
+ * 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
+ * @param {Object} subChanges path to SubmoduleChange
+ * @param {[NodeGit.Commit]} parents
+ * @param {(String) => Boolean} keepAsSubmodule
+ * @param {(String) => String|null} adjustPath
+ * @param {Bool} skipEmpty
+ * @param {Set of String} allowed_to_fail
+ * @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,
+ allowed_to_fail) {
+ 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);
+ assert.instanceOf(allowed_to_fail, Set);
+
+ let updateModules = false; // if any kept subs added or removed
+ 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) {
+ const metaSha = commit.id().tostrS();
+ 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.allowedToFailNoteRef} add -m skip ${metaSha}`);
+ }
+ const subTreeId = subCommit.treeId();
+ changes[name] = new TreeUtil.Change(subTreeId, FILEMODE.TREE);
+
+ // 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) {
+ const id = NodeGit.Oid.fromString(newSha);
+ changes[name] = new TreeUtil.Change(id, FILEMODE.COMMIT);
+ }
+
+ for (let name in subChanges) {
+ const mapped = adjustPath(name);
+ if (null === mapped) {
+ continue; // CONTINUE
+ }
+ const newSha = subChanges[name].newSha;
+ changes[mapped] = null;
+
+ if (keepAsSubmodule(name)) {
+ updateModules = true;
+ if (null !== newSha) {
+ changeKept(name, newSha);
+ }
+ } else if (null !== newSha) {
+ yield stitchSub(mapped, 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;
+ }
+
+ let newCommit = null;
+
+ if (!skipEmpty || 0 !== Object.keys(changes).length) {
+ // If we've got changes or are not skipping commits, we make one.
+
+ let parentTree = null;
+ if (0 !== parents.length) {
+ const parentCommit = parents[0];
+ parentTree = yield parentCommit.getTree();
+ }
+
+ const newTree = yield TreeUtil.writeTree(repo, parentTree, changes);
+
+ 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);
+ } 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 = {};
+ }
+ return {
+ stitchedCommit: newCommit,
+ subCommits: subCommits,
+ };
+});
+
+/**
+ * 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, name, 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, name + "/");
+ }
+ 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);
+ }
+ }
+});
+
+/**
+ * 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;
+ };
+};
+
+/**
+ * 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 BulkNotesUtil.readNotes(repo, exports.convertedNoteRef);
+ 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;
+});
+
+/**
+ * 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);
+
+ 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;
+ });
+};
+
+/**
+ * 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
+ * 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.");
+
+ 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, getConverted);
+
+ console.log("listing submodule changes");
+
+ const changes = yield exports.listSubmoduleChanges(repo, commitsToStitch);
+
+ const adjustPath = exports.makeAdjustPathFunction(joinRoot);
+
+ console.log(commitsToStitch.length, "to stitch");
+
+ if (fetch) {
+ console.log("listing fetches");
+ const fetches = yield exports.listFetches(repo,
+ commitsToStitch,
+ changes,
+ options.keepAsSubmodule,
+ 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(subsRepo, name, url, subFetches);
+ console.timeEnd(fetchTimeMessage);
+ });
+ yield DoWorkQueue.doInParallel(subNames,
+ doFetch,
+ {limit: options.numParallel});
+ }
+
+ 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 BulkNotesUtil.writeNotes(repo,
+ exports.referenceNoteRef,
+ referenceNotes);
+ yield BulkNotesUtil.writeNotes(repo,
+ exports.convertedNoteRef,
+ convertedNotes);
+ records = {};
+ });
+
+ const allowed_to_fail = yield exports.readAllowedToFailList(repo);
+
+ 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 = yield getConverted(parent.id().tostrS());
+ if (null !== newParentSha && undefined !== newParentSha) {
+ const newParent = yield repo.getCommit(newParentSha);
+ newParents.push(newParent);
+ }
+ }
+
+ const result = yield exports.writeStitchedCommit(
+ repo,
+ next,
+ changes[nextSha],
+ newParents,
+ options.keepAsSubmodule,
+ adjustPath,
+ skipEmpty,
+ allowed_to_fail);
+ 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);
+
+ 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`
+ // unchanged.
+
+ 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()}.`);
+ yield NodeGit.Branch.create(repo, targetBranchName, lastCommit, 1);
+ }
+});
+
diff --git a/node/lib/util/submodule_change.js b/node/lib/util/submodule_change.js
new file mode 100644
index 000000000..5790d577e
--- /dev/null
+++ b/node/lib/util/submodule_change.js
@@ -0,0 +1,114 @@
+/*
+ * 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 describes the changes from `oldSha` to `newSha` as well as
+ * the destination that the changes will be appliedd: `ourSha`
+ */
+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. 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 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);
+ if (null !== oldSha) {
+ assert.isString(oldSha);
+ }
+ 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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.
+ */
+ get deleted() {
+ return this.d_newSha === null;
+ }
+}
+
+module.exports = SubmoduleChange;
diff --git a/node/lib/util/submodule_config_util.js b/node/lib/util/submodule_config_util.js
index e3b8c85af..a298efe70 100644
--- a/node/lib/util/submodule_config_util.js
+++ b/node/lib/util/submodule_config_util.js
@@ -50,9 +50,19 @@ 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 DoWorkQueue = require("./do_work_queue");
+const GitUtil = require("./git_util");
+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 +80,139 @@ 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 repositories having the specified `submoduleNames` in the
+ * specified `repo`.
+ *
+ * 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
+ * @param {String[]} submoduleNames
+ */
+exports.deinit = co.wrap(function *(repo, submoduleNames) {
+ assert.instanceOf(repo, NodeGit.Repository);
+ assert.isArray(submoduleNames);
+
+ const sparse = yield SparseCheckoutUtil.inSparseMode(repo);
+
+ const deinitOne = co.wrap(function *(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
+
+ // 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);
+
+ 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) {
+ // 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;
+ }
+ }
+ } 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);
+ }
+});
+
/**
* Return the relative path from the working directory of a submodule having
* the specified `subName` to its .git directory.
@@ -116,7 +259,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.
@@ -234,6 +386,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`.
*
@@ -248,15 +420,34 @@ 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);
});
+/**
+ * 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`.
*
@@ -287,16 +478,11 @@ 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.
*
* @async
- * @param {NodeGit.Repository} repo
- * @param {String} name
- * @param {String} url
- *
- * @async
* @param {String} repoPath
* @param {String} name
* @param {String} url
@@ -305,7 +491,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.
@@ -354,27 +541,32 @@ 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) {
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();
- yield exports.initSubmodule(repoPath, name, url);
+ 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
// flags so that it will set it up as a git link.
@@ -383,23 +575,36 @@ 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 {
- yield NodeGit.Repository.open(subRepoDir);
+ 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.
-
- 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
- });
+ subRepo = yield initRepo();
}
+ 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
@@ -433,81 +638,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.
- *
- * @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.
@@ -524,7 +654,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}
@@ -532,3 +662,78 @@ 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
+ * @param {Boolean} cached do not write to the working tree
+ */
+exports.writeUrls = co.wrap(function *(repo, index, urls, cached) {
+ if (undefined === cached) {
+ cached = false;
+ }
+ else {
+ assert.isBoolean(cached);
+ }
+
+ 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) {
+ 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 {
+ 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);
+ }
+ }
+});
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/lib/util/submodule_rebase_util.js b/node/lib/util/submodule_rebase_util.js
new file mode 100644
index 000000000..efa7dca6a
--- /dev/null
+++ b/node/lib/util/submodule_rebase_util.js
@@ -0,0 +1,385 @@
+/*
+ * 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 ConfigUtil = require("./config_util");
+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");
+
+/**
+ * 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);
+ }));
+ }
+});
+
+/**
+ * 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 = yield ConfigUtil.defaultSignature(repo);
+ 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:
+ * 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
+ * @returns {Boolean} return.ffwd
+ */
+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,
+ ffwd: false,
+ };
+ const signature = yield ConfigUtil.defaultSignature(repo);
+ while (null !== op) {
+ const index = yield repo.index();
+ if (index.hasConflicts()) {
+ result.conflictedCommit = op.id().tostrS();
+ return result; // RETURN
+ }
+ 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);
+ 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
+ * @return {Boolean} return.ffwd true if fast-forwarded
+ */
+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 headSha = head.target().tostrS();
+ const branchSha = branch.id().tostrS();
+ const upstreamSha = (upstream && upstream.id().tostrS()) || null;
+
+ const result = {
+ commits: {},
+ conflictedCommit: null,
+ ffwd: false,
+ };
+
+ // 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
+ }
+
+ // 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)
+
+ if (null === upstream) {
+ if (yield NodeGit.Graph.descendantOf(repo, branchSha, headSha)) {
+ yield GitUtil.setHeadHard(repo, branch);
+ result.ffwd = true;
+ return result; // RETURN
+ }
+ }
+
+ 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 `Submodule ${colors.red(name)} is conflicted.\n`;
+};
+
+/**
+ * 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`. 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.
+ *
+ * @param {NodeGit.Repository} repo
+ * @param {NodeGit.Index} index
+ * @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
+ */
+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);
+
+ exports.logCommit(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);
+ }
+ else {
+ yield index.addByPath(name);
+ yield index.conflictRemove(name);
+ }
+ });
+ yield DoWorkQueue.doInParallel(Object.keys(subs), continueSub);
+ yield SparseCheckoutUtil.setSparseBitsAndWriteIndex(repo, index);
+ 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/lib/util/submodule_util.js b/node/lib/util/submodule_util.js
index b1549bd6c..8467e628d 100644
--- a/node/lib/util/submodule_util.js
+++ b/node/lib/util/submodule_util.js
@@ -41,10 +41,14 @@ 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");
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
@@ -128,7 +132,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();
@@ -136,8 +140,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];
@@ -151,18 +155,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);
@@ -191,12 +196,60 @@ 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;
};
+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 modules_dir = path.join(repo.path(), "modules");
+ const out = [];
+
+ 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
+ // 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 dirs) {
+ if (!gitReservedNames.has(name)) {
+ filtered.push(name);
+ }
+ }
+ dirs.splice(0, dirs.length, ...filtered);
+ out.push(root.substring(modules_dir.length + 1));
+ }
+ });
+
+
+ return out;
+
+});
+
+
/**
* Return true if the submodule having the specified `submoduleName` in the
* specified `repo` is visible and false otherwise.
@@ -235,6 +288,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`.
@@ -258,14 +330,20 @@ 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
+ // 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);
}
});
@@ -289,7 +367,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;
@@ -299,49 +377,30 @@ 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);
});
/**
- * 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`. Fail
+ * if the specified 'allowMetaChanges' is not true and `diff` contains
+ * non-submodule changes to the meta-repo.
*
* @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
+ * @param {Bool} allowMetaChanges
+ * @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.
+exports.getSubmoduleChangesFromDiff = function (diff, allowMetaChanges) {
+ assert.instanceOf(diff, NodeGit.Diff);
+ assert.isBoolean(allowMetaChanges);
- 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);
const num = diff.numDeltas();
- const result = {
- added: {},
- changed: {},
- removed: {},
- modulesFileChanged: false,
- };
+ 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()) {
@@ -358,55 +417,114 @@ 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(),
+ null);
+ } else if (!allowMetaChanges && path !== modulesFileName) {
+ throw new UserError(`\
+Modification to meta-repo file ${colors.red(path)} is not supported.`);
}
} 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(),
+ null);
+ } else if (!allowMetaChanges && path !== modulesFileName) {
+ throw new UserError(`\
+Addition to meta-repo of file ${colors.red(path)} is not supported.`);
}
} 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,
+ null);
+ } else if (!allowMetaChanges && path !== modulesFileName) {
+ throw new UserError(`\
+Deletion of meta-repo file ${colors.red(path)} is not supported.`);
}
} break;
}
}
return result;
+};
+
+/**
+ * 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'. 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.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 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, baseTree, tree, null);
+ 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 => {
@@ -418,19 +536,23 @@ exports.getSubmodulesForCommit = co.wrap(function *(repo, commit) {
/**
* 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 '/').
+ *
+ * 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 = co.wrap(function *(workdir, dir, indexSubNames) {
- assert.isString(workdir);
+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);
@@ -439,106 +561,181 @@ 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);
- }
- 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));
- });
- }
+ // 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] === "/" ||
+ longPath[shortPath.length] === "/"
+ );
+ };
+ const result = [];
+ for (const subPath of indexSubNames) {
+ if (subPath === dir) {
+ return [dir]; // RETURN
+ } else if (isParentDir(dir, subPath)) {
+ result.push(subPath);
+ } else if (includeParents && isParentDir(subPath, dir)) {
+ result.push(subPath);
}
- });
- yield listForFilename(dir);
+ }
return result;
-});
+};
/**
* 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`.
*
- * @async
* @param {String} workdir
* @param {String} cwd
* @param {String[]} submoduleNames
* @param {String[]} paths
+ * @param {Boolean} throwOnMissing
* @return {String[]}
*/
-exports.resolveSubmoduleNames = co.wrap(function *(workdir,
- cwd,
- submoduleNames,
- paths) {
+exports.resolveSubmoduleNames = function (workdir,
+ cwd,
+ submoduleNames,
+ paths,
+ throwOnMissing) {
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,
+ false);
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;
- }));
+ });
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` 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
+ * @param {Boolean} failOnUnprefixed
* @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,
+ 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.
-
- yield paths.map(co.wrap(function *(path) {
- const subs = yield exports.getSubmodulesInPath(workdir,
- path,
- indexSubNames);
- subs.forEach(subName => result[subName] = []);
- }));
+ // contained, and clean the relevant specs out of paths
+
+ const remainingPaths = [];
+ const add = subName => result[subName] = [];
+ for (const path of paths) {
+ const subs = exports.getSubmodulesInPath(path, indexSubNames);
+ 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
@@ -555,9 +752,14 @@ exports.resolvePaths = co.wrap(function *(workdir,
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 + "/")) {
+ if (filename === subName) {
+ found = true;
+ result[subName] = [];
+ } else if (filename.startsWith(subName + "/")) {
+ found = true;
const pathInSub = filename.slice(subName.length + 1,
filename.length);
const subPaths = result[subName];
@@ -569,10 +771,14 @@ exports.resolvePaths = co.wrap(function *(workdir,
}
}
}
+ if (!found && failOnUnprefixed) {
+ throw new UserError(`\
+pathspec '${filename}' did not match any files`);
+ }
}
return result;
-});
+};
/**
* Create references having the specified `refs` names in each of the specified
@@ -588,7 +794,7 @@ exports.resolvePaths = co.wrap(function *(workdir,
* @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);
@@ -630,32 +836,8 @@ exports.addRefs = co.wrap(function *(repo, refs, submodules) {
name,
NodeGit.Oid.fromString(sha),
1,
- "addRefs");
+ "syncRefs");
}
}));
}));
});
-
-/**
- * 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/lib/util/syncrefs.js b/node/lib/util/syncrefs.js
new file mode 100644
index 000000000..20095abfe
--- /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.ALL);
+ yield SubmoduleUtil.syncRefs(repo, refs, subs);
+});
diff --git a/node/lib/util/synthetic_branch_util.js b/node/lib/util/synthetic_branch_util.js
index 76375bc6d..455da8e4e 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");
@@ -45,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
@@ -53,6 +55,26 @@ function identity(v) {
return v;
}
+function SyntheticBranchConfig(urlSkipPattern, pathSkipPattern) {
+ if (urlSkipPattern.length > 0) {
+ const urlSkipRE = new RegExp(urlSkipPattern);
+ this.urlSkipTest = function(url) {
+ return urlSkipRE.test(url);
+ };
+ } else {
+ this.urlSkipTest = function() { return false; };
+ }
+
+ if (pathSkipPattern.length > 0) {
+ const pathSkipRE = new RegExp(pathSkipPattern);
+ this.pathSkipTest = function(path) {
+ return pathSkipRE.test(path);
+ };
+ } else {
+ this.pathSkipTest = function() { return false; };
+ }
+}
+
/**
* (This has to be public so we can mock it for testing)
* @param {NodeGit.Commit} commit
@@ -61,89 +83,181 @@ 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);
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 ConfigUtil.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.
- }
+ let 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 " +
"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 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
+ * @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.pathSkipTest(path);
+}
+
+/**
+ * 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
+ * @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.urlSkipTest(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
- * @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, path) {
assert.instanceOf(repo, NodeGit.Repository);
+ assert.instanceOf(cfg, SyntheticBranchConfig);
assert.instanceOf(submoduleEntry, NodeGit.TreeEntry);
- const localPath = yield *urlToLocalPath(repo, url);
+ if (skipCheckForURL(cfg, 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();
- 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;
}
}
+/**
+ * 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);
+ const config = yield repo.config();
+ const urlSkipPattern = (
+ yield ConfigUtil.getConfigString(
+ config, "gitmeta.skipsyntheticrefpattern")) || "";
+ const pathSkipPattern = (
+ yield ConfigUtil.getConfigString(
+ config, "gitmeta.skipsyntheticrefpathpattern")) || "";
+
+ const cfg = new SyntheticBranchConfig(urlSkipPattern,
+ pathSkipPattern);
+
+ const parent = yield GitUtil.getParentCommit(repo, commit);
+ const names = yield computeChangedSubmodules(repo,
+ commit,
+ parent);
+
const submodules = yield SubmoduleUtil.getSubmodulesForCommit(repo,
- commit);
+ commit,
+ names);
const getChanges = SubmoduleUtil.getSubmoduleChanges;
- const changes = yield getChanges(repo, commit);
+ const changes = yield getChanges(repo, commit, null, true);
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) {
const entry = yield commit.getEntry(path);
const submodulePath = entry.path();
- const url = submodules[submodulePath].url;
- return yield *checkSubmodule(repo, commit, entry, 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,
+ submodulePath);
});
return (yield result).every(identity);
});
@@ -158,14 +272,19 @@ 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
+ 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);
@@ -180,8 +299,8 @@ function* parentLoop(repo, commit, oldSha, handled) {
return true;
}
- const ok = yield GitUtil.readNote(repo, NOTES_REF, commit.id());
- if (ok !== null && ok.message() === "ok") {
+ 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;
}
@@ -198,33 +317,44 @@ 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);
- });
- 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
* @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);
+ // deleting is OK
+ if (newSha === "0000000000000000000000000000000000000000") {
+ return true;
+ }
+
const newAnnotated = yield GitUtil.resolveCommitish(repo, newSha);
if (newAnnotated === null) {
@@ -233,14 +363,8 @@ function* checkUpdate(repo, oldSha, newSha, handled) {
}
const newCommit = yield repo.getCommit(newAnnotated.id());
- const success = yield parentLoop(repo, newCommit, oldSha,
- handled);
- if (success) {
- yield NodeGit.Note.create(repo, NOTES_REF, newCommit.committer(),
- newCommit.committer(), newAnnotated.id(),
- "ok", 1);
- }
- return success;
+ return yield parentLoop(repo, notesRepo, newCommit, oldSha,
+ handled);
}
/**
@@ -249,22 +373,26 @@ 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 " +
- update.ref);
+ "Update to ref '" +
+ update.ref +
+ "' failed synthetic branch check. " +
+ "Did you forget to use `git meta push`?");
}
return !ok;
});
@@ -279,11 +407,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)) {
@@ -313,6 +442,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.
@@ -337,8 +471,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/lib/util/test_util.js b/node/lib/util/test_util.js
index 6f40651c7..ca551371e 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,8 +132,7 @@ exports.createSimpleRepositoryOnBranch = co.wrap(function *(branchName) {
const repo = yield exports.createSimpleRepository();
const commit = yield repo.getHeadCommit();
- const sig = repo.defaultSignature();
- const publicBranch = yield repo.createBranch(branchName, commit, 0, sig);
+ const publicBranch = yield repo.createBranch(branchName, commit, 0);
yield repo.setHead(publicBranch.name());
return repo;
@@ -211,7 +212,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,
@@ -255,13 +256,13 @@ 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 sig = bare.defaultSignature();
+ const refs = yield repo.getReferences();
+ const sig = yield ConfigUtil.defaultSignature(bare);
for (let i = 0; i < refs.length; ++i) {
const ref = refs[i];
const shorthand = ref.shorthand();
diff --git a/node/lib/util/text_util.js b/node/lib/util/text_util.js
new file mode 100644
index 000000000..2828a4ab5
--- /dev/null
+++ b/node/lib/util/text_util.js
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+/**
+ * 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
+ * @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;
+};
+
+/**
+ * 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/lib/util/tree_util.js b/node/lib/util/tree_util.js
index 03b95fcab..f721160d5 100644
--- a/node/lib/util/tree_util.js
+++ b/node/lib/util/tree_util.js
@@ -32,10 +32,12 @@
const assert = require("chai").assert;
const co = require("co");
+const fs = require("fs-promise");
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`,
@@ -51,8 +53,8 @@ const RepoStatus = require("./repo_status");
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,
@@ -60,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 data = flatTree[path];
- tree[leafPath] = data;
+ const leafData = tree[leafPath];
+ const data = flatTree[subpath];
+
+ // 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;
@@ -143,13 +158,27 @@ 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.
- 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.
@@ -157,12 +186,18 @@ 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 =
+ builder.insert(filename, entry.id, entry.mode);
+ treeEntries.push(inserted);
}
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);
}
@@ -170,29 +205,31 @@ 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);
- 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);
}
else {
- yield builder.insert(filename,
- subtree.id(),
- NodeGit.TreeEntry.FILEMODE.TREE);
+ const inserted = builder.insert(
+ filename,
+ subtree.id(),
+ NodeGit.TreeEntry.FILEMODE.TREE);
+ treeEntries.push(inserted);
}
}
}
- const id = builder.write();
+ const id = yield builder.write();
return yield repo.getTree(id);
});
- return yield writeSubtree(baseTree, directory);
+ return yield writeSubtree(baseTree, directory, "");
});
/**
@@ -202,20 +239,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
@@ -227,11 +257,12 @@ 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);
+ let touchedModules = false;
const FILESTATUS = RepoStatus.FILESTATUS;
const FILEMODE = NodeGit.TreeEntry.FILEMODE;
@@ -240,20 +271,28 @@ 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);
+ const sha = yield exports.hashFile(repo, subpath);
+ result[subpath] = new Change(sha, filemode);
}
break;
case FILESTATUS.MODIFIED:
- result[path] = new Change(exports.hashFile(repo, path),
- FILEMODE.BLOB);
+ const sha = yield exports.hashFile(repo, subpath);
+ result[subpath] = new Change(sha,filemode);
break;
case FILESTATUS.REMOVED:
- result[path] = null;
+ result[subpath] = null;
break;
}
}
@@ -262,16 +301,39 @@ 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 = yield exports.hashFile(repo, modulesName);
+ result[modulesName] = new Change(id, FILEMODE.BLOB);
}
return result;
-};
+});
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/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/lib/util/write_repo_ast_util.js b/node/lib/util/write_repo_ast_util.js
index 8f64ce0ed..1db6274a6 100644
--- a/node/lib/util/write_repo_ast_util.js
+++ b/node/lib/util/write_repo_ast_util.js
@@ -45,31 +45,22 @@ 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");
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");
- // Begin module-local methods
+const FILEMODE = NodeGit.TreeEntry.FILEMODE;
-/**
- * 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.tostrS();
-});
+ // Begin module-local methods
/**
* Configure the specified `repo` to have settings needed by git-meta tests.
@@ -86,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]
@@ -102,7 +91,6 @@ const configRepo = co.wrap(function *(repo) {
* @return {NodeGit.Tree} return.tree
*/
const writeTree = co.wrap(function *(repo,
- db,
shaMap,
changes,
parent) {
@@ -116,7 +104,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`
@@ -124,6 +111,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) {
@@ -131,11 +123,12 @@ const writeTree = co.wrap(function *(repo,
pathToChange[filename] = null;
}
- else if ("string" === typeof entry) {
- // A string is just plain data.
-
- const id = yield hashObject(db, entry);
- 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
@@ -172,7 +165,7 @@ const writeTree = co.wrap(function *(repo,
\turl = ${url}
`;
}
- const dataId = yield hashObject(db, data);
+ const dataId = (yield GitUtil.hashObject(repo, data)).tostrS();
pathToChange[SubmoduleConfigUtil.modulesFileName] =
new TreeUtil.Change(dataId, FILEMODE.BLOB);
}
@@ -214,10 +207,9 @@ 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();
+ const sig = yield ConfigUtil.defaultSignature(repo);
const commitObjs = {}; // map from new id to `Commit` object
@@ -243,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);
@@ -324,6 +315,24 @@ 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 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) {
const newSha = commitMap[commit];
const newId = NodeGit.Oid.fromString(newSha);
@@ -373,25 +382,35 @@ 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);
}
}
- else if (null !== ast.currentBranchName) {
- const currentBranch =
- yield repo.getBranch("refs/heads/" + ast.currentBranchName);
- yield repo.checkoutBranch(currentBranch);
- }
- else if (null !== ast.head) {
- const headCommit = yield repo.getCommit(newHeadSha);
- repo.setHeadDetached(newHeadSha);
- yield NodeGit.Reset.reset(repo, headCommit, NodeGit.Reset.TYPE.HARD);
+ else if (null !== ast.currentBranchName || null !== ast.head) {
+ // If we use NodeGit to checkout, it will not respect the
+ // sparse-checkout settings.
+
+ if (null === ast.currentBranchName) {
+ repo.detachHead();
+ }
+
+ const toCheckout = ast.currentBranchName || newHeadSha;
+ const checkoutStr = `\
+git -C '${repo.workdir()}' checkout ${toCheckout}
+`;
+ 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.
+ }
}
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) {
@@ -437,6 +456,13 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) {
indexHead = rebase.onto;
}
+ // 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.
@@ -444,24 +470,48 @@ const configureRepo = co.wrap(function *(repo, ast, commitMap, treeCache) {
if (null !== indexHead) {
indexParent = treeCache[indexHead];
}
- const db = yield repo.odb();
const trees = yield writeTree(repo,
- db,
commitMap,
ast.index,
indexParent);
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
// not. I didn't see anything about `clean` and `Checkout.index`
// didn't seem to work..
+ 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 -a -f
+${checkoutStr}
`;
yield exec(checkoutIndexStr);
@@ -477,7 +527,10 @@ git -C '${repo.workdir()}' checkout-index -a -f
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");
+ }
}
}
}
@@ -577,6 +630,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 = {};
@@ -593,6 +650,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
@@ -635,26 +707,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:
+ // Re-list commits now that URLs are updated.
- let commits = {};
- for (let repoName in repos) {
- const repo = repos[repoName];
- Object.assign(commits, repo.commits);
-
- // Also, commits from open submodules.
-
- 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);
@@ -691,7 +765,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]);
}
@@ -716,6 +790,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;
@@ -736,7 +813,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];
@@ -746,7 +823,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/package.json b/node/package.json
index 08759cbf6..15d4e2126 100644
--- a/node/package.json
+++ b/node/package.json
@@ -27,24 +27,29 @@
"preferGlobal": true,
"homepage": "https://github.com/twosigma/git-meta#readme",
"dependencies": {
- "argparse": "",
+ "argparse": "^1.0.0",
+ "async-mutex": "^0.2.4",
+ "binary-search": "",
"chai": "",
"child-process-promise": "",
"co": "",
"colors": "",
+ "deeper": "",
"fs-promise": "",
- "nodegit": "^0.19.0",
+ "git-range": "",
+ "group-by": "",
+ "nodegit": "^0.25.0",
+ "readdir-withfiletypes": "^1.0.2",
"rimraf": "",
"split": ""
},
"devDependencies": {
"deepcopy": "",
- "deeper": "",
"istanbul": "",
- "mkdirp": "",
- "mocha": "^3.1.2",
+ "mkdirp": "0.5.1",
+ "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/add.js b/node/test/util/add.js
index 89b2a0ff1..f8b1e6580 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: [""],
@@ -129,17 +134,80 @@ 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 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"],
+ 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: [""],
+ },
+ "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",
+ },
+ "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];
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,
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/bulk_notes_util.js b/node/test/util/bulk_notes_util.js
new file mode 100644
index 000000000..d3fa6e4f2
--- /dev/null
+++ b/node/test/util/bulk_notes_util.js
@@ -0,0 +1,149 @@
+/*
+ * 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 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("aabbffffffffffffffff"),
+ path.join("aa", "bb", "ffffffffffffffff"));
+ });
+});
+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 = 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);
+ const expected = {};
+ expected[fooSha] = "foo";
+ 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 () {
+ 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/checkout.js b/node/test/util/checkout.js
index 414227de5..f597244b2 100644
--- a/node/test/util/checkout.js
+++ b/node/test/util/checkout.js
@@ -32,10 +32,12 @@
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 SubmoduleUtil = require("../../lib/util/submodule_util");
const UserError = require("../../lib/util/user_error");
describe("Checkout", function () {
@@ -91,30 +93,51 @@ 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,
+ },
+ "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",
+ 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 +156,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",
},
@@ -290,12 +313,37 @@ 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,
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,
+ expectedCheckoutFromIndex: true,
+ 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,
+ 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",
committish: "1",
@@ -405,6 +453,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",
@@ -437,12 +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)) {
@@ -453,7 +519,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];
@@ -462,6 +528,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);
}));
@@ -476,7 +546,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,
@@ -493,7 +563,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",
@@ -556,4 +626,87 @@ 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 resolvedPaths = SubmoduleUtil.resolvePaths(c.paths,
+ index,
+ open,
+ true);
+ let checkoutFromIndex;
+ let annotated;
+ if (c.commit === ":0") {
+ checkoutFromIndex = true;
+ } else {
+ const mapped = maps.reverseCommitMap[c.commit];
+ annotated = yield NodeGit.Commit.lookup(repo, mapped);
+ }
+
+ yield Checkout.checkoutFiles(repo, {
+ commit: annotated,
+ resolvedPaths: resolvedPaths,
+ checkoutFromIndex: checkoutFromIndex
+ });
+ });
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.input,
+ c.expected,
+ manipulator,
+ c.fails);
+
+ }));
+ });
+ });
});
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());
+ }));
+});
diff --git a/node/test/util/cherry_pick_util.js b/node/test/util/cherry_pick_util.js
new file mode 100644
index 000000000..e00e35cab
--- /dev/null
+++ b/node/test/util/cherry_pick_util.js
@@ -0,0 +1,1098 @@
+/*
+ * 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 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
+ * 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;
+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,
+ },
+ "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, base);
+ assert.equal(result, c.expected);
+ }));
+ });
+});
+describe("computeChangesFromIndex", 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", null),
+ },
+ },
+ "addition": {
+ input: "a=B|x=S:Ct-1 s=Sa:1;Bt=t",
+ simpleChanges: {
+ "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"),
+ },
+ },
+ "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(new ConflictEntry(FILEMODE.COMMIT, "1"),
+ 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),
+ },
+ fails: true,
+ },
+ "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),
+ },
+ },
+ };
+ 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 head = yield repo.getHeadCommit();
+ const target = yield repo.getCommit(reverseCommitMap.t);
+ 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];
+ changes[name] = new SubmoduleChange(commitMap[change.oldSha],
+ commitMap[change.newSha],
+ null);
+ }
+ 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 || {}, "simple");
+
+ 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 || {});
+ }));
+ });
+ 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,
+ reverseCommitMap.a);
+ 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
+ // 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", null),
+ },
+ 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", 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;
+ 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", null),
+ },
+ 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", null),
+ },
+ 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],
+ null);
+ });
+ 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("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.
+ //
+ // 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",
+ },
+ "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 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|
+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:Os Edetached HEAD,b,a!I *a=~*a*8!W a=\
+<<<<<<< HEAD
+a
+=======
+8
+>>>>>>> message
+;
+`,
+ 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: `
+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: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.
+ 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 => {
+ 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.rewriteCommit(x, eightCommit,
+ "testCommand");
+ 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.
+
+ 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",
+ },
+ "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",
+ },
+ "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.
+ 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)`,
+ },
+ };
+
+ 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]);
+
+ 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);
+ });
+ 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;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;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;QC 1: g: 0 g;Bmaster=3;Os EHEAD,b,a!I b=b`,
+ expected: `
+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",
+ },
+ "continue with staged files": {
+ 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;Q`,
+ },
+ };
+ 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);
+ }));
+ });
+
+ 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.isNull(rast.sequencerState);
+ }));
+});
+
+describe("abort", function () {
+ const cases = {
+ "no cherry": {
+ input: "x=S",
+ fails: true,
+ },
+ "some changes in meta": {
+ 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 s=S/a:1;Bmaster=2;QC 1: 2: 0 2;I *s=~*~*s=S:1`,
+ expected: "x=S",
+ },
+
+ "sub with a conflict": {
+ input: `
+a=B:Ca-1;Cb-1 a=8;Ba=a;Bb=b|
+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
+=======
+8
+>>>>>>> message
+;
+`,
+ expected: `x=E:Q;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);
+ }));
+ });
+});
+});
+
+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);
+ }
+ );
+ });
+});
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/close_util.js b/node/test/util/close_util.js
index 300910b23..c37659bb1 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": {
@@ -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"],
@@ -74,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"],
@@ -96,24 +118,39 @@ describe("close_util", function () {
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 => {
- 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 + (sparse ? " sparse" : ""), 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);
- }));
+ }));
+ }
});
});
});
diff --git a/node/test/util/commit.js b/node/test/util/commit.js
index 7c57b62a5..26b72c3d0 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 fs = require("fs-promise");
const NodeGit = require("nodegit");
const path = require("path");
@@ -45,6 +46,16 @@ const TestUtil = require("../../lib/util/test_util");
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;
+ });
+}
+
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
@@ -73,7 +84,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, msgfunc(message),
+ subMessages, false);
return {
commitMap: mapCommitResult(result),
};
@@ -683,7 +695,6 @@ Untracked files:
expected: base,
options: {
all: false,
- showMetaChanges: true,
paths: [],
},
},
@@ -691,110 +702,34 @@ 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,
- },
- }),
+ expected: base,
},
- "with all": {
- state: "x=S:W README.md=88",
+ "sub staged": {
+ state: "a=B|x=U:C3-2;Bmaster=3;Os I a=b",
options: {
- showMetaChanges: true,
all: true,
},
- expected: base.copy({
- staged: {
- "README.md": FILESTATUS.MODIFIED,
+ 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: FILESTATUS.ADDED,
+ },
+ }), SAME),
+ }),
},
}),
},
- "sub staged": {
- state: "a=B|x=U:C3-2;Bmaster=3;Os I a=b",
+ "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,
},
@@ -808,7 +743,7 @@ Untracked files:
workdir: new Submodule.Workdir(new RepoStatus({
headCommit: "1",
staged: {
- a: FILESTATUS.ADDED,
+ "a/b/c": FILESTATUS.ADDED,
},
}), SAME),
}),
@@ -1000,11 +935,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 +960,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),
}),
},
},
@@ -1143,7 +1084,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 *() {
@@ -1187,51 +1130,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: {},
}),
},
},
@@ -1288,6 +1193,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
+ }
}),
},
},
@@ -1391,6 +1299,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];
@@ -1442,9 +1440,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,
@@ -1470,15 +1472,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",
@@ -1607,6 +1600,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];
@@ -1630,7 +1631,8 @@ x=U:Chi\n#x-2 foo=moo,s=Sa:s;Bmaster=x;Os Cmeh\n#s-1 a=a!H=s`,
subsToAmend,
all,
message,
- subMessages);
+ subMessages,
+ false);
return { commitMap: mapCommitResult(result) };
});
yield RepoASTTestUtil.testMultiRepoManipulator(c.input,
@@ -1897,22 +1899,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 => {
@@ -1935,6 +1922,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",
@@ -1948,6 +1942,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, },
@@ -1959,6 +1958,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 },
@@ -2029,6 +2033,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, };
@@ -2083,7 +2091,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);
+ msgfunc(message),
+ false);
const commitMap = {};
commitMap[result.metaCommit] = "x";
Object.keys(result.submoduleCommits).forEach(name => {
@@ -2633,7 +2642,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
@@ -2652,7 +2662,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.
@@ -2675,7 +2686,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
@@ -2703,7 +2715,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)
@@ -2740,7 +2753,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
@@ -2775,7 +2789,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.
# -----------------------------------------------------------------------------
@@ -2819,7 +2834,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
@@ -2868,7 +2884,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.
@@ -3052,41 +3069,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",
@@ -3096,9 +3100,19 @@ and for d
"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,
@@ -3120,32 +3134,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 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,
- 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,
@@ -3176,32 +3199,50 @@ 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=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=foo`
+ },
+ "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;Bmaster=x",
},
"amend with change": {
- initial: "x=S:C2-1 README.md=foo;Bmaster=2;I a=b",
+ 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 README.md=foo,a=b;Bmaster=x",
+ expected: `
+x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=foo,a=b`
},
"amend with no all": {
- initial: "x=S:C2-1;Bmaster=2;W README.md=8",
+ 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",
- 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`
},
"amend with all": {
- initial: "x=S:C2-1;Bmaster=2;W README.md=8",
+ 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,README.md=8;Bmaster=x",
+ expected: `
+x=U:Cfoo\n#x-2 s=Sa:s;Bmaster=x;Os Cfoo\n#s-1 README.md=8`,
},
- "amend with all but no meta": {
- initial: "x=S:C2-1;Bmaster=2;W 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,
- meta: false,
- 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=8!W foo=bar`,
},
"mismatch": {
initial: `
@@ -3224,9 +3265,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",
@@ -3234,38 +3278,47 @@ 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",
- editor: null,
+ 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
},
};
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 meta = undefined === c.meta ? true : false;
- const result = yield Commit.doAmendCommand(
- repo,
- cwd,
- c.message || null,
- meta,
- c.all || false,
- c.interactive || false,
- editor);
- return {
- commitMap: mapCommitResult(result),
- };
});
it(caseName, co.wrap(function *() {
yield RepoASTTestUtil.testMultiRepoManipulator(c.initial,
@@ -3275,4 +3328,170 @@ x=U:Chola\n#x-2 s=Sa:s;Bmaster=x;Os Cthere\n#s-1 a=a!H=s`,
}));
});
});
+
+ describe("submoduleHooksAreRun", function () {
+ const addNewFileHook = `#!/bin/bash
+echo -n bar > addedbyhook
+git add addedbyhook
+`;
+ const message = "msg";
+
+ 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`;
+
+ 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 result = yield Commit.doAmendCommand(
+ repo,
+ cwd,
+ message,
+ true, // all
+ false, // interactive
+ false,
+ false);
+
+ return {
+ commitMap: mapCommitResult(result),
+ };
+
+ });
+
+ 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,
+ msgfunc(message),
+ false);
+ const commitMap = {};
+ commitMap[result.metaCommit] = "x";
+ Object.keys(result.submoduleCommits).forEach(name => {
+ const sha = result.submoduleCommits[name];
+ commitMap[sha] = name;
+ });
+ return { commitMap: commitMap };
+ });
+
+ yield RepoASTTestUtil.testMultiRepoManipulator(initial,
+ expected,
+ f,
+ false);
+ }));
+
+ });
+});
+
+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();
+ // 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();
+
+ const repoStatus = yield Commit.getCommitStatus(repo, repo.path(), {
+ all: true,
+ paths: [],
+ });
+
+ yield Commit.commit(repo, true, repoStatus,
+ msgfunc("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);
+
+ }));
});
diff --git a/node/test/util/config_util.js b/node/test/util/config_util.js
new file mode 100644
index 000000000..27fdbc872
--- /dev/null
+++ b/node/test/util/config_util.js
@@ -0,0 +1,130 @@
+/*
+ * 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);
+ }));
+ });
+});
+
+describe("defaultSignature", function () {
+ it("works", co.wrap(function *() {
+ const repo = yield TestUtil.createSimpleRepository();
+ const actual = yield ConfigUtil.defaultSignature(repo);
+ 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/test/util/conflict_util.js b/node/test/util/conflict_util.js
new file mode 100644
index 000000000..504e58f93
--- /dev/null
+++ b/node/test/util/conflict_util.js
@@ -0,0 +1,133 @@
+/*
+ * 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 GitUtil = require("../../lib/util/git_util");
+const TestUtil = require("../../lib/util/test_util");
+
+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 GitUtil.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 GitUtil.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 GitUtil.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/deinit_util.js b/node/test/util/deinit_util.js
deleted file mode 100644
index c983333f4..000000000
--- a/node/test/util/deinit_util.js
+++ /dev/null
@@ -1,87 +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 NodeGit = require("nodegit");
-
-const DeinitUtil = require("../../lib/util/deinit_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);
- }));
-});
diff --git a/node/test/util/destitch_util.js b/node/test/util/destitch_util.js
new file mode 100644
index 000000000..48220855d
--- /dev/null
+++ b/node/test/util/destitch_util.js
@@ -0,0 +1,762 @@
+/*
+ * 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 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");
+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 BulkNotesUtil.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 = 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);
+ 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 = 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);
+ 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 BulkNotesUtil.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 BulkNotesUtil.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 BulkNotesUtil.readNotes(repo, localNotesRef);
+ const notes = BulkNotesUtil.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, {
+ includeRefsCommits : true,
+ actualTransformer: refMapper,
+ });
+ }));
+ });
+});
+});
diff --git a/node/test/util/diff_util.js b/node/test/util/diff_util.js
index 396ca92bd..7da94254c 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 },
@@ -130,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": {
@@ -208,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
@@ -217,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
@@ -226,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,
@@ -236,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,
@@ -252,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,
@@ -282,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": {
@@ -326,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": {
@@ -347,13 +346,16 @@ 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": {
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": {
@@ -366,11 +368,15 @@ 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": {
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^",
},
@@ -419,11 +425,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/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);
+ }
+ }));
});
diff --git a/node/test/util/git_util.js b/node/test/util/git_util.js
index e265eb860..e015e53e9 100644
--- a/node/test/util/git_util.js
+++ b/node/test/util/git_util.js
@@ -35,9 +35,9 @@ 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 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");
@@ -60,6 +60,7 @@ describe("GitUtil", function () {
expected: {
remoteName: null,
branchName: "master",
+ pushRemoteName: null,
},
},
"with remote": {
@@ -67,6 +68,7 @@ describe("GitUtil", function () {
branch: "bar",
expected: {
remoteName: "hoo",
+ pushRemoteName: "hoo",
branchName: "gob",
},
},
@@ -75,21 +77,97 @@ 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);
}));
});
});
+ 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");
@@ -199,6 +277,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
@@ -298,42 +388,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", function () {
- const tempdir = os.tmpdir();
- 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");
- }));
- });
-
describe("getCurrentRepo", function () {
let cwd;
@@ -378,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,
@@ -406,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",
@@ -417,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",
@@ -456,6 +531,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];
@@ -648,7 +726,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 *() {
@@ -661,7 +740,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);
}));
@@ -680,59 +760,13 @@ 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");
}));
});
- 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": {
@@ -850,12 +884,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"],
@@ -891,9 +932,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) {
@@ -927,7 +968,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");
}));
});
@@ -1101,7 +1143,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,
@@ -1111,4 +1153,87 @@ 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);
+ }
+ }));
+ });
+ });
+ 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);
+ }));
+ });
+ });
+ 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);
+ }));
+ });
});
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);
+ }));
+ });
+});
diff --git a/node/test/util/hook.js b/node/test/util/hook.js
new file mode 100644
index 000000000..f14363fcd
--- /dev/null
+++ b/node/test/util/hook.js
@@ -0,0 +1,72 @@
+/*
+ * 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 repo = yield TestUtil.createSimpleRepository();
+ const hookName = "fake_hook";
+ assert.doesNotThrow(
+ function () {
+ //Nothing happened, no error throws.
+ },
+ yield Hook.execHook(repo, 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);
+ const hookOutputFile = path.join(hooksDir, "hook_test");
+ yield fs.writeFile(hookFile,
+ "#!/bin/bash \necho 'it is a test hook' >" + hookOutputFile);
+ yield fs.chmod(hookFile, "755");
+ 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");
+ }));
+ });
+});
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/merge.js b/node/test/util/merge.js
deleted file mode 100644
index 4dee1280b..000000000
--- a/node/test/util/merge.js
+++ /dev/null
@@ -1,228 +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 Merge = require("../../lib//util/merge");
-const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util");
-const StatusUtil = require("../../lib/util/status_util");
-
-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);
- }));
- });
-});
diff --git a/node/test/util/merge_bare.js b/node/test/util/merge_bare.js
new file mode 100644
index 000000000..b46df8f7a
--- /dev/null
+++ b/node/test/util/merge_bare.js
@@ -0,0 +1,387 @@
+/*
+ * 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 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 = MergeCommon.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`,
+ theirCommit: "4",
+ ourCommit: "3",
+ },
+ "fast forward in normal mode": {
+ initial: "a=B|x=S:C2-1 s=Sa:1;Bfoo=2",
+ theirCommit: "2",
+ ourCommit: "1",
+ mode: MODE.NORMAL,
+ parents: ["1"],
+ },
+ "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`,
+ 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`,
+ 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|
+x=U:C3-2 t=Sa:b;C4-2 s=Sa:b;Bmaster=3;Bfoo=4;Os`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ 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",
+ },
+ "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`,
+ 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`,
+ 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`,
+ theirCommit: "3",
+ ourCommit: "2",
+ fails: true,
+ 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`,
+ theirCommit: "4",
+ ourCommit: "3",
+ fails: true,
+ 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`,
+ 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`,
+ 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`,
+ 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`,
+ theirCommit: "3",
+ ourCommit: "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`,
+ theirCommit: "t",
+ ourCommit: "l",
+ },
+ };
+ 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";
+
+ const doMerge = co.wrap(function *(repos, maps) {
+ const upToDate = null === expected;
+ const x = repos.x;
+ const reverseCommitMap = maps.reverseCommitMap;
+ 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 mode = !("mode" in c) ? MODE.FORCE_COMMIT : c.mode;
+ 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) {
+ 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]) :
+ [theirSha, ourSha];
+ 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_full_open.js b/node/test/util/merge_full_open.js
new file mode 100644
index 000000000..eadba1192
--- /dev/null
+++ b/node/test/util/merge_full_open.js
@@ -0,0 +1,434 @@
+/*
+ * 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",
+ 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",
+ 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",
+ 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
new file mode 100644
index 000000000..4fa50657d
--- /dev/null
+++ b/node/test/util/merge_util.js
@@ -0,0 +1,628 @@
+/*
+ * 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 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("MergeUtil", function () {
+ describe("fastForwardMerge", function () {
+ const cases = {
+ "simple": {
+ initial: "a=B|x=S:C2-1 q=Sa:1;Bfoo=2",
+ commit: "2",
+ expected: "x=E:Bmaster=2",
+ },
+ "simple detached": {
+ initial: "a=B|x=S:C2-1 u=Sa:1;Bfoo=2;*=",
+ commit: "2",
+ 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",
+ 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",
+ 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",
+ 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",
+ fails: true,
+ },
+ "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";
+ yield MergeUtil.fastForwardMerge(x,
+ commit,
+ message);
+ return {
+ commitMap: {},
+ };
+ });
+ 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 = 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",
+ 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",
+ 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",
+ 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",
+ },
+ "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",
+ },
+ "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`,
+ },
+ "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`,
+ 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;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",
+ },
+ "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|
+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",
+ 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")}
+`,
+ },
+ "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.ALLOW_BARE;
+
+ const doNotRecurse = c.doNotRecurse || [];
+ const result = yield MergeUtil.merge(x,
+ null,
+ commit,
+ mode,
+ openOption,
+ doNotRecurse,
+ 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,
+ expected || {},
+ doMerge,
+ c.fails);
+ }));
+ });
+ });
+ describe("continue", function () {
+ const cases = {
+ "no merge": {
+ initial: "x=S",
+ fails: true,
+ },
+ "continue in meta": {
+ 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;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;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;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;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;Q;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;
+ 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;Q;
+ Os Cyou\n#s-a,ac a=foo!H=s!Bac=ac;
+ Ot`,
+ },
+ };
+ 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:Qfoo#M 1: 1: 0 1",
+ expected: "x=E:Q",
+ },
+ "noop with sub": {
+ 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: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];
+ 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/open.js b/node/test/util/open.js
index 3ee32770d..c40e78849 100644
--- a/node/test/util/open.js
+++ b/node/test/util/open.js
@@ -34,11 +34,13 @@ 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");
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
@@ -51,6 +53,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",
@@ -76,7 +84,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,
@@ -96,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");
- const s2 = yield opener.getSubrepo("s");
+ 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");
@@ -106,11 +115,15 @@ 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", 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");
+ 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";
@@ -121,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");
+ const s = yield opener.getSubrepo("s", FORCE_OPEN);
const head = yield s.getHeadCommit();
assert.equal(head.id().tostrS(), subSha);
}));
@@ -146,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");
+ yield opener.getSubrepo("s", FORCE_OPEN);
const open = yield opener.getOpenSubs();
assert.deepEqual(Array.from(open), []);
}));
@@ -171,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");
+ yield opener.getSubrepo("s", FORCE_OPEN);
const opened = yield opener.getOpenedSubs();
assert.deepEqual(opened, []);
}));
@@ -180,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");
+ yield opener.getSubrepo("s", FORCE_OPEN);
const opened = yield opener.getOpenedSubs();
assert.deepEqual(opened, ["s"]);
}));
@@ -189,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 *() {
@@ -197,16 +210,20 @@ 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 result = yield opener.isOpen("s");
+ const s = yield opener.getSubrepo("s", FORCE_OPEN);
+ const result = 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";
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);
}));
});
diff --git a/node/test/util/print_status_util.js b/node/test/util/print_status_util.js
index d2007cde7..3661fc8c4 100644
--- a/node/test/util/print_status_util.js
+++ b/node/test/util/print_status_util.js
@@ -32,12 +32,17 @@
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");
describe("PrintStatusUtil", function () {
+ const CommitAndRef = SequencerState.CommitAndRef;
+ const TYPE = SequencerState.TYPE;
+ const FILEMODE = NodeGit.TreeEntry.FILEMODE;
+ const BLOB = FILEMODE.BLOB;
const FILESTATUS = RepoStatus.FILESTATUS;
const RELATION = RepoStatus.Submodule.COMMIT_RELATION;
const StatusDescriptor = PrintStatusUtil.StatusDescriptor;
@@ -76,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": {
@@ -101,6 +109,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];
@@ -624,30 +637,48 @@ describe("PrintStatusUtil", function () {
});
});
- describe("printRebase", function () {
+ describe("getSequencerCommand", function () {
const cases = {
- "basic": {
- input: new Rebase("master", "xxx", "ffffffffffffffff"),
- check: /rebase in progress/
+ "merge": {
+ input: TYPE.MERGE,
+ expected: "merge",
},
- "branch": {
- input: new Rebase("master", "xxx", "ffffffffffffffff"),
- check: /master/
+ "rebase": {
+ input: TYPE.REBASE,
+ expected: "rebase",
},
- "sha": {
- input: new Rebase("master", "xxx", "ffffffffffffffff"),
- check: /ffff/,
+ "cherry-pick": {
+ input: TYPE.CHERRY_PICK,
+ expected: "cherry-pick",
},
};
Object.keys(cases).forEach(caseName => {
const c = cases[caseName];
it(caseName, function () {
- const result = PrintStatusUtil.printRebase(c.input);
- assert.match(result, c.check);
+ 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": {
@@ -663,6 +694,11 @@ describe("PrintStatusUtil", function () {
}),
check: /aaaa/,
},
+ "no-commits": {
+ input: new RepoStatus({
+ }),
+ check: /No commits yet/,
+ },
};
Object.keys(cases).forEach(caseName => {
const c = cases[caseName];
@@ -687,20 +723,39 @@ 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
+`,
+ shortExact: "\n",
},
- "rebase": {
+ "sequencer": {
input: new RepoStatus({
currentBranchName: "master",
- rebase: new Rebase("x", "y", "z"),
+ sequencerState: new SequencerState({
+ type: TYPE.REBASE,
+ originalHead: new CommitAndRef("foo", null),
+ target: new CommitAndRef("bar", "baz"),
+ commits: ["2", "1"],
+ currentCommit: 1,
+ }),
}),
- regex: /rebas/,
+ 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
+`,
+ shortExact: "\n",
},
"detached": {
input: new RepoStatus({
headCommit: "ffffaaaaffffaaaa",
}),
regex: /detached/,
+ shortExact: "\n",
},
"dirty meta": {
input: new RepoStatus({
@@ -710,6 +765,8 @@ describe("PrintStatusUtil", function () {
},
}),
regex: /.*qrst/,
+ shortExact: `${colors.green("A")} qrst
+`,
},
"dirty sub": {
input: new RepoStatus({
@@ -726,6 +783,31 @@ describe("PrintStatusUtil", function () {
},
}),
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({
@@ -736,6 +818,8 @@ describe("PrintStatusUtil", function () {
}),
cwd: "u/v",
regex: /\.\.\/\.\.\/qrst/,
+ shortExact: `${colors.green("A")} qrst
+`,
},
"untracked": {
input: new RepoStatus({
@@ -749,6 +833,8 @@ Untracked files:
\t${colors.red("foo")}
+`,
+ shortExact: `${colors.red("?") + colors.red("?")} foo
`,
},
"change in sub workdir": {
@@ -768,6 +854,8 @@ Changes to be committed:
\t${colors.green("modified: zap")} (submodule, new commits)
+`,
+ shortExact: `${colors.green("M")} zap
`,
},
};
@@ -787,6 +875,11 @@ Changes to be committed:
else {
assert.match(result, c.regex);
}
+
+
+ const shortResult = PrintStatusUtil.printRepoStatusShort(
+ c.input, cwd);
+ assert.equal(c.shortExact, shortResult);
});
});
});
@@ -794,15 +887,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 +905,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 +914,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 +924,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 +936,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 +950,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 +960,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 +974,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/pull.js b/node/test/util/pull.js
index e30548fff..93a7380b5 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",
@@ -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];
@@ -117,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));
diff --git a/node/test/util/push.js b/node/test/util/push.js
index dfec266bb..62baab433 100644
--- a/node/test/util/push.js
+++ b/node/test/util/push.js
@@ -30,12 +30,19 @@
*/
"use strict";
-const co = require("co");
+const assert = require("chai").assert;
+const co = require("co");
+const NodeGit = require("nodegit");
+const rimraf = require("rimraf");
-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 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");
+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,11 +197,208 @@ 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 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" : {
+ 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);
+ yield SubmoduleConfigUtil.deinit(repo, subs);
+ }
+
+ const pushMap = yield Push.getPushMap(repo, source, 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,
+ false,
+ {includeRefsCommits : true});
+ }));
+ });
+
+ 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, "2", 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, "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, "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, "2", commit);
+ assert.deepEqual({}, pushMapRm);
+ }));
+});
+
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);
});
@@ -213,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"),
@@ -294,8 +545,121 @@ x=E:Rorigin=a foo=2`,
c.expected,
c.manipulator,
c.fails, {
+ includeRefsCommits : true,
expectedTransformer: refMapper,
});
}));
});
});
+
+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 ref, no close pushed commit": {
+ source: "5",
+ refs: {
+ },
+ expectedCommit: null,
+ },
+ "one ref, commit is pushed": {
+ source: "5",
+ refs: {
+ "refs/remotes/origin/1": "5",
+ },
+ expectedCommit: "5",
+ },
+ "one ref, far commit": {
+ source: "5",
+ refs: {
+ "refs/remotes/origin/1": "1",
+ },
+ expectedCommit: "1",
+ },
+ "one ref, close commit": {
+ source: "5",
+ refs: {
+ "refs/remotes/origin/1": "2",
+ },
+ expectedCommit: "1",
+ },
+ "lots of refs, none matching": {
+ source: "5",
+ remote: "origin",
+ refs: {
+ "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",
+ refs: {
+ "refs/remotes/origin/1": "1",
+ "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",
+ refs: {
+ "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",
+ 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, commit);
+ if (null !== c.expectedCommit) {
+ assert.deepEqual(repos.reverseCommitMap[c.expectedCommit],
+ actualCommit.id().tostrS());
+ } else {
+ assert.deepEqual(null, actualCommit);
+ }
+ }));
+ });
+});
diff --git a/node/test/util/read_repo_ast_util.js b/node/test/util/read_repo_ast_util.js
index af84dc922..b19c2fd69 100644
--- a/node/test/util/read_repo_ast_util.js
+++ b/node/test/util/read_repo_ast_util.js
@@ -36,14 +36,21 @@ const fs = require("fs-promise");
const NodeGit = require("nodegit");
const path = require("path");
-const DeinitUtil = require("../../lib/util/deinit_util");
+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");
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");
+const CommitAndRef = SequencerState.CommitAndRef;
+const File = RepoAST.File;
+
// Test utilities
/**
@@ -60,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({
@@ -71,7 +78,6 @@ const astFromSimpleRepo = co.wrap(function *(repo) {
});
});
-
/**
* Create a repository with a branch and two commits and a `RepoAST` object
* representing its expected state.
@@ -97,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",
});
@@ -146,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",
});
@@ -210,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({
@@ -233,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({
@@ -262,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({
@@ -292,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({
@@ -320,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({
@@ -339,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({
@@ -378,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({
@@ -402,9 +408,9 @@ 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 = builder.write();
+ const treeObj = yield builder.write();
const tree = yield r.getTree(treeObj.tostrS());
const commitId = yield NodeGit.Commit.create(r,
0,
@@ -437,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();
@@ -461,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 = 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");
@@ -471,7 +476,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();
@@ -504,7 +509,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({
@@ -534,10 +539,10 @@ 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":""},
+ changes: { "README.md": new File("", false)},
message: "first commit",
});
commits[commit.id().tostrS()] = new Commit({
@@ -582,10 +587,10 @@ 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":""},
+ changes: { "README.md": new File("", false)},
message: "first commit",
});
commits[commit.id().tostrS()] = new Commit({
@@ -633,11 +638,11 @@ 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({
- changes: {"README.md":""},
+ changes: { "README.md": new File("", false)},
message: "first commit",
});
commits[commit.id().tostrS()] = new Commit({
@@ -683,7 +688,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({
@@ -691,7 +696,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);
@@ -711,7 +716,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({
@@ -719,7 +724,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);
@@ -741,7 +746,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({
@@ -749,7 +754,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);
@@ -770,7 +775,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({
@@ -794,11 +799,11 @@ 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({
- changes: {"README.md":""},
+ changes: { "README.md": new File("", false)},
message: "first commit",
});
const expected = new RepoAST({
@@ -839,11 +844,11 @@ 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({
- changes: {"README.md":""},
+ changes: { "README.md": new File("", false)},
message: "first commit",
});
commits[commit.id().tostrS()] = new Commit({
@@ -876,7 +881,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({
@@ -898,7 +903,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({
@@ -908,7 +913,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);
@@ -920,7 +925,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({
@@ -930,7 +935,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);
@@ -946,7 +951,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({
@@ -956,8 +961,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);
@@ -1001,8 +1006,8 @@ describe("readRAST", function () {
const workdir = repo.workdir();
const firstCommit = yield repo.getHeadCommit();
const firstSha = firstCommit.id().tostrS();
- const sig = repo.defaultSignature();
- yield repo.createBranch("b", firstCommit, 0, sig);
+ const sig = yield repo.defaultSignature();
+ 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"]);
@@ -1021,22 +1026,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({
@@ -1054,7 +1059,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.
@@ -1063,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"]);
@@ -1086,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);
@@ -1141,23 +1146,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({
@@ -1211,7 +1216,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.
@@ -1220,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"]);
@@ -1243,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);
@@ -1293,23 +1298,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({
@@ -1366,7 +1371,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);
@@ -1374,7 +1379,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 = {};
@@ -1433,15 +1438,73 @@ 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("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);
@@ -1459,7 +1522,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),
@@ -1516,14 +1579,15 @@ describe("readRAST", function () {
r,
"x",
"foo",
- null);
+ null,
+ false);
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": ""},
+ changes: { "README.md": new File("", false)},
message: "first commit",
});
const expected = new RepoAST({
@@ -1562,7 +1626,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,
@@ -1574,7 +1639,7 @@ describe("readRAST", function () {
};
subCommits[subCommit.id().tostrS()] = new RepoAST.Commit({
changes: {
- "README.md": "data",
+ "README.md": new File("data", false),
},
message: "message\n",
});
@@ -1583,7 +1648,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({
@@ -1622,10 +1687,354 @@ 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);
}));
+ 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 GitUtil.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(new File("xxx", false),
+ new File("yyy", false),
+ new File("zzz", false)),
+ },
+ workdir: {
+ "README.md": new File("conflicted", false),
+ },
+ });
+ 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 GitUtil.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(new File("xxx", false),
+ null,
+ new File("zzz", false)),
+ },
+ workdir: {
+ "README.md": new File("conflicted", false),
+ },
+ });
+ 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": new File("", false)},
+ 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);
+ }));
+ });
+ 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, {});
+ }));
+ 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/rebase_util.js b/node/test/util/rebase_util.js
index b0668030f..f1b7e5a6d 100644
--- a/node/test/util/rebase_util.js
+++ b/node/test/util/rebase_util.js
@@ -30,428 +30,746 @@
*/
"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 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.
- let commitMap = {};
- function addNewCommit(newCommit, oldCommit, suffix) {
- const oldLogicalCommit = maps.commitMap[oldCommit];
- commitMap[newCommit] = oldLogicalCommit + suffix;
+ const commitMap = {};
+ RepoASTTestUtil.mapCommits(commitMap,
+ result.metaCommits,
+ maps.commitMap,
+ "M");
+ RepoASTTestUtil.mapSubCommits(commitMap,
+ result.submoduleCommits,
+ maps.commitMap);
+ const newCommits = result.newCommits || {};
+ for (let path in newCommits) {
+ commitMap[newCommits[path]] = `N${path}`;
}
- 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);
- });
- });
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);
-
- return yield RebaseUtil.rebase(repo, originalCommit);
- });
+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 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"),
+ 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 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: "\
-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: "\
-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: `
-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: `
-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: `
-x=E:C7M-6M s=Sa:ds;C6M-5M 6=6;C5M-4M s=Sa:cs;C4M-3 4=4;Bmaster=7M;
+ "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);
+ });
+ 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",
+ onto: "1",
+ },
+ "nothing to do, in past": {
+ initial: "x=S:C2-1;Bmaster=2",
+ onto: "1",
+ },
+ "nothing to do, detached": {
+ initial: "x=S:C2-1;H=2",
+ onto: "1",
+ },
+ "ffwd": {
+ initial: "x=S:C2-1 s=S/a:1;Bfoo=2",
+ onto: "2",
+ expected: "x=E:Bmaster=2",
+ },
+ "simple rebase": {
+ 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",
+ },
+ "two commits": {
+ initial: `
+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",
+ },
+ "up-to-date with sub": {
+ initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=3;Bfoo=2",
+ onto: "2",
+ },
+ "ffwd with sub": {
+ initial: "a=Aa:Cb-a;Bfoo=b|x=U:C3-2 s=Sa:b;Bmaster=2;Bfoo=3",
+ onto: "3",
+ expected: "x=E:Bmaster=3",
+ },
+ "rebase change in closed sub": {
+ 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=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=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:C4M-3 s=Sa:bs;Bmaster=4M;Os Cbs-c b=b!H=bs`,
+ },
+ "rebase change in sub with two intervening commits": {
+ initial: `
+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 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;
+ },
+ "rebase change in sub with two intervening commits, open": {
+ initial: `
+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"),
- expected: `
-x=E:C7M-6M s=Sa:ds;C6M-5M 6=6;C5M-4M s=Sa:cs;C4M-3 4=4;Bmaster=7M;
+ onto: "3",
+ expected: `
+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: "\
+ },
+ "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: "\
-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: `
-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: "\
-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: "\
-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",
- },
- "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: "\
-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",
- },
- "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",
- },
- "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",
- },
- "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",
- },
- "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",
- },
- "open sub ffwd'd": {
- initial: `
+ onto: "4",
+ expected: "x=E:Bmaster=4",
+ },
+ "no ffwd, but can ffwd sub": {
+ 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=B:Ca-1;Cb-a;Cc-b;Bmaster=b;Bfoo=c|
+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",
+ },
+ "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=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|
+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`,
+ 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 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 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`,
+ },
+ "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`,
+ 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`,
+ 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"),
- 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 = `
+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=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: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;
+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.
+ 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: `
+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": {
+// 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);
+ if (null !== result.errorMessage) {
+ assert.isString(result.errorMessage);
}
- assert(threw, "should have thrown");
- const open = yield SubmoduleUtil.isVisible(repo, "s");
- assert(open, "should be open");
+ assert.equal(result.errorMessage, errorMessage);
+ return result;
+ });
+ const rebase = makeRebaser(rebaseOp);
+ it(caseName, co.wrap(function *() {
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.initial,
+ c.expected,
+ rebase,
+ c.fails);
}));
});
+});
- 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: `
-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);
- }));
- });
+describe("abort", function () {
+ const cases = {
+ "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: `
+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`
+ },
+ "check that branch is restored": {
+ 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: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 => {
+ 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: `
-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`,
- },
- "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: `
-x=S:C4M-3M 4=4;C3M-2 3=3,qq=hh;Bmaster=4M;E`,
- },
- "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: `
-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: `
+});
+describe("continue", function () {
+ const cases = {
+ "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: `
+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,
+ },
+ "continue finishes with new commit": {
+ 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;H=4;Bold=3;
+ QR 3:refs/heads/master 4: 0 3;
+ Os I foo=bar`,
+ expected: `
+x=E:C3M-4 s=Sa:Ns;Bmaster=3M;*=master;Q;Os CNs-b foo=bar!H=Ns;`,
+ },
+ "continue makes a new conflict with current op": {
+ initial: `
+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: `\
+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!
+ 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`
- },
- "with rebase in submodule, other open subs": {
- initial: `
+ expected: `
+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`
- },
- "with rebase in submodule, staged commit in another submodule": {
- initial: `
+ expected: `
+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;
+ expected: `
+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: `
+ },
+ "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;
+ expected: `
+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: `
+ },
+ "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`,
- },
- "multiple in subs": {
- initial: `
+ expected: `
+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;
+ expected: `
+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`
+ },
+ };
+ Object.keys(cases).forEach(caseName => {
+ const c = cases[caseName];
+ it(caseName, co.wrap(function *() {
+ 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;
+ }));
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.initial,
+ c.expected,
+ continuer,
+ c.fails);
+ }));
+ });
+
+ 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 continuer = makeRebaser(co.wrap(function *(repos) {
- return yield RebaseUtil.continue(repos.x);
- }));
- yield RepoASTTestUtil.testMultiRepoManipulator(c.initial,
- c.expected,
- continuer,
- c.fails);
+ 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);
}));
});
});
});
+
+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);
+ }));
+ });
+});
+});
diff --git a/node/test/util/repo_ast.js b/node/test/util/repo_ast.js
index 1792e177b..5cd7d9c9a 100644
--- a/node/test/util/repo_ast.js
+++ b/node/test/util/repo_ast.js
@@ -35,6 +35,10 @@ 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;
describe("Branch", function () {
it("breath", function () {
@@ -49,6 +53,14 @@ describe("RepoAST", function () {
});
});
+ 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");
@@ -61,6 +73,93 @@ 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(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);
+ 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 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(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),
+ 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 () {
@@ -77,11 +176,11 @@ describe("RepoAST", function () {
"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": {
@@ -192,12 +291,20 @@ 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,
+ esequencerState: ("sequencerState" in expected) ?
+ expected.sequencerState : null,
+ esparse : ("sparse" in expected) ?
+ expected.sparse : false,
fails : fails,
};
}
+ 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(
@@ -207,20 +314,22 @@ describe("RepoAST", function () {
head: null,
currentBranchName: null,
rebase: null,
+ sequencerState: null,
bare: false,
+ sparse: false,
},
undefined,
false),
"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),
@@ -230,6 +339,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) },
@@ -349,20 +470,20 @@ describe("RepoAST", function () {
"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},
@@ -373,23 +494,34 @@ describe("RepoAST", function () {
head: "1",
index: { foo: new RepoAST.Submodule("z", "a") },
}, false),
+ "index with conflict": m({
+ commits: { "1": c1},
+ head: "1",
+ index: { foo: aConflict },
+ workdir: { foo: barFile },
+ }, {
+ commits: { "1": c1},
+ head: "1",
+ 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 },
@@ -408,45 +540,45 @@ describe("RepoAST", function () {
}, 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",
@@ -488,6 +620,74 @@ describe("RepoAST", function () {
"bad rebase": m({
rebase: new Rebase("fff", "1", "1"),
}, undefined, true),
+ "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 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 sequencer": m({
+ sequencerState: new SequencerState({
+ type: REBASE,
+ originalHead: new CommitAndRef("foo", null),
+ target: new CommitAndRef("bar", null),
+ commits: ["2", "1"],
+ currentCommit: 1,
+ }),
+ }, undefined, true),
+ "with sparse": m({ sparse: true }, { sparse: true} , false),
};
Object.keys(cases).forEach(caseName => {
it(caseName, function () {
@@ -510,7 +710,9 @@ describe("RepoAST", function () {
assert.deepEqual(obj.workdir, c.eworkdir);
assert.deepEqual(obj.openSubmodules, c.eopenSubmodules);
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);
@@ -557,18 +759,12 @@ describe("RepoAST", function () {
});
});
+ 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 c2 = new Commit({ changes: { foo: "baz" }});
- const mergeChild = new Commit({
- parents: ["3"],
- changes: { bam: "blast" },
- });
- const merge = new Commit({
- parents: ["1", "2"],
- changes: {}
- });
+ const c1 = new Commit({ changes: { foo: barFile }});
const deleter = new Commit({
parents: ["1"],
changes: { foo: null }
@@ -582,28 +778,9 @@ describe("RepoAST", function () {
"one": {
commits: { "1": c1},
from: "1",
- expected: { foo: "bar" },
+ expected: { foo: barFile },
ecache: {
- "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" },
+ "1": { foo: barFile },
},
},
"deletion": {
@@ -618,18 +795,18 @@ describe("RepoAST", function () {
"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 => {
@@ -648,16 +825,25 @@ describe("RepoAST", function () {
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,
+ originalHead: new CommitAndRef("1", null),
+ target: new CommitAndRef("1", null),
+ commits: ["1"],
+ currentCommit: 0,
+ }),
bare: false,
+ sparse: false,
});
const newArgs = {
commits: { "2": new RepoAST.Commit()},
@@ -666,10 +852,18 @@ describe("RepoAST", function () {
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,
+ originalHead: new CommitAndRef("2", "refs/heads/master"),
+ target: new CommitAndRef("2", null),
+ commits: ["2"],
+ currentCommit: 0,
+ }),
bare: false,
+ sparse: false,
};
const cases = {
"trivial": {
@@ -686,6 +880,7 @@ describe("RepoAST", function () {
index: {},
workdir: {},
rebase: null,
+ sequencerState: null,
},
e: new RepoAST({
commits: { "1": new RepoAST.Commit()},
@@ -696,6 +891,23 @@ describe("RepoAST", function () {
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 () {
@@ -711,7 +923,9 @@ 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.sequencerState, c.e.sequencerState);
assert.equal(obj.bare, c.e.bare);
+ assert.equal(obj.sparse, c.e.sparse);
});
});
});
@@ -722,19 +936,33 @@ describe("RepoAST", function () {
// `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 c1 = new Commit({ changes: { foo: "bar" }});
+ const Conflict = RepoAST.Conflict;
+ 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: 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_test_util.js b/node/test/util/repo_ast_test_util.js
index 2ec98276d..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 = 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 = 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 = 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 = 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: {
@@ -332,7 +328,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];
@@ -353,4 +359,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 ec244d9e5..1a7e38f04 100644
--- a/node/test/util/repo_ast_util.js
+++ b/node/test/util/repo_ast_util.js
@@ -35,8 +35,16 @@ const assert = require("chai").assert;
const RepoAST = require("../../lib/util/repo_ast");
const RepoASTUtil = require("../../lib/util/repo_ast_util");
-describe("RepoAstUtil", function () {
+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;
+ const MERGE = Sequencer.TYPE.MERGE;
+ const CommitAndRef = Sequencer.CommitAndRef;
+ const Submodule = RepoAST.Submodule;
describe("assertEqualCommits", function () {
const Commit = RepoAST.Commit;
const cases = {
@@ -47,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: barFile },
+ message: "foo",
+ }),
+ },
+ "with null data": {
+ actual: new Commit({
+ parents: ["1"],
+ changes: { foo: null },
message: "foo",
}),
expected: new Commit({
parents: ["1"],
- changes: { foo: "bar" },
+ changes: { foo: null },
message: "foo",
}),
},
@@ -63,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,
},
@@ -131,12 +151,13 @@ describe("RepoAstUtil", function () {
describe("assertEqualASTs", function () {
const AST = RepoAST;
+ const Conflict = RepoAST.Conflict;
const Commit = AST.Commit;
const Rebase = AST.Rebase;
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({
@@ -162,10 +183,18 @@ 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({
+ type: MERGE,
+ originalHead: new CommitAndRef("1", null),
+ target: new CommitAndRef("1", "foo/bar"),
+ commits: ["1"],
+ currentCommit: 0,
+ }),
bare: false,
+ sparse: false,
}),
expected: new AST({
commits: { "1": aCommit},
@@ -175,10 +204,18 @@ 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({
+ type: MERGE,
+ originalHead: new CommitAndRef("1", null),
+ target: new CommitAndRef("1", "foo/bar"),
+ commits: ["1"],
+ currentCommit: 0,
+ }),
bare: false,
+ sparse: false,
}),
},
"bad bare": {
@@ -189,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({
@@ -206,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,
@@ -219,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({
@@ -232,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,
@@ -246,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({
@@ -257,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,
@@ -272,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({
@@ -284,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,
@@ -299,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({
@@ -308,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,
@@ -321,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({
@@ -331,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,
@@ -343,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({
@@ -354,11 +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: barFile, },
+ }),
+ expected: new AST({
+ index: { foo: new File("baz", false), },
+ }),
+ fails: true,
+ },
+ "regex match data miss": {
+ actual: new AST({
+ index: { foo: barFile, },
+ }),
+ expected: new AST({
+ index: { foo: new File("^^ar", false) },
+ }),
+ fails: true,
+ },
+ "regex match data hit": {
+ actual: new AST({
+ index: { foo: barFile, },
+ }),
+ expected: new AST({
+ 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},
@@ -367,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({
@@ -377,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,
@@ -390,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},
@@ -399,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,
@@ -412,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({
@@ -422,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",
@@ -497,7 +572,120 @@ 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: {
+ "foo": new Conflict(new File("foo", false),
+ new File("bar", false),
+ new File("baz", true)),
+ },
+ workdir: {
+ "foo": new File("boo", false),
+ },
+ }),
+ expected: new AST({
+ index: {
+ "foo": new Conflict(new File("foo", false),
+ new File("bar", false),
+ new File("baz", true)),
+ },
+ workdir: {
+ "foo": new File("boo", false),
+ },
+ }),
+ },
+ "diff Conflict": {
+ actual: new AST({
+ index: {
+ "foo": new Conflict(new File("foo", false),
+ new File("bar", false),
+ new File("baz", true)),
+ },
+ workdir: {
+ "foo": new File("boo", false),
+ },
+ }),
+ expected: new AST({
+ index: {
+ "foo": new Conflict(new File("foo", true),
+ new File("bar", false),
+ new File("baz", true)),
+ },
+ workdir: {
+ "foo": new File("boo", false),
+ },
+ }),
+ 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 () {
@@ -545,6 +733,7 @@ describe("RepoAstUtil", function () {
}
}),
m: {},
+ fails: true,
e: new RepoAST({
commits: { "1": c1 },
head: "1",
@@ -659,6 +848,7 @@ describe("RepoAstUtil", function () {
},
head: "2",
}),
+ m: { "2": "2", "y": "y" },
u: { x: "z" },
e: new RepoAST({
commits: {
@@ -678,7 +868,7 @@ describe("RepoAstUtil", function () {
},
head: "2",
}),
- m: { "3": "4" },
+ m: { "3": "4", "2": "2" },
e: new RepoAST({
commits: {
"2": new RepoAST.Commit({
@@ -697,7 +887,7 @@ describe("RepoAstUtil", function () {
},
head: "2",
}),
- m: { "3": "4" },
+ m: { "3": "4", "2": "2" },
u: { x: "z" },
e: new RepoAST({
commits: {
@@ -717,7 +907,7 @@ describe("RepoAstUtil", function () {
},
head: "2",
}),
- m: { "8": "4" },
+ m: { "2": "2", "3": "3" },
u: { r: "z" },
e: new RepoAST({
commits: {
@@ -735,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({
@@ -744,7 +934,7 @@ describe("RepoAstUtil", function () {
remotes: {
foo: new RepoAST.Remote("my-url"),
},
- index: { foo: "bar" },
+ index: { foo: barFile },
}),
},
"index unchanged submodule": {
@@ -755,11 +945,11 @@ describe("RepoAstUtil", function () {
foo: new RepoAST.Remote("my-url"),
},
index: {
- foo: "bar",
+ foo: barFile,
baz: new RepoAST.Submodule("x", "y"),
},
}),
- m: { "1": "2"},
+ m: { "1": "2", "y": "y" },
u: { "q": "z"},
e: new RepoAST({
commits: { "2": c1 },
@@ -768,7 +958,7 @@ describe("RepoAstUtil", function () {
foo: new RepoAST.Remote("my-url"),
},
index: {
- foo: "bar",
+ foo: barFile,
baz: new RepoAST.Submodule("x", "y"),
},
}),
@@ -781,7 +971,7 @@ describe("RepoAstUtil", function () {
foo: new RepoAST.Remote("my-url"),
},
index: {
- foo: "bar",
+ foo: barFile,
baz: new RepoAST.Submodule("q", "1"),
},
}),
@@ -794,11 +984,33 @@ describe("RepoAstUtil", function () {
foo: new RepoAST.Remote("my-url"),
},
index: {
- foo: "bar",
+ foo: barFile,
baz: new RepoAST.Submodule("z", "2"),
},
}),
},
+ "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 },
@@ -806,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({
@@ -815,7 +1027,7 @@ describe("RepoAstUtil", function () {
remotes: {
foo: new RepoAST.Remote("my-url"),
},
- workdir: { foo: "bar" },
+ workdir: { foo: barFile },
}),
},
"submodule with changes": {
@@ -831,7 +1043,7 @@ describe("RepoAstUtil", function () {
}
})},
}),
- m: { "1": "2" },
+ m: { "1": "2", "y": "y" },
u: { "x": "z" },
e: new RepoAST({
commits: { "2": c1 },
@@ -859,17 +1071,31 @@ describe("RepoAstUtil", function () {
rebase: new Rebase("foo", "2", "2"),
}),
},
- "rebase unmapped": {
+ "sequencer": {
i: new RepoAST({
commits: { "1": c1 },
head: "1",
- rebase: new Rebase("foo", "1", "1"),
+ sequencerState: new Sequencer({
+ type: MERGE,
+ originalHead: new CommitAndRef("1", null),
+ target: new CommitAndRef("1", "foo/bar"),
+ commits: ["1"],
+ currentCommit: 0,
+ message: "meh",
+ }),
}),
- m: {},
+ m: { "1": "2"},
e: new RepoAST({
- commits: { "1": c1 },
- head: "1",
- rebase: new Rebase("foo", "1", "1"),
+ 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,
+ message: "meh",
+ }),
}),
},
};
@@ -878,9 +1104,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);
});
});
@@ -890,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/repo_status.js b/node/test/util/repo_status.js
index 440e337c0..516a8d74a 100644
--- a/node/test/util/repo_status.js
+++ b/node/test/util/repo_status.js
@@ -32,10 +32,14 @@
const assert = require("chai").assert;
-const Rebase = require("../../lib/util/rebase");
-const RepoStatus = require("../../lib/util/repo_status");
+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;
@@ -475,6 +479,7 @@ describe("RepoStatus", function () {
workdir: {},
submodules: {},
rebase: null,
+ sequencerState: null,
};
return Object.assign(result, args);
}
@@ -500,6 +505,13 @@ describe("RepoStatus", function () {
}),
},
rebase: new Rebase("foo", "1", "2"),
+ sequencerState: new SequencerState({
+ type: MERGE,
+ originalHead: new CommitAndRef("foo", null),
+ target: new CommitAndRef("bar", "baz"),
+ commits: ["2", "1"],
+ currentCommit: 1,
+ }),
},
e: m({
currentBranchName: "foo",
@@ -513,6 +525,13 @@ describe("RepoStatus", function () {
}),
},
rebase: new Rebase("foo", "1", "2"),
+ sequencerState: new SequencerState({
+ type: MERGE,
+ originalHead: new CommitAndRef("foo", null),
+ target: new CommitAndRef("bar", "baz"),
+ commits: ["2", "1"],
+ currentCommit: 1,
+ }),
}),
}
};
@@ -528,6 +547,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.sequencerState, c.e.sequencerState);
});
});
@@ -853,6 +873,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": {
@@ -957,6 +1050,13 @@ describe("RepoStatus", function () {
},
workdir: { x: FILESTATUS.MODIFIED },
rebase: new Rebase("2", "4", "b"),
+ 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",
@@ -967,6 +1067,13 @@ describe("RepoStatus", function () {
},
workdir: { x: FILESTATUS.ADDED },
rebase: new Rebase("a", "4", "b"),
+ 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();
@@ -984,6 +1091,7 @@ describe("RepoStatus", function () {
submodules: anotherStat.submodules,
workdir: anotherStat.workdir,
rebase: anotherStat.rebase,
+ sequencerState: anotherStat.sequencerState,
});
assert.deepEqual(newStat, anotherStat);
});
diff --git a/node/test/util/reset.js b/node/test/util/reset.js
index 2b9d85707..f7df3e2b8 100644
--- a/node/test/util/reset.js
+++ b/node/test/util/reset.js
@@ -30,13 +30,163 @@
*/
"use strict";
-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 Reset = require("../../lib/util/reset");
-const RepoASTTestUtil = require("../../lib/util/repo_ast_test_util");
+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("resetMetaRepo", function () {
+ const cases = {
+ "noop": {
+ input: "x=S",
+ to: "1",
+ },
+ "another": {
+ input: "x=S:C2-1;Bfoo=2",
+ to: "2",
+ expected: "x=E:I 2=2;W 2",
+ },
+ "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",
+ },
+ "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 *() {
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.input,
+ c.expected,
+ updateHead);
+
+ }));
+ });
+ 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");
+ 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, 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"], 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);
+ }));
+ });
describe("reset", function () {
// We are deferring the actual reset logic to NodeGit, so we are not
@@ -61,31 +211,19 @@ 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",
+ "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,
- 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"
},
- "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 +248,37 @@ 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",
+ },
+ "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,
+ },
+ "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];
@@ -139,31 +308,12 @@ 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`,
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/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);
+ }));
+ });
+ });
+});
diff --git a/node/test/util/sequencer_state.js b/node/test/util/sequencer_state.js
new file mode 100644
index 000000000..2e6e7e69a
--- /dev/null
+++ b/node/test/util/sequencer_state.js
@@ -0,0 +1,290 @@
+/*
+ * 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,
+ message: "meh",
+ });
+ 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);
+ assert.equal(seq.message, "meh");
+ });
+ 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,
+ message: "moo",
+ }),
+ rhs: new SequencerState({
+ type: TYPE.MERGE,
+ originalHead: cnr0,
+ target: cnr1,
+ commits: ["1", "2", "3"],
+ currentCommit: 1,
+ message: "moo",
+ }),
+ 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,
+ },
+ "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];
+ it(caseName, function () {
+ const result = c.lhs.equal(c.rhs);
+ assert.equal(result, c.expected);
+ });
+ });
+ });
+ 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,
+ originalHead: new CommitAndRef("a", null),
+ 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, msg=meh)`);
+ });
+});
diff --git a/node/test/util/sequencer_state_util.js b/node/test/util/sequencer_state_util.js
new file mode 100644
index 000000000..8571f6c3d
--- /dev/null
+++ b/node/test/util/sequencer_state_util.js
@@ -0,0 +1,406 @@
+/*
+ * 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);
+ 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();
+ 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);
+ }));
+ 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 = {
+ "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"],
+ message: "foo",
+ }),
+ commitMap: {
+ "1": "2",
+ },
+ expected: new SequencerState({
+ type: SequencerState.TYPE.REBASE,
+ originalHead: new CommitAndRef("2", null),
+ target: new CommitAndRef("2", "foo"),
+ currentCommit: 0,
+ commits: ["2"],
+ message: "foo",
+ }),
+ },
+ "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);
+ });
+ });
+});
+});
diff --git a/node/test/util/shorthand_parser_util.js b/node/test/util/shorthand_parser_util.js
index 66d1d70f6..d3c75990e 100644
--- a/node/test/util/shorthand_parser_util.js
+++ b/node/test/util/shorthand_parser_util.js
@@ -36,7 +36,31 @@ 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;
+ 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: {
@@ -110,6 +134,7 @@ describe("ShorthandParserUtil", function () {
});
describe("parseRepoShorthandRaw", function () {
const Commit = RepoAST.Commit;
+ const Conflict = RepoAST.Conflict;
const Submodule = RepoAST.Submodule;
function m(args) {
let result = {
@@ -135,6 +160,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), },
@@ -153,7 +179,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"1": new Commit({
parents: ["2"],
- changes: { "1": "1"},
+ changes: { "1": new File("1", false)},
message: "message\n",
}),
}
@@ -173,7 +199,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"1": new Commit({
parents: [],
- changes: { y: "2" },
+ changes: { y: new File("2", false) },
message: "message\n",
}),
},
@@ -183,7 +209,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"1": new Commit({
parents: ["2"],
- changes: { "1": "1"},
+ changes: { "1": new File("1", false) },
message: "hello world",
}),
}
@@ -192,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",
}),
},
@@ -201,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",
}),
},
@@ -210,7 +239,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"xxx2": new Commit({
parents: ["yy"],
- changes: { "xxx2": "xxx2"},
+ changes: { "xxx2": new File("xxx2", false) },
message: "message\n",
}),
}
@@ -219,7 +248,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"1": new Commit({
parents: ["2"],
- changes: { "foo": "bar"},
+ changes: { "foo": new File("bar", false) },
message: "message\n",
}),
}
@@ -228,7 +257,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"1": new Commit({
parents: ["2"],
- changes: { "foo": ""},
+ changes: { "foo": new File("", false) },
message: "message\n",
}),
}
@@ -237,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",
}),
}
@@ -246,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",
}),
}
@@ -262,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 },
@@ -275,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 },
@@ -297,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",
}),
},
@@ -401,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": {
@@ -425,25 +468,64 @@ 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(new File("x", false),
+ new File("y", false),
+ new Submodule("/x", "2")),
+ b: new File("q", false),
+ }
+ }),
+ },
+ "index with conflict and nulls": {
+ i: "S:I *a=*~*,b=q",
+ e: m({
+ type: "S",
+ index: {
+ a: new Conflict(new File("", false),
+ null,
+ new File("", false)),
+ b: new File("q", false),
+ }
+ }),
+ },
"workdir change": {
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": {
@@ -482,7 +564,7 @@ describe("ShorthandParserUtil", function () {
branches: {
master: new RepoAST.Branch("foo", null),
},
- workdir: { x: "z" },
+ workdir: { x: new File("z", false) },
}),
},
}),
@@ -524,7 +606,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"2": new Commit({
parents: ["1"],
- changes: { "2": "2"},
+ changes: { "2": new File("2", false) },
message: "message\n",
}),
},
@@ -542,7 +624,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"2": new Commit({
parents: ["1"],
- changes: { "2": "2"},
+ changes: { "2": new File("2", false) },
message: "message\n",
}),
},
@@ -599,6 +681,66 @@ describe("ShorthandParserUtil", function () {
rebase: 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"],
+ }),
+ }),
+ },
+ "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({
@@ -631,10 +773,13 @@ 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);
assert.deepEqual(r.rebase, e.rebase);
+ assert.deepEqual(r.sequencerState, e.sequencerState);
+ assert.equal(r.sparse, e.sparse);
});
});
});
@@ -653,6 +798,12 @@ describe("ShorthandParserUtil", function () {
i: "S",
e: S
},
+ "sparse": {
+ i: "%S",
+ e: S.copy({
+ sparse: true,
+ }),
+ },
"null": {
i: "N",
e: new RepoAST(),
@@ -663,7 +814,7 @@ describe("ShorthandParserUtil", function () {
commits: {
"1": new Commit({
changes: {
- "1": "1",
+ "1": new File("1", false),
},
message: "message\n",
}),
@@ -689,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"],
}),
@@ -706,7 +857,7 @@ describe("ShorthandParserUtil", function () {
commits: {
xyz: new Commit({
changes: {
- xyz: "xyz",
+ xyz: new File("xyz", false),
},
message: "changed xyz",
}),
@@ -735,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;
@@ -755,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;
@@ -784,7 +935,7 @@ describe("ShorthandParserUtil", function () {
i: "S:I a=b",
e: S.copy({
index: {
- a: "b",
+ a: new File("b", false),
}
}),
},
@@ -806,6 +957,18 @@ describe("ShorthandParserUtil", function () {
rebase: new RepoAST.Rebase("foo", "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 () {
@@ -841,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"],
}),
@@ -1006,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",
}),
},
@@ -1036,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") },
})
@@ -1058,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",
}),
@@ -1086,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",
}),
@@ -1119,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",
}),
},
@@ -1167,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",
}),
@@ -1194,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",
}),
},
@@ -1231,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",
}),
},
@@ -1256,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/sparse_checkout_util.js b/node/test/util/sparse_checkout_util.js
new file mode 100644
index 000000000..0bad2aa02
--- /dev/null
+++ b/node/test/util/sparse_checkout_util.js
@@ -0,0 +1,112 @@
+/*
+ * 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 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", 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, true);
+ }));
+});
+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);
+ const content = SparseCheckoutUtil.readSparseCheckout(repo);
+ assert.equal(content, ".gitmodules\n");
+ }));
+});
+describe("readSparseCheckout", function () {
+ it("doesn't exist", co.wrap(function *() {
+ const repo = yield TestUtil.createSimpleRepository();
+ const result = SparseCheckoutUtil.readSparseCheckout(repo);
+ assert.equal(result, "");
+ }));
+ it("exists", co.wrap(function *() {
+ const repo = yield TestUtil.createSimpleRepository();
+ yield SparseCheckoutUtil.setSparseMode(repo);
+ const result = SparseCheckoutUtil.readSparseCheckout(repo);
+ assert.equal(result, ".gitmodules\n");
+ }));
+});
+describe("addToSparseCheckoutFile", function () {
+ it("breathing", co.wrap(function *() {
+ const repo = yield TestUtil.createSimpleRepository();
+ yield SparseCheckoutUtil.setSparseMode(repo);
+ 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/stash_util.js b/node/test/util/stash_util.js
index 8e61b8fb9..5f07b8d2b 100644
--- a/node/test/util/stash_util.js
+++ b/node/test/util/stash_util.js
@@ -42,13 +42,13 @@ 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];
log.append(NodeGit.Oid.fromString(sha), sig, `log of ${logSha}`);
}
- log.write();
+ yield log.write();
});
/**
@@ -153,12 +153,14 @@ 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,
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);
@@ -204,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`,
@@ -212,6 +220,12 @@ 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=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",
expected: `x=E:Cstash#s-2 ;Fmeta-stash=s`,
@@ -277,15 +291,20 @@ 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 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,
@@ -297,21 +316,36 @@ 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}`;
+ }
+ }
+ }
+
+ // 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 {
@@ -457,6 +491,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!
@@ -468,29 +503,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 () {
@@ -558,6 +598,7 @@ x=E:Os Bss=ss!
const cases = {
"nothing to pop": {
init: "x=S",
+ fails: true,
},
"failed": {
init: `
@@ -618,35 +659,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, true);
});
- }));
+ it(caseName, co.wrap(function *() {
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.init,
+ c.expected,
+ popper,
+ c.fails, {
+ expectedTransformer: refMapper,
+ });
+ }));
+ });
});
});
@@ -684,4 +730,164 @@ 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, 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",
+ message: "foo",
+ includeMeta: true,
+ incrementTimestamp: 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, 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,
+ 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,
+ 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, 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
+ // 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 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 = {};
+ if (null !== result) {
+ const metaSha = result.metaCommit;
+ const commit = yield repo.getCommit(metaSha);
+ 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);
+ for (let path in result.subCommits) {
+ const subSha = result.subCommits[path];
+ const subRepo = yield SubmoduleUtil.getRepo(repo,
+ path);
+ const subCommit = yield subRepo.getCommit(subSha);
+ if (!(subSha in mapping.commitMap)) {
+ 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);
+ }));
+ });
+ });
});
diff --git a/node/test/util/status_util.js b/node/test/util/status_util.js
index e3c7fef73..50a926fba 100644
--- a/node/test/util/status_util.js
+++ b/node/test/util/status_util.js
@@ -32,25 +32,34 @@
const assert = require("chai").assert;
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");
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");
+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;
const RELATION = RepoStatus.Submodule.COMMIT_RELATION;
const Submodule = RepoStatus.Submodule;
const Commit = Submodule.Commit;
const Index = Submodule.Index;
const Workdir = Submodule.Workdir;
-
+
describe("remapSubmodule", function () {
const cases = {
"all": {
@@ -104,6 +113,13 @@ describe("StatusUtil", function () {
staged: { x: RepoStatus.FILESTATUS.ADDED },
workdir: { y: RepoStatus.FILESTATUS.ADDED },
rebase: new Rebase("foo", "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: {},
@@ -113,6 +129,13 @@ describe("StatusUtil", function () {
staged: { x: RepoStatus.FILESTATUS.ADDED },
workdir: { y: RepoStatus.FILESTATUS.ADDED },
rebase: new Rebase("foo", "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": {
@@ -417,7 +440,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 *() {
@@ -466,6 +488,71 @@ 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("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 () {
@@ -482,13 +569,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(),
@@ -500,8 +580,25 @@ x=S:C2-1 s=Sa:a;I s=Sa:b;Bmaster=2;Os H=1`,
rebase: new Rebase("master", "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: {
+ showMetaChanges: true,
+ },
expected: new RepoStatus({
currentBranchName: "master",
headCommit: "1",
@@ -518,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, },
+ options: {
+ untrackedFilesOption: DiffUtil.UNTRACKED_FILES_OPTIONS.ALL,
+ showMetaChanges: true
+ },
},
"ignore meta": {
state: "x=S:I README.md=whoohoo",
@@ -527,16 +627,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,13 +644,26 @@ 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:
//
// - 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": {
@@ -590,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",
@@ -602,6 +717,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": {
@@ -614,6 +732,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": {
@@ -632,6 +753,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": {
@@ -643,9 +767,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 +787,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 +850,50 @@ 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",
+ staged: {
+ ".gitmodules": FILESTATUS.ADDED,
+ },
+ submodules: {
+ s: new Submodule({
+ commit: null,
+ index: new Index(null, "a", null),
+ workdir: new Workdir(new RepoStatus({
+ headCommit: null,
+ staged: {
+ q: FILESTATUS.ADDED,
+ },
+ }), null),
+ }),
+ },
+ }),
+ },
+ "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];
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,
@@ -738,4 +902,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,
+ },
+ "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 () {
+ 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/stitch_util.js b/node/test/util/stitch_util.js
new file mode 100644
index 000000000..263c41724
--- /dev/null
+++ b/node/test/util/stitch_util.js
@@ -0,0 +1,1526 @@
+/*
+ * 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 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");
+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;
+
+/**
+ * Replace refs and notes with their equivalent logical mapping.
+ */
+function refMapper(actual, mapping) {
+ const fetchedSubRe = /(commits\/(?:[a-z/]*\/)?)(.*)/;
+ 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 ||
+ StitchUtil.changeCacheRef === 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;
+}
+
+/**
+ * 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("readAllowedToFailList", function () {
+ it("breathing", co.wrap(function *() {
+ const written = yield RepoASTTestUtil.createRepo(`
+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.readAllowedToFailList(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");
+ 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("makeReferenceNoteContent", function () {
+ it("breathing", co.wrap(function *() {
+ 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;
+ const result =
+ JSON.parse(StitchUtil.makeReferenceNoteContent("1", subs));
+ const expected = {
+ metaRepoCommit: "1",
+ 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("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);
+ });
+ });
+
+});
+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("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", null)
+ };
+ changes[nextSha] = {
+ "baz/bam": new SubmoduleChange("3", "4", null)
+ };
+ 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("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", null)
+ };
+ changes[nextSha] = {
+ "baz/bam": new SubmoduleChange("3", "4", null)
+ };
+ 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": {
+ input: "x=S",
+ commit: "1",
+ parents: [],
+ keepAsSubmodule: () => false,
+ subCommits: {},
+ expected: `
+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,
+ subCommits: {},
+ expected: `x=E:Cthe first commit#s-2 ;Bstitched=s`,
+ },
+ "new stitched sub": {
+ input: `
+x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`,
+ commit: "2",
+ parents: [],
+ keepAsSubmodule: () => false,
+ subCommits: { s: "a" },
+ expected: `x=E:C*#s s/a=a;Bstitched=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,
+ subCommits: { s: "a" },
+ expected: `x=E:C*#s-1 s/a=a;Bstitched=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,
+ subCommits: { s: "a", t: "b" },
+ expected: `
+x=E:C*#s s/a=a,t/b=b;Bstitched=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,
+ 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: `
+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,
+ subCommits: {},
+ expected: `x=E:Cs-c s/a;Bstitched=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,
+ subCommits: { s: "a" },
+ expected: `x=E:C*#s s/a=a,t=S.:b;Bstitched=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,
+ subCommits: {},
+ expected: `x=E:Cs-p s=S.:b;Bstitched=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,
+ subCommits: {},
+ expected: `x=E:Cs-p s;Bstitched=s`,
+ },
+ "empty commit, but not skipped": {
+ input: `
+x=B:Ca;Cfoo#2 ;Ba=a;Bmaster=2;Bfoo=1`,
+ commit: "2",
+ parents: ["1"],
+ keepAsSubmodule: () => false,
+ subCommits: {},
+ expected: `x=E:C*#s-1 ;Bstitched=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,
+ subCommits: {},
+ },
+ "skipped empty, with parent": {
+ input: `
+x=B:Ca;C2-1 ;Ba=a;Bmaster=2;Bstitched=1`,
+ commit: "2",
+ parents: ["1"],
+ keepAsSubmodule: () => false,
+ skipEmpty: true,
+ subCommits: {},
+ },
+ "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",
+ 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, 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,
+ allowed_to_fail: ["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 => {
+ 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 changes = yield getCommitChanges(x, commit);
+ 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(
+ x,
+ commit,
+ changes,
+ parents,
+ c.keepAsSubmodule,
+ adjustPath,
+ skipEmpty,
+ allowed_to_fail);
+ 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(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.stitchedCommit,
+ 1,
+ "stitched");
+ }
+ const stitchSha = stitch.stitchedCommit.id().tostrS();
+ const commitMap = {};
+ if (!(stitchSha in maps.commitMap)) {
+ commitMap[stitch.stitchedCommit.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 changes = yield getCommitChanges(repo, head);
+ const allowed_to_fail = new Set();
+ const stitch = yield StitchUtil.writeStitchedCommit(repo,
+ head,
+ changes,
+ [],
+ () => false,
+ (x) => x,
+ false,
+ allowed_to_fail);
+ 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, null),
+ };
+ expected[firstSha] = {};
+ const result =
+ yield StitchUtil.listSubmoduleChanges(repo, [head, parent]);
+ assert.deepEqual(result, expected);
+ }));
+});
+describe("listFetches", function () {
+ const cases = {
+ "trivial": {
+ state: "S",
+ toFetch: [],
+ keepAsSubmodule: () => false,
+ expected: {},
+ },
+ "a sub, not picked": {
+ state: "S:C2-1 s=S/a:1;Bmaster=2",
+ toFetch: ["1"],
+ keepAsSubmodule: () => false,
+ expected: {},
+ },
+ "added sub": {
+ state: "S:C2-1 s=S/a:1;Bmaster=2",
+ toFetch: ["2"],
+ keepAsSubmodule: () => false,
+ 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,
+ expected: {},
+ },
+ "adjusted to null": {
+ state: "S:C2-1 s=S/a:1;Bmaster=2",
+ toFetch: ["2"],
+ keepAsSubmodule: () => false,
+ adjustPath: () => null,
+ 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,
+ 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,
+ 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,
+ 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 changes = yield StitchUtil.listSubmoduleChanges(repo,
+ toFetch);
+ const result = yield StitchUtil.listFetches(repo,
+ toFetch,
+ changes,
+ c.keepAsSubmodule,
+ adjustPath);
+ 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/x/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, "x", url, fetches);
+ });
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.input,
+ c.expected,
+ fetcher,
+ c.fails, {
+ includeRefsCommits: true,
+ 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("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 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[twoSha] = "1";
+ assert.deepEqual(expected, result);
+ }));
+});
+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.isUndefined(result);
+
+ // Now delete and make sure we're remembering the result.
+
+ NodeGit.Reference.remove(repo, StitchUtil.convertedNoteRef);
+ const nextResult = yield fun(headSha);
+ assert.isUndefined(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("note present, stitched commit 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 notes = {};
+ notes[headSha] = StitchUtil.makeConvertedNoteContent(null);
+ yield BulkNotesUtil.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([headSha], 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 BulkNotesUtil.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": {
+ input: `
+x=B:Ca;Cfoo#2-1 s=S.:a;Ba=a;Bmaster=2`,
+ commit: "2",
+ targetBranchName: "my-branch",
+ keepAsSubmodule: () => false,
+ numParallel: 8,
+ preloadCache: true,
+ 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,
+ preloadCache: c.preloadCache,
+ };
+ 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/submodule_change.js b/node/test/util/submodule_change.js
new file mode 100644
index 000000000..bf4c9c8b3
--- /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", null);
+ assert.equal(change.oldSha, "old");
+ assert.equal(change.newSha, "new");
+ });
+});
diff --git a/node/test/util/submodule_config_util.js b/node/test/util/submodule_config_util.js
index e71d6a48e..5b4c99d26 100644
--- a/node/test/util/submodule_config_util.js
+++ b/node/test/util/submodule_config_util.js
@@ -37,13 +37,145 @@ 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);
+ 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,
+ 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 for sub is gone
+
+ let failed = false;
+ try {
+ 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;
+ }
+ assert(failed);
+ }));
+ });
+
describe("computeRelativeGitDir", function () {
const cases = {
"simple": {
@@ -406,6 +538,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();
@@ -429,8 +591,7 @@ 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, {
@@ -440,6 +601,22 @@ describe("SubmoduleConfigUtil", function () {
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"
+ });
+ const expected =`\
+foo
+[submodule "xxx"]
+\turl = yyy
`;
assert.equal(data, expected);
}));
@@ -462,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());
@@ -483,19 +652,32 @@ 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,
"my message");
- yield DeinitUtil.deinit(repo, subName);
+ 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);
+ 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)));
@@ -531,18 +713,45 @@ 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,
"foo",
url,
- null);
+ null,
+ false);
const remote = yield newSub.getRemote("origin");
const newUrl = remote.url();
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();
@@ -594,12 +803,12 @@ 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,
"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.
@@ -612,7 +821,8 @@ foo
repo,
"foo",
url,
- templateDir);
+ templateDir,
+ false);
const copiedPath = path.join(repo.path(),
"modules",
@@ -623,136 +833,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": {},
@@ -768,5 +848,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_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];
diff --git a/node/test/util/submodule_rebase_util.js b/node/test/util/submodule_rebase_util.js
new file mode 100644
index 000000000..178a306ab
--- /dev/null
+++ b/node/test/util/submodule_rebase_util.js
@@ -0,0 +1,401 @@
+/*
+ * 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 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");
+
+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,
+ },
+ "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",
+ conflictedCommit: "r",
+ expectedTransformer: function (expected, mapping) {
+ const content = `\
+<<<<<<< ${mapping.reverseCommitMap["2"]}
+2
+=======
+3
+>>>>>>> message
+`;
+ expected.x = expected.x.copy({
+ workdir: {
+ "2": new RepoAST.File(content, false),
+ },
+ });
+ 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 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,
+ },
+ "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",
+ 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=\
+<<<<<<< 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,
+ },
+ "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 *() {
+ 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: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: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!Bq=q`,
+ expected: `
+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": {
+ 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: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: `
+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;
+`,
+ baseCommit: "3",
+ expected: `
+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;
+`,
+ },
+ "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: `\
+Submodule ${colors.red("s")} is conflicted.
+`,
+ },
+ "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:Cfoo#M-2 s=Sa:Ns;Bmaster=M;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;
+ });
+ if (null !== result.metaCommit) {
+ commitMap[result.metaCommit] = "M";
+ }
+ return {
+ commitMap: commitMap,
+ };
+ });
+ yield RepoASTTestUtil.testMultiRepoManipulator(c.initial,
+ c.expected,
+ continuer,
+ c.fails);
+ }));
+ });
+});
+});
diff --git a/node/test/util/submodule_util.js b/node/test/util/submodule_util.js
index 6db494d21..930a1b23f 100644
--- a/node/test/util/submodule_util.js
+++ b/node/test/util/submodule_util.js
@@ -32,11 +32,13 @@
const assert = require("chai").assert;
const co = require("co");
+const fs = require("fs-promise");
const NodeGit = require("nodegit");
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");
@@ -104,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.
@@ -192,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.
@@ -201,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");
}));
});
@@ -296,6 +298,40 @@ 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);
+ yield SubmoduleConfigUtil.deinit(x, subs);
+ }
+ 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`.
@@ -332,10 +368,21 @@ 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, []);
}));
+
+ 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 () {
@@ -358,118 +405,109 @@ describe("SubmoduleUtil", function () {
}));
});
- describe("getSubmoduleChanges", function () {
+ describe("getSubmoduleChangesFromDiff", function () {
const cases = {
"trivial": {
state: "S",
from: "1",
- added: {},
- changed: {},
- removed: {},
- modules: false,
+ 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",
- added: {},
- changed: {},
- removed: {},
- modules: false,
+ 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",
- added: {},
- changed: {},
- removed: {},
- modules: false,
+ 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",
- added: {},
- changed: {},
- removed: {},
- modules: false,
+ result: {},
+ allowMetaChanges: true,
},
"added one": {
state: "S:C2-1 x=Sa:1;H=2",
from: "2",
- added: {
- "x": "1",
+ result: {
+ "x": new SubmoduleChange(null, "1", null),
},
- changed: {},
- removed: {},
- modules: true,
+ allowMetaChanges: false,
},
"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", null),
+ x: new SubmoduleChange(null, "1", null),
},
- changed: {},
- removed: {},
- modules: true,
+ allowMetaChanges: 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", null),
},
- removed: {},
- modules: false,
+ allowMetaChanges: true,
},
"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: {},
+ 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",
from: "3",
- added: {
- c: "2",
+ result: {
+ a: new SubmoduleChange("1", "2", null),
+ c: new SubmoduleChange(null, "2", null),
},
- changed: {
- a: {
- "new": "2",
- old: "1",
- },
- },
- removed: {},
- modules: true,
+ 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",
- added: {},
- changed: {},
- removed: {
- "a": "1",
+ result: {
+ a: new SubmoduleChange("1", null, null),
},
- modules: true,
+ allowMetaChanges: false,
},
"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", null),
+ a: new SubmoduleChange("1", null, null),
},
- modules: true,
+ allowMetaChanges: true,
},
};
Object.keys(cases).forEach(caseName => {
@@ -480,33 +518,143 @@ 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 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
- const commitMap = written.oldCommitMap;
- const expAdded = Object.assign({}, c.added);
- for (let name in expAdded) {
- expAdded[name] = commitMap[expAdded[name]];
+ 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, null);
+ });
+ 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", null),
+ },
+ 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", null),
+ },
+ 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", null),
+ "w": new SubmoduleChange(null, "1", null),
+ },
+ 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 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]);
}
- const expChanged = Object.assign({}, c.changed);
- for (let name in expChanged) {
- expChanged[name] = {
- "new": commitMap[expChanged[name]["new"]],
- "old": commitMap[expChanged[name].old],
- };
+ try {
+ changes = yield SubmoduleUtil.getSubmoduleChanges(
+ repo,
+ commit,
+ baseCommit,
+ c.allowMetaChanges);
+ }
+ catch (e) {
+ exception = e;
}
- const expRemoved = Object.assign({}, c.removed);
- for (let name in expRemoved) {
- expRemoved[name] = commitMap[expRemoved[name]];
+ const shouldFail = c.fails || false;
+ if (undefined === exception) {
+ assert.equal(false, shouldFail);
}
+ else {
+ if (!shouldFail || !(exception instanceof UserError)) {
+ throw exception;
+ }
+ return; // RETURN
+ }
+
+ const commitMap = written.commitMap;
- 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");
+ // 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, null);
+ });
+ assert.deepEqual(changes, c.result);
}));
});
});
@@ -515,17 +663,49 @@ 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", null),
+ bar: new Submodule("a", "1", null),
+ },
+ 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", 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": {
state: "S:Cu 1=1;Bu=u",
commit: "u",
expected: {},
+ names: null,
},
};
Object.keys(cases).forEach(caseName => {
@@ -536,13 +716,16 @@ 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];
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);
}));
@@ -551,54 +734,81 @@ 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"],
+ },
+ "does not overmatch": {
+ dir: "q",
+ 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, co.wrap(function *() {
- const written =
- yield RepoASTTestUtil.createMultiRepos(c.state);
- const x = written.repos.x;
- const result = yield SubmoduleUtil.getSubmodulesInPath(
- x.workdir(),
- c.dir,
- c.indexSubNames);
+ it(caseName, function() {
+ const result = SubmoduleUtil.getSubmodulesInPath(
+ c.dir,
+ c.indexSubNames,
+ c.includeSubdirs);
assert.deepEqual(result.sort(), c.expected.sort());
- }));
+ });
});
});
describe("resolveSubmoduleNames", function () {
@@ -608,11 +818,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",
@@ -653,7 +862,9 @@ describe("SubmoduleUtil", function () {
x.workdir(),
cwd,
subs,
- paths);
+ paths,
+ false,
+ true);
}
catch (e) {
if (!(e instanceof UserError)) {
@@ -673,43 +884,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: {
@@ -718,7 +929,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: {
@@ -727,13 +938,42 @@ describe("SubmoduleUtil", function () {
},
},
"path not in sub": {
- state: "a=B|x=U:Os",
+ subNames: ["s"],
paths: ["README.md"],
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": {
- state: "a=B|x=U:I sam=3;Os",
+ subNames: ["s"],
paths: ["sam"],
open: ["s"],
expected: {},
@@ -741,20 +981,25 @@ 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);
- assert.deepEqual(result, c.expected);
- }));
+ it(caseName, function () {
+ 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);
+ }
+ });
});
});
- describe("addRefs", function () {
+ describe("syncRefs", function () {
const cases = {
"trivial": {
input: "x=S",
@@ -787,41 +1032,15 @@ 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);
}));
});
});
- 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");
- }));
- });
});
diff --git a/node/test/util/synthetic-branch.js b/node/test/util/synthetic-branch.js
index c22ccda28..032741baa 100644
--- a/node/test/util/synthetic-branch.js
+++ b/node/test/util/synthetic-branch.js
@@ -41,10 +41,13 @@ 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 notesRepo = repos.n || repos.x;
const config = yield x.config();
yield config.setString("gitmeta.subreporootpath", "../../");
+ yield config.setString("gitmeta.skipsyntheticrefpattern",
+ "skip");
const head = yield x.getHeadCommit();
@@ -59,24 +62,19 @@ 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, {});
+ const pass = yield SyntheticBranch.checkUpdate(x, notesRepo, old,
+ headId, {});
if (!pass) {
throw new UserError("fail");
}
- return {
- commitMap: maps,
- };
});
const cases = {
"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;" +
@@ -100,67 +98,93 @@ 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 2=ok;" +
"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
},
+ // 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/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/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"
+ },
"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 2=ok;" +
"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 2=ok;" +
"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 2=ok;" +
"N refs/notes/git-meta/subrepo-check 3=ok|" +
- "z=S:C4-1;Bmaster=4;BmetaTEST=4",
+ "z=S:C4-1;Bmaster=4",
},
};
@@ -171,6 +195,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);
}));
});
});
@@ -198,11 +244,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(),
@@ -210,7 +256,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",
@@ -219,6 +265,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();
@@ -252,7 +338,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()
@@ -264,7 +350,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()
@@ -273,7 +359,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()
diff --git a/node/test/util/text_util.js b/node/test/util/text_util.js
new file mode 100644
index 000000000..f9b50d9e7
--- /dev/null
+++ b/node/test/util/text_util.js
@@ -0,0 +1,114 @@
+/*
+ * 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));
+ });
+ });
+
+ 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);
+ }
+ });
+ });
+});
diff --git a/node/test/util/tree_util.js b/node/test/util/tree_util.js
index 88ae230f6..d5efd0995 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;
@@ -84,6 +85,46 @@ 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",
+ },
+ }
+ },
+ "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" },
+ },
+ },
};
Object.keys(cases).forEach((caseName) => {
const c = cases[caseName];
@@ -235,6 +276,91 @@ 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());
+ }));
+ 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");
+ 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 *() {
@@ -243,7 +369,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);
@@ -261,7 +387,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 *() {
@@ -273,7 +400,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);
@@ -297,7 +425,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 *() {
@@ -314,12 +443,91 @@ 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),
});
}));
+ 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 = yield 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 = yield 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 = yield 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 = yield 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 = yield 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({
@@ -339,7 +547,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 *() {
@@ -351,7 +560,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);
@@ -359,5 +569,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);
+ }));
});
});
diff --git a/node/test/util/write_repo_ast_util.js b/node/test/util/write_repo_ast_util.js
index 8203587e8..7a97cf80c 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");
@@ -179,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];
@@ -290,6 +296,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",
@@ -342,10 +349,18 @@ 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 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(),
},
+ "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 => {
@@ -365,6 +380,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",
@@ -395,6 +411,9 @@ 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",
+ "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];
@@ -447,5 +466,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);
+ }));
});
});
diff --git a/node/yarn.lock b/node/yarn.lock
new file mode 100644
index 000000000..27d1553ac
--- /dev/null
+++ b/node/yarn.lock
@@ -0,0 +1,2371 @@
+# 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@^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==
+ 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-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"
+ 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.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"
+
+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=
+
+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"
+ 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.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"
+ 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, 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==
+ 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.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"
+ 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"
+
+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"
+ 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"
+
+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"
+ 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.7"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
+ integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
+ dependencies:
+ minipass "^2.6.0"
+
+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=
+
+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-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"
+ 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"
+
+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"
+ 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.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"
+ 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-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"
+ 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-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"
+ 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.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"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+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-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"
+ 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-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"
+ integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+ 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"
+ integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+ 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"
+ 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.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.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"
+ 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, 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.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==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.2.1:
+ 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.9.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"
+
+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"
+ 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.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"
+ 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@^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:
+ 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 "^4.4.8"
+ which "1"
+
+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"
+ 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.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.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"
+ 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-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==
+
+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.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"
+ integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+ dependencies:
+ 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"
+ 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.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"
+ 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.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"
+ 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"
+
+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"
+ 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:
+ 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==
+
+"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.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"
+ 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@^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"
+ 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"
+ 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"
+
+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"
+ 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.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"
+ 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=
+
+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"
+ 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"
+
+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"
+ 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.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"
+ 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"
]