Merge branch 'stable-3.6' * stable-3.6: RenameEmail: Preserve files which are not updated Change-Id: I257bfaafc0623f2d0319fbd39d0520aa84a2a0b4
diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 2c0d24a..0000000 --- a/.eslintrc.json +++ /dev/null
@@ -1,240 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "google" - ], - "parserOptions": { - "ecmaVersion": 10, - "sourceType": "module" - }, - "env": { - "browser": true, - "es6": true - }, - "globals": { - "__dirname": false, - "app": false, - "page": false, - "Polymer": false, - "process": false, - "require": false, - "Gerrit": false, - "Promise": false, - "assert": false, - "test": false, - "flushAsynchronousOperations": false - }, - "rules": { - "arrow-parens": [ - "error", - "as-needed" - ], - "block-spacing": [ - "error", - "always" - ], - "brace-style": [ - "error", - "1tbs", - { - "allowSingleLine": true - } - ], - "camelcase": "off", - "comma-dangle": [ - "error", - { - "arrays": "always-multiline", - "objects": "always-multiline", - "imports": "always-multiline", - "exports": "always-multiline", - "functions": "never" - } - ], - "eol-last": "off", - "indent": [ - "error", - 2, - { - "MemberExpression": 2, - "FunctionDeclaration": { - "body": 1, - "parameters": 2 - }, - "FunctionExpression": { - "body": 1, - "parameters": 2 - }, - "CallExpression": { - "arguments": 2 - }, - "ArrayExpression": 1, - "ObjectExpression": 1, - "SwitchCase": 1 - } - ], - "keyword-spacing": [ - "error", - { - "after": true, - "before": true - } - ], - "lines-between-class-members": [ - "error", - "always" - ], - "max-len": [ - "error", - 80, - 2, - { - "ignoreComments": true, - "ignorePattern": "^import .*;$", - "ignoreTemplateLiterals": true - } - ], - "new-cap": [ - "error", - { - "capIsNewExceptions": [ - "Polymer", - "LegacyElementMixin", - "GestureEventListeners", - "LegacyDataMixin", - "CodeOwnersModelMixin" - ] - } - ], - "no-console": "off", - "no-multiple-empty-lines": [ - "error", - { - "max": 1 - } - ], - "no-prototype-builtins": "off", - "no-redeclare": "off", - "no-restricted-syntax": [ - "error", - { - "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']", - "message": "Remove test.only." - }, - { - "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']", - "message": "Remove suite.only." - } - ], - "no-undef": "off", - "no-useless-escape": "off", - "no-var": "error", - "object-shorthand": [ - "error", - "always" - ], - "padding-line-between-statements": [ - "error", - { - "blankLine": "always", - "prev": "class", - "next": "*" - }, - { - "blankLine": "always", - "prev": "*", - "next": "class" - } - ], - "prefer-arrow-callback": "error", - "prefer-const": "error", - "prefer-spread": "error", - "quote-props": [ - "error", - "consistent-as-needed" - ], - "require-jsdoc": "off", - "semi": [ - 2, - "always" - ], - "template-curly-spacing": "error", - "valid-jsdoc": "off", - "require-jsdoc": 0, - "valid-jsdoc": 0, - "jsdoc/check-alignment": 2, - "jsdoc/check-examples": 0, - "jsdoc/check-indentation": 0, - "jsdoc/check-param-names": 0, - "jsdoc/check-syntax": 0, - "jsdoc/check-tag-names": 0, - "jsdoc/check-types": 0, - "jsdoc/implements-on-classes": 2, - "jsdoc/match-description": 0, - "jsdoc/newline-after-description": 2, - "jsdoc/no-types": 0, - "jsdoc/no-undefined-types": 0, - "jsdoc/require-description": 0, - "jsdoc/require-description-complete-sentence": 0, - "jsdoc/require-example": 0, - "jsdoc/require-hyphen-before-param-description": 0, - "jsdoc/require-jsdoc": 0, - "jsdoc/require-param": 0, - "jsdoc/require-param-description": 0, - "jsdoc/require-param-name": 2, - "jsdoc/require-param-type": 2, - "jsdoc/require-returns": 0, - "jsdoc/require-returns-check": 0, - "jsdoc/require-returns-description": 0, - "jsdoc/require-returns-type": 2, - "jsdoc/valid-types": 2, - "jsdoc/require-file-overview": [ - "error", - { - "tags": { - "license": { - "mustExist": true, - "preventDuplicates": true - } - } - } - ], - "import/named": 2, - "import/no-unresolved": 2, - "import/no-self-import": 2, - // The no-cycle rule is slow, because it doesn't cache dependencies. - // Disable it. - "import/no-cycle": 0, - "import/no-useless-path-segments": 2, - "import/no-unused-modules": 2, - "import/no-default-export": 2 - }, - "plugins": [ - "html", - "jsdoc", - "import" - ], - "settings": { - "html/report-bad-indent": "error" - }, - "overrides": [ - { - "files": [ - "*_html.js", - "*-styles.js", - "externs.js" - ], - "rules": { - "max-len": "off" - } - }, - { - "files": [ - "*.html" - ], - "rules": { - "jsdoc/require-file-overview": "off" - } - } - ] -}
diff --git a/BUILD b/BUILD index 91a4e96..65d4548 100644 --- a/BUILD +++ b/BUILD
@@ -19,7 +19,7 @@ "Gerrit-HttpModule: com.google.gerrit.plugins.codeowners.module.HttpModule", "Gerrit-BatchModule: com.google.gerrit.plugins.codeowners.module.BatchModule", ], - resource_jars = ["//plugins/code-owners/ui:code-owners"], + resource_jars = ["//plugins/code-owners/web:code-owners"], resource_strip_prefix = "plugins/code-owners/resources", resources = glob(["resources/**/*"]), deps = [
diff --git a/README.md b/README.md index 3767276..ef90515 100644 --- a/README.md +++ b/README.md
@@ -20,13 +20,22 @@ ## JavaScript Plugin -For testing the plugin with -[Gerrit FE Dev Helper](https://gerrit.googlesource.com/gerrit-fe-dev-helper/) -build the JavaScript bundle and copy it to the `plugins/` folder: +From the root of the gerrit repository. - bazel build //plugins/code-owners/ui:code-owners - cp -f bazel-bin/plugins/code-owners/ui/code-owners.js plugins/ +``` +bazel test //plugins/code-owners/web:karma_test +``` + +For testing the plugin with the +[Gerrit FE Dev Helper](https://gerrit.googlesource.com/gerrit-fe-dev-helper/) +the command below builds + +``` + bazel build //plugins/code-owners/web:code-owners + ln -s bazel-bin/plugins/code-owners/web/code-owners.js polygerrit-ui/app/plugins/ +``` + and let the Dev Helper redirect from `.+/plugins/code-owners/static/code-owners.js` to -`http://localhost:8081/plugins_/code-owners.js`. +`http://localhost:8081/plugins/code-owners.js`.
diff --git a/bower.json b/bower.json deleted file mode 100644 index daa1644..0000000 --- a/bower.json +++ /dev/null
@@ -1,14 +0,0 @@ -{ - "name": "code-owners", - "license": "http://www.apache.org/licenses/LICENSE-2.0", - "devDependencies": { - "es6-promise": "^3.3.1", - "chai": "^4.2.0", - "mocha": "^6.2.2", - "web-component-tester": "^6.5.1" - }, - "private": true, - "resolutions": { - "chai": "^4.0.0" - } -} \ No newline at end of file
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java index 8ea5a5b..f7a2943 100644 --- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java +++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersIT.java
@@ -25,7 +25,6 @@ import com.google.gerrit.plugins.codeowners.api.impl.ProjectCodeOwnersFactory; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId; import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig; -import com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig; import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend; import com.google.gerrit.testing.ConfigSuite; import java.util.Arrays; @@ -45,26 +44,6 @@ */ public class AbstractCodeOwnersIT extends AbstractCodeOwnersTest { /** - * Returns a {@code gerrit.config} without code owner backend configuration to test the default - * setup. - */ - @ConfigSuite.Default - public static Config defaultConfig() { - Config cfg = new Config(); - - // Disable asynchronous posting of change messages during tests to avoid parallel updates to - // NoteDb and hence risking LOCK_FAILURES (especially needed since the test API does not retry - // on LOCK_FAILURES). - cfg.setBoolean( - "plugin", - "code-owners", - GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER, - /* value= */ false); - - return cfg; - } - - /** * Returns a {@code gerrit.config} for every code owner backend so that all code owner backends * are tested. */
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java index 32c82a1..1283932 100644 --- a/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java +++ b/java/com/google/gerrit/plugins/codeowners/acceptance/AbstractCodeOwnersTest.java
@@ -38,12 +38,17 @@ import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation.Builder; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference; import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; +import com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig; import com.google.gerrit.plugins.codeowners.backend.config.StatusConfig; import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend; import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend; import com.google.gerrit.plugins.codeowners.util.JgitPath; +import com.google.gerrit.testing.ConfigSuite; import com.google.inject.Inject; import java.nio.file.Path; import java.util.Map; @@ -69,6 +74,31 @@ name = "code-owners", sysModule = "com.google.gerrit.plugins.codeowners.acceptance.TestModule") public class AbstractCodeOwnersTest extends LightweightPluginDaemonTest { + /** + * Returns a {@code gerrit.config} without code owner backend configuration to test the default + * setup. + */ + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + + // Disable asynchronous posting of change messages during tests to avoid parallel updates to + // NoteDb and hence risking LOCK_FAILURES (especially needed since the test API does not retry + // on LOCK_FAILURES). + cfg.setBoolean( + "plugin", + "code-owners", + GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER, + /* value= */ false); + cfg.setBoolean( + "plugin", + "code-owners", + GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + /* value= */ false); + + return cfg; + } + @Inject private ProjectOperations projectOperations; private CodeOwnerConfigOperations codeOwnerConfigOperations; @@ -341,4 +371,13 @@ } throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName()); } + + protected CodeOwnerConfigReference createCodeOwnerConfigReference( + CodeOwnerConfigImportMode importMode, CodeOwnerConfig.Key codeOwnerConfigKey) { + return CodeOwnerConfigReference.builder( + importMode, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()) + .setProject(codeOwnerConfigKey.project()) + .setBranch(codeOwnerConfigKey.branchNameKey().branch()) + .build(); + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BUILD b/java/com/google/gerrit/plugins/codeowners/api/BUILD index 9ab1591..f8d6d60 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/BUILD +++ b/java/com/google/gerrit/plugins/codeowners/api/BUILD
@@ -6,6 +6,7 @@ srcs = glob(["*.java"]), visibility = ["//visibility:public"], deps = PLUGIN_DEPS_NEVERLINK + [ + "//lib/errorprone:annotations", "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/backend", "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common", "//plugins/code-owners/proto:owners_metadata_java_proto",
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java new file mode 100644 index 0000000..41fa62b --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerConfigFileInfo.java
@@ -0,0 +1,65 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.api; + +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; +import java.util.List; + +/** + * Representation of a code owner config file in the REST API. + * + * <p>This class determines the JSON format of code owner config files in the REST API. + * + * <p>The JSON only contains the location of the code owner config file (project, branch, path) and + * information about the imported code owner config files. It's not a representation of the code + * owner config file content (the content of a code owner config file is represented by {@link + * CodeOwnerConfigInfo}). + */ +public class CodeOwnerConfigFileInfo { + /** + * The name of the project from which the code owner config was loaded, or for unresolved imports, + * from which the code owner config was supposed to be loaded. + */ + public String project; + + /** + * The name of the branch from which the code owner config was loaded, or for unresolved imports, + * from which the code owner config was supposed to be loaded. + */ + public String branch; + + /** The path of the code owner config file. */ + public String path; + + /** Imported code owner config files. */ + public List<CodeOwnerConfigFileInfo> imports; + + /** Imported code owner config files that couldn't be resolved. */ + public List<CodeOwnerConfigFileInfo> unresolvedImports; + + /** + * Message explaining why this code owner config couldn't be resolved. + * + * <p>Only set for unresolved imports. + */ + public String unresolvedErrorMessage; + + /** + * The import mode. + * + * <p>Only set for imports. + */ + public CodeOwnerConfigImportMode importMode; +}
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java index a2079b8..390e34c 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java +++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwners.java
@@ -17,6 +17,7 @@ import static java.util.Objects.requireNonNull; import com.google.common.collect.ImmutableSet; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gerrit.extensions.client.ListAccountsOption; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -78,6 +79,7 @@ * * <p>Appends to the options which have been set so far. */ + @CanIgnoreReturnValue public QueryRequest withOptions( ListAccountsOption option, ListAccountsOption... furtherOptions) { this.options.add(requireNonNull(option, "option")); @@ -90,6 +92,7 @@ * * @param limit the limit */ + @CanIgnoreReturnValue public QueryRequest withLimit(int limit) { this.limit = limit; return this; @@ -100,6 +103,7 @@ * * @param seed seed that should be used to shuffle code owners that have the same score */ + @CanIgnoreReturnValue public QueryRequest withSeed(long seed) { this.seed = seed; return this; @@ -112,6 +116,7 @@ * @param resolveAllUsers whether code ownerships that are assigned to all users should be * resolved to random users */ + @CanIgnoreReturnValue public QueryRequest setResolveAllUsers(boolean resolveAllUsers) { this.resolveAllUsers = resolveAllUsers; return this; @@ -123,6 +128,7 @@ * @param highestScoreOnly whether only the code owners with the highest score should be * returned */ + @CanIgnoreReturnValue public QueryRequest withHighestScoreOnly(boolean highestScoreOnly) { this.highestScoreOnly = highestScoreOnly; return this; @@ -135,6 +141,7 @@ * * @param debug whether debug logs should be included into the response */ + @CanIgnoreReturnValue public QueryRequest withDebug(boolean debug) { this.debug = debug; return this; @@ -147,6 +154,7 @@ * * @param revision the revision from which the code owner configs should be read */ + @CanIgnoreReturnValue public QueryRequest forRevision(String revision) { this.revision = revision; return this;
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java index d699029..763dff2 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java +++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnersInfo.java
@@ -34,6 +34,9 @@ */ public Boolean ownedByAllUsers; + /** The code owner config files that have been inspected to gather the code owners. */ + public List<CodeOwnerConfigFileInfo> codeOwnerConfigs; + /** * Debug logs that may help to understand why a user is or isn't suggested as a code owner. *
diff --git a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java index bda0e27..ff5d971 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java +++ b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathInfo.java
@@ -14,15 +14,25 @@ package com.google.gerrit.plugins.codeowners.api; +import com.google.gerrit.extensions.common.AccountInfo; +import java.util.List; + /** JSON representation of a file path the may be owned by the user. */ public class OwnedPathInfo { /** The path of the file that may be owned by the user. */ public String path; /** - * Whether the user owns this path. + * Whether the user owns this path. This is computed for the given 'user' parameter. * * <p>Not set if {@code false}. */ public Boolean owned; + + /** + * The owners for the given file as Account IDs. + * + * <p>May not be set if the list is empty. + */ + public List<AccountInfo> owners; }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java index 98e9e90..7980ee6 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java +++ b/java/com/google/gerrit/plugins/codeowners/api/OwnedPathsInfo.java
@@ -35,18 +35,6 @@ public List<OwnedChangedFileInfo> ownedChangedFiles; /** - * The list of the owned new and old paths that are contained in {@link #ownedChangedFiles}. - * - * <p>The paths are returned as absolute paths. - * - * <p>The paths are sorted alphabetically. - * - * <p>May contain more entries than the limit that was specified on the request (if the users owns - * new and old path of renamed files). - */ - public List<String> ownedPaths; - - /** * Whether the request would deliver more results if not limited. * * <p>Not set if {@code false}.
diff --git a/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java index 7b2655b..d1c582b 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java +++ b/java/com/google/gerrit/plugins/codeowners/api/RevisionCodeOwners.java
@@ -94,11 +94,12 @@ public abstract Map<String, List<ConsistencyProblemInfo>> check() throws RestApiException; } - /** Request to check code owner config files. */ + /** Request to check owned_paths. */ abstract class OwnedPathsRequest { private Integer start; private Integer limit; private String user; + private boolean checkReviewers; /** * Sets a limit on the number of owned paths that should be returned. @@ -141,6 +142,17 @@ return user; } + /** Sets whether code ownership is checked for the reviewers of the change" */ + public OwnedPathsRequest withCheckReviewers(boolean checkReviewers) { + this.checkReviewers = checkReviewers; + return this; + } + + /** Returns whether checkReviewers is set */ + public boolean getCheckReviewers() { + return this.checkReviewers; + } + /** Executes the request and retrieves the paths that are owned by the user. */ public abstract OwnedPathsInfo get() throws RestApiException; }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java index db8cb27..d1e50f9 100644 --- a/java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java +++ b/java/com/google/gerrit/plugins/codeowners/api/impl/RevisionCodeOwnersImpl.java
@@ -77,6 +77,7 @@ GetOwnedPaths getOwnedPaths = getOwnedPathsProvider.get(); getStart().ifPresent(getOwnedPaths::setStart); getLimit().ifPresent(getOwnedPaths::setLimit); + getOwnedPaths.setCheckReviewers(getCheckReviewers()); getOwnedPaths.setUser(getUser()); return getOwnedPaths.apply(revisionResource).value(); } catch (Exception e) {
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java index a0df99d..0be6cce 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -15,6 +15,8 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; @@ -30,6 +32,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.context.RefUpdateContext; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; @@ -251,7 +254,10 @@ codeOwnerConfigKey) .setCodeOwnerConfigUpdate(codeOwnerConfigUpdate); - try (MetaDataUpdate metaDataUpdate = + try ( + RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN); + RefUpdateContext ctx = RefUpdateContext.open(VERSIONED_META_DATA_CHANGE); + MetaDataUpdate metaDataUpdate = createMetaDataUpdate(codeOwnerConfigKey.project(), repository, currentUser)) { codeOwnerConfigFile.commit(metaDataUpdate); }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BUILD b/java/com/google/gerrit/plugins/codeowners/backend/BUILD index 83671a0..04aaf14 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/BUILD +++ b/java/com/google/gerrit/plugins/codeowners/backend/BUILD
@@ -6,6 +6,7 @@ srcs = glob(["**/*.java"]), visibility = ["//visibility:public"], deps = PLUGIN_DEPS_NEVERLINK + [ + "//lib/errorprone:annotations", "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/common", "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/metrics", "//plugins/code-owners/java/com/google/gerrit/plugins/codeowners/util",
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java index 1309f8f..cba9409 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/BackendModule.java
@@ -36,6 +36,8 @@ public class BackendModule extends FactoryModule { @Override protected void configure() { + factory(ChangedFilesByPatchSetCache.Factory.class); + factory(CodeOwnerApprovalCheckInput.Loader.Factory.class); factory(CodeOwnersUpdate.Factory.class); factory(CodeOwnerConfigScanner.Factory.class); factory(CodeOwnersPluginGlobalConfigSnapshot.Factory.class);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java index 27412f2..1d98268 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFiles.java
@@ -93,8 +93,8 @@ if (mergeCommitStrategy.equals(MergeCommitStrategy.FILES_WITH_CONFLICT_RESOLUTION) || isInitialCommit(project, revision)) { // Use parentNum=0 to do the comparison against the default base. - // For non-merge commits the default base is the only parent (aka parent 1, initial commits - // are not supported). + // For non-merge commits the default base is the only parent (aka parent 1). + // Initial commits are supported when using parentNum=0. // For merge commits the default base is the auto-merge commit which should be used as base // if the merge commit strategy is FILES_WITH_CONFLICT_RESOLUTION. fileDiffOutputs =
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/ChangedFilesByPatchSetCache.java b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFilesByPatchSetCache.java new file mode 100644 index 0000000..3d5fc9c --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/backend/ChangedFilesByPatchSetCache.java
@@ -0,0 +1,100 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; +import com.google.gerrit.plugins.codeowners.common.ChangedFile; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.patch.DiffNotAvailableException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Computes and caches the {@link ChangedFile}s for the patch sets of a change. + * + * <p>The changed files for a patch set are computed lazily. This way we do not compute changed + * files unnecessarily that are never requested. + * + * <p>This class is not thread-safe. + */ +public class ChangedFilesByPatchSetCache { + interface Factory { + ChangedFilesByPatchSetCache create( + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, ChangeNotes changeNotes); + } + + private final ChangedFiles changedFiles; + private final CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig; + private final ChangeNotes changeNotes; + + private Map<PatchSet.Id, ImmutableList<ChangedFile>> cache = new HashMap<>(); + + @Inject + public ChangedFilesByPatchSetCache( + ChangedFiles changedFiles, + @Assisted CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + @Assisted ChangeNotes changeNotes) { + this.changedFiles = changedFiles; + this.codeOwnersConfig = codeOwnersConfig; + this.changeNotes = changeNotes; + } + + public ImmutableList<ChangedFile> get(PatchSet.Id patchSetId) { + return cache.computeIfAbsent(patchSetId, this::compute); + } + + private ImmutableList<ChangedFile> compute(PatchSet.Id patchSetId) { + checkState( + patchSetId.changeId().equals(changeNotes.getChange().getId()), + "patch set %s belongs to other change than change %s", + patchSetId, + changeNotes.getChange().getId().get()); + PatchSet patchSet = getPatchSet(patchSetId); + try { + return changedFiles.getFromDiffCache( + changeNotes.getProjectName(), + patchSet.commitId(), + codeOwnersConfig.getMergeCommitStrategy()); + } catch (IOException | DiffNotAvailableException e) { + throw new StorageException( + String.format( + "failed to retrieve changed files for patch set %d of change %d" + + " in project %s (commit=%s)", + patchSetId.get(), + patchSetId.changeId().get(), + changeNotes.getProjectName(), + patchSet.commitId()), + e); + } + } + + private PatchSet getPatchSet(PatchSet.Id patchSetId) { + PatchSet patchSet = changeNotes.getPatchSets().get(patchSetId); + if (patchSet == null) { + throw new StorageException( + String.format( + "patch set %s not found in change %d", patchSetId, changeNotes.getChangeId().get())); + } + return patchSet; + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java index a04c1ad..71c0044 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import com.google.common.flogger.FluentLogger; import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Account; @@ -34,20 +35,19 @@ import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.metrics.Timer0; +import com.google.gerrit.metrics.Timer1; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval; import com.google.gerrit.plugins.codeowners.common.ChangedFile; import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus; import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; -import com.google.gerrit.server.approval.ApprovalsUtil; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.PureRevertCache; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.patch.DiffNotAvailableException; import com.google.gerrit.server.util.AccountTemplateUtil; -import com.google.gerrit.server.util.LabelVote; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -88,8 +88,9 @@ private final PureRevertCache pureRevertCache; private final Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider; private final Provider<CodeOwnerResolver> codeOwnerResolverProvider; - private final ApprovalsUtil approvalsUtil; + private final CodeOwnerApprovalCheckInput.Loader.Factory inputLoaderFactory; private final CodeOwnerMetrics codeOwnerMetrics; + private final ChangedFilesByPatchSetCache.Factory changedFilesByPatchSetCacheFactory; @Inject CodeOwnerApprovalCheck( @@ -99,16 +100,18 @@ PureRevertCache pureRevertCache, Provider<CodeOwnerConfigHierarchy> codeOwnerConfigHierarchyProvider, Provider<CodeOwnerResolver> codeOwnerResolverProvider, - ApprovalsUtil approvalsUtil, - CodeOwnerMetrics codeOwnerMetrics) { + CodeOwnerApprovalCheckInput.Loader.Factory codeOwnerApprovalCheckInputLoaderFactory, + CodeOwnerMetrics codeOwnerMetrics, + ChangedFilesByPatchSetCache.Factory changedFilesByPatchSetCacheFactory) { this.repoManager = repoManager; this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.changedFiles = changedFiles; this.pureRevertCache = pureRevertCache; this.codeOwnerConfigHierarchyProvider = codeOwnerConfigHierarchyProvider; this.codeOwnerResolverProvider = codeOwnerResolverProvider; - this.approvalsUtil = approvalsUtil; + this.inputLoaderFactory = codeOwnerApprovalCheckInputLoaderFactory; this.codeOwnerMetrics = codeOwnerMetrics; + this.changedFilesByPatchSetCacheFactory = changedFilesByPatchSetCacheFactory; } /** @@ -119,10 +122,16 @@ * @param accountId account ID of the code owner for which the owned files should be returned * @param start number of owned paths to skip * @param limit the max number of owned paths that should be returned (0 = unlimited) + * @param checkReviewers whether to check if the reviewers are in the owners. * @return the paths of the files in the given patch set that are owned by the specified account */ public ImmutableList<OwnedChangedFile> getOwnedPaths( - ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId, int start, int limit) { + ChangeNotes changeNotes, + PatchSet patchSet, + Account.Id accountId, + int start, + int limit, + boolean checkReviewers) { try (Timer0.Context ctx = codeOwnerMetrics.computeOwnedPaths.start()) { logger.atFine().log( "compute owned paths for account %d (project = %s, change = %d, patch set = %d," @@ -133,17 +142,21 @@ patchSet.id().get(), start, limit); - + ImmutableSet.Builder<Account.Id> checkOwnerIds = ImmutableSet.builder(); + checkOwnerIds.add(accountId); + if (checkReviewers) { + checkOwnerIds.addAll(changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER)); + } Stream<FileCodeOwnerStatus> fileStatuses = - getFileStatusesForAccount(changeNotes, patchSet, accountId) + getFileStatusesForAccounts(changeNotes, patchSet, checkOwnerIds.build()) .filter( fileStatus -> (fileStatus.newPathStatus().isPresent() - && fileStatus.newPathStatus().get().status() - == CodeOwnerStatus.APPROVED) + && fileStatus.newPathStatus().get().owners().isPresent() + && !fileStatus.newPathStatus().get().owners().get().isEmpty()) || (fileStatus.oldPathStatus().isPresent() - && fileStatus.oldPathStatus().get().status() - == CodeOwnerStatus.APPROVED)); + && fileStatus.oldPathStatus().get().owners().isPresent() + && !fileStatus.oldPathStatus().get().owners().get().isEmpty())); if (start > 0) { fileStatuses = fileStatuses.skip(start); } @@ -161,7 +174,11 @@ newPathStatus -> OwnedPath.create( newPathStatus.path(), - newPathStatus.status() == CodeOwnerStatus.APPROVED)) + newPathStatus.owners().isPresent() + && newPathStatus.owners().get().contains(accountId), + newPathStatus.owners().isPresent() + ? newPathStatus.owners().get() + : ImmutableSet.of())) .orElse(null), fileStatus .oldPathStatus() @@ -169,7 +186,11 @@ oldPathStatus -> OwnedPath.create( oldPathStatus.path(), - oldPathStatus.status() == CodeOwnerStatus.APPROVED)) + oldPathStatus.owners().isPresent() + && oldPathStatus.owners().get().contains(accountId), + oldPathStatus.owners().isPresent() + ? oldPathStatus.owners().get() + : ImmutableSet.of())) .orElse(null))) .collect(toImmutableList()); } catch (IOException | DiffNotAvailableException e) { @@ -195,9 +216,12 @@ changeNotes.getChangeId().get(), changeNotes.getProjectName()); CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get(); CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get().enforceVisibility(false); + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig = + codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName()); try { boolean isSubmittable = - !getFileStatuses(codeOwnerConfigHierarchy, codeOwnerResolver, changeNotes) + !getFileStatuses( + codeOwnersConfig, codeOwnerConfigHierarchy, codeOwnerResolver, changeNotes) .anyMatch( fileStatus -> (fileStatus.newPathStatus().isPresent() @@ -230,17 +254,22 @@ * * @param start number of file statuses to skip * @param limit the max number of file statuses that should be returned (0 = unlimited) - * @see #getFileStatuses(CodeOwnerConfigHierarchy, CodeOwnerResolver, ChangeNotes) + * @see #getFileStatuses(CodeOwnersPluginProjectConfigSnapshot, CodeOwnerConfigHierarchy, + * CodeOwnerResolver, ChangeNotes) */ public ImmutableSet<FileCodeOwnerStatus> getFileStatusesAsSet( ChangeNotes changeNotes, int start, int limit) throws IOException, DiffNotAvailableException { requireNonNull(changeNotes, "changeNotes"); - try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatuses.start()) { + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig = + codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName()); + try (Timer1.Context<Boolean> ctx = + codeOwnerMetrics.computeFileStatuses.start(codeOwnersConfig.areStickyApprovalsEnabled())) { logger.atFine().log( "compute file statuses (project = %s, change = %d, start = %d, limit = %d)", changeNotes.getProjectName(), changeNotes.getChangeId().get(), start, limit); Stream<FileCodeOwnerStatus> fileStatuses = getFileStatuses( + codeOwnersConfig, codeOwnerConfigHierarchyProvider.get(), codeOwnerResolverProvider.get().enforceVisibility(false), changeNotes); @@ -283,6 +312,7 @@ * returned */ private Stream<FileCodeOwnerStatus> getFileStatuses( + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, CodeOwnerResolver codeOwnerResolver, ChangeNotes changeNotes) @@ -293,10 +323,6 @@ "prepare stream to compute file statuses (project = %s, change = %d)", changeNotes.getProjectName(), changeNotes.getChangeId().get()); - CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig = - codeOwnersPluginConfiguration.getProjectConfig(changeNotes.getProjectName()); - - Account.Id changeOwner = changeNotes.getChange().getOwner(); Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); ImmutableSet<Account.Id> exemptedAccounts = codeOwnersConfig.getExemptedAccounts(); logger.atFine().log("exemptedAccounts = %s", exemptedAccounts); @@ -323,37 +349,6 @@ "change is a pure revert and is exempted from requiring code owner approvals"); } - boolean implicitApprovalConfig = codeOwnersConfig.areImplicitApprovalsEnabled(); - boolean enableImplicitApproval = - implicitApprovalConfig && changeOwner.equals(patchSetUploader); - logger.atFine().log( - "changeOwner = %d, patchSetUploader = %d, implict approval config = %s\n=> implicit approval is %s", - changeOwner.get(), - patchSetUploader.get(), - implicitApprovalConfig, - enableImplicitApproval ? "enabled" : "disabled"); - - ImmutableList<PatchSetApproval> currentPatchSetApprovals = - getCurrentPatchSetApprovals(changeNotes); - - RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); - logger.atFine().log("requiredApproval = %s", requiredApproval); - - ImmutableSet<RequiredApproval> overrideApprovals = codeOwnersConfig.getOverrideApprovals(); - ImmutableSet<PatchSetApproval> overrides = - getOverride(currentPatchSetApprovals, overrideApprovals, patchSetUploader); - logger.atFine().log( - "hasOverride = %s (overrideApprovals = %s, overrides = %s)", - !overrides.isEmpty(), - overrideApprovals.stream() - .map( - overrideApproval -> - String.format( - "%s (ignoreSelfApproval = %s)", - overrideApproval, overrideApproval.labelType().isIgnoreSelfApproval())) - .collect(toImmutableList()), - overrides); - BranchNameKey branch = changeNotes.getChange().getDest(); Optional<ObjectId> revision = getDestBranchRevision(changeNotes.getChange()); if (revision.isPresent()) { @@ -363,17 +358,8 @@ logger.atFine().log("dest branch %s does not exist", branch.branch()); } - CodeOwnerResolverResult globalCodeOwners = - codeOwnerResolver.resolveGlobalCodeOwners(changeNotes.getProjectName()); - logger.atFine().log("global code owners = %s", globalCodeOwners); - - ImmutableSet<Account.Id> reviewerAccountIds = - getReviewerAccountIds(requiredApproval, changeNotes, patchSetUploader); - ImmutableSet<Account.Id> approverAccountIds = - getApproverAccountIds(currentPatchSetApprovals, requiredApproval, patchSetUploader); - logger.atFine().log("reviewers = %s, approvers = %s", reviewerAccountIds, approverAccountIds); - - FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners(); + CodeOwnerApprovalCheckInput input = + inputLoaderFactory.create(codeOwnersConfig, codeOwnerResolver, changeNotes).load(); return changedFiles .getFromDiffCache( @@ -384,15 +370,12 @@ getFileStatus( codeOwnerConfigHierarchy, codeOwnerResolver, + codeOwnersConfig, + changedFilesByPatchSetCacheFactory.create(codeOwnersConfig, changeNotes), branch, revision.orElse(null), - globalCodeOwners, - enableImplicitApproval ? changeOwner : null, - reviewerAccountIds, - approverAccountIds, - fallbackCodeOwners, - overrides, - changedFile)); + changedFile, + input)); } } @@ -405,22 +388,25 @@ * <p>The purpose of this method is to find the files/paths in a change that are owned by the * given account. * + * <p>As a side-effect, it also computes, for each file, who the approvers are if the file is not + * approved. + * * @param changeNotes the notes of the change for which the code owner statuses should be returned * @param patchSet the patch set for which the code owner statuses should be returned - * @param accountId the ID of the account for which an approval should be assumed + * @param accountIds The accounts to check whether they have owners permission. */ @VisibleForTesting - public Stream<FileCodeOwnerStatus> getFileStatusesForAccount( - ChangeNotes changeNotes, PatchSet patchSet, Account.Id accountId) + public Stream<FileCodeOwnerStatus> getFileStatusesForAccounts( + ChangeNotes changeNotes, PatchSet patchSet, ImmutableSet<Account.Id> accountIds) throws IOException, DiffNotAvailableException { requireNonNull(changeNotes, "changeNotes"); requireNonNull(patchSet, "patchSet"); - requireNonNull(accountId, "accountId"); + requireNonNull(accountIds, "accountIds"); try (Timer0.Context ctx = codeOwnerMetrics.prepareFileStatusComputationForAccount.start()) { logger.atFine().log( - "prepare stream to compute file statuses for account %d (project = %s, change = %d," + "prepare stream to compute file statuses for accounts %s (project = %s, change = %d," + " patch set = %d)", - accountId.get(), + accountIds, changeNotes.getProjectName(), changeNotes.getChangeId().get(), patchSet.id().get()); @@ -446,6 +432,9 @@ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy = codeOwnerConfigHierarchyProvider.get(); CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get().enforceVisibility(false); + CodeOwnerApprovalCheckInput input = + CodeOwnerApprovalCheckInput.createForComputingOwnedPaths( + codeOwnersConfig, codeOwnerResolver, changeNotes, accountIds); return changedFiles.getFromDiffCache(changeNotes.getProjectName(), patchSet.commitId()) .stream() .map( @@ -453,20 +442,12 @@ getFileStatus( codeOwnerConfigHierarchy, codeOwnerResolver, + codeOwnersConfig, + changedFilesByPatchSetCacheFactory.create(codeOwnersConfig, changeNotes), branch, revision.orElse(null), - /* globalCodeOwners= */ CodeOwnerResolverResult.createEmpty(), - // Do not check for implicit approvals since implicit approvals of other users - // should be ignored. For the given account we do not need to check for - // implicit approvals since all owned files are already covered by the - // explicit approval. - /* implicitApprover= */ null, - /* reviewerAccountIds= */ ImmutableSet.of(), - // Assume an explicit approval of the given account. - /* approverAccountIds= */ ImmutableSet.of(accountId), - fallbackCodeOwners, - /* overrides= */ ImmutableSet.of(), - changedFile)); + changedFile, + input)); } } @@ -509,15 +490,12 @@ private FileCodeOwnerStatus getFileStatus( CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, CodeOwnerResolver codeOwnerResolver, + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + ChangedFilesByPatchSetCache changedFilesByPatchSetCache, BranchNameKey branch, @Nullable ObjectId revision, - CodeOwnerResolverResult globalCodeOwners, - @Nullable Account.Id implicitApprover, - ImmutableSet<Account.Id> reviewerAccountIds, - ImmutableSet<Account.Id> approverAccountIds, - FallbackCodeOwners fallbackCodeOwners, - ImmutableSet<PatchSetApproval> overrides, - ChangedFile changedFile) { + ChangedFile changedFile, + CodeOwnerApprovalCheckInput input) { try (Timer0.Context ctx = codeOwnerMetrics.computeFileStatus.start()) { logger.atFine().log("computing file status for %s", changedFile); @@ -530,15 +508,12 @@ getPathCodeOwnerStatus( codeOwnerConfigHierarchy, codeOwnerResolver, + codeOwnersConfig, + changedFilesByPatchSetCache, branch, revision, - globalCodeOwners, - implicitApprover, - reviewerAccountIds, - approverAccountIds, - fallbackCodeOwners, - overrides, - newPath)); + newPath, + input)); // Compute the code owner status for the old path, if the file was deleted or renamed. Optional<PathCodeOwnerStatus> oldPathStatus = Optional.empty(); @@ -553,15 +528,12 @@ getPathCodeOwnerStatus( codeOwnerConfigHierarchy, codeOwnerResolver, + codeOwnersConfig, + changedFilesByPatchSetCache, branch, revision, - globalCodeOwners, - implicitApprover, - reviewerAccountIds, - approverAccountIds, - fallbackCodeOwners, - overrides, - changedFile.oldPath().get())); + changedFile.oldPath().get(), + input)); } FileCodeOwnerStatus fileCodeOwnerStatus = @@ -574,22 +546,19 @@ private PathCodeOwnerStatus getPathCodeOwnerStatus( CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, CodeOwnerResolver codeOwnerResolver, + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + ChangedFilesByPatchSetCache changedFilesByPatchSetCache, BranchNameKey branch, @Nullable ObjectId revision, - CodeOwnerResolverResult globalCodeOwners, - @Nullable Account.Id implicitApprover, - ImmutableSet<Account.Id> reviewerAccountIds, - ImmutableSet<Account.Id> approverAccountIds, - FallbackCodeOwners fallbackCodeOwners, - ImmutableSet<PatchSetApproval> overrides, - Path absolutePath) { + Path absolutePath, + CodeOwnerApprovalCheckInput input) { logger.atFine().log("computing path status for %s", absolutePath); - if (!overrides.isEmpty()) { + if (!input.overrides().isEmpty()) { logger.atFine().log( "the status for path %s is %s since an override is present (overrides = %s)", - absolutePath, CodeOwnerStatus.APPROVED.name(), overrides); - Optional<PatchSetApproval> override = overrides.stream().findAny(); + absolutePath, CodeOwnerStatus.APPROVED.name(), input.overrides()); + Optional<PatchSetApproval> override = input.overrides().stream().findAny(); checkState(override.isPresent(), "no override found"); return PathCodeOwnerStatus.create( absolutePath, @@ -603,19 +572,42 @@ AtomicReference<CodeOwnerStatus> codeOwnerStatus = new AtomicReference<>(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); AtomicReference<String> reason = new AtomicReference<>(/* initialValue= */ null); + ImmutableSet.Builder<Account.Id> activeOwners = ImmutableSet.builder(); - if (isApproved( - globalCodeOwners, - CodeOwnerKind.GLOBAL_CODE_OWNER, - approverAccountIds, - implicitApprover, - reason)) { + boolean isGloballyApproved = + isApproved( + absolutePath, + input.globalCodeOwners(), + CodeOwnerKind.GLOBAL_CODE_OWNER, + codeOwnersConfig, + changedFilesByPatchSetCache, + input, + reason); + + if (isGloballyApproved) { codeOwnerStatus.set(CodeOwnerStatus.APPROVED); + } + + if (input.globalCodeOwners().ownedByAllUsers()) { + activeOwners.addAll(input.approvers()); + activeOwners.addAll(input.reviewers()); } else { + activeOwners.addAll( + Sets.intersection(input.globalCodeOwners().codeOwnersAccountIds(), input.approvers())); + activeOwners.addAll( + Sets.intersection(input.globalCodeOwners().codeOwnersAccountIds(), input.reviewers())); + } + + // Only check recursively for all OWNERs in two scenarios: + // 1. The path was not globally approved + // 2. The path was globally approved but is not owned by all users and we + // want to calculate all ownerIds. + if (!isGloballyApproved + || (input.checkAllOwners() && !input.globalCodeOwners().ownedByAllUsers())) { logger.atFine().log("%s was not approved by a global code owner", absolutePath); if (isPending( - globalCodeOwners, CodeOwnerKind.GLOBAL_CODE_OWNER, reviewerAccountIds, reason)) { + input.globalCodeOwners(), CodeOwnerKind.GLOBAL_CODE_OWNER, input.reviewers(), reason)) { codeOwnerStatus.set(CodeOwnerStatus.PENDING); } @@ -634,6 +626,17 @@ CodeOwnerResolverResult codeOwners = resolveCodeOwners(codeOwnerResolver, pathCodeOwners); + + boolean ownedByAllUsers = codeOwners.ownedByAllUsers(); + if (ownedByAllUsers) { + activeOwners.addAll(input.approvers()); + activeOwners.addAll(input.reviewers()); + } else { + activeOwners.addAll( + Sets.intersection(codeOwners.codeOwnersAccountIds(), input.approvers())); + activeOwners.addAll( + Sets.intersection(codeOwners.codeOwnersAccountIds(), input.reviewers())); + } logger.atFine().log( "code owners = %s (code owner kind = %s, code owner config folder path = %s," + " file name = %s)", @@ -647,10 +650,18 @@ } if (isApproved( - codeOwners, codeOwnerKind, approverAccountIds, implicitApprover, reason)) { + absolutePath, + codeOwners, + codeOwnerKind, + codeOwnersConfig, + changedFilesByPatchSetCache, + input, + reason)) { codeOwnerStatus.set(CodeOwnerStatus.APPROVED); - return false; - } else if (isPending(codeOwners, codeOwnerKind, reviewerAccountIds, reason)) { + // No need to recurse if we are not checking all owners or all owners are + // are already added. + return input.checkAllOwners() && !ownedByAllUsers; + } else if (isPending(codeOwners, codeOwnerKind, input.reviewers(), reason)) { codeOwnerStatus.set(CodeOwnerStatus.PENDING); // We need to continue to check if any of the higher-level code owners approved @@ -678,12 +689,28 @@ CodeOwnerStatus codeOwnerStatusForFallbackCodeOwners = getCodeOwnerStatusForFallbackCodeOwners( codeOwnerStatus.get(), - implicitApprover, - reviewerAccountIds, - approverAccountIds, - fallbackCodeOwners, + input.implicitApprover().orElse(null), + input.reviewers(), + input.approvers(), + input.fallbackCodeOwners(), absolutePath, reason); + + if (codeOwnerStatusForFallbackCodeOwners.equals(CodeOwnerStatus.APPROVED)) { + activeOwners.addAll(input.approvers()); + } else if (codeOwnerStatusForFallbackCodeOwners.equals(CodeOwnerStatus.PENDING)) { + switch (input.fallbackCodeOwners()) { + case NONE: + // do nothing, if codeOwnerStatus is PENDING, the reviewers that are code owners have + // already been added to activeOwners + break; + case ALL_USERS: + // all users are code owners + activeOwners.addAll(input.approvers()); + activeOwners.addAll(input.reviewers()); + break; + } + } // Merge codeOwnerStatusForFallbackCodeOwners into codeOwnerStatus: // * codeOwnerStatus is the code owner status without taking fallback code owners into // account @@ -713,7 +740,11 @@ "%s has code owner status %s (reason = %s)", absolutePath, codeOwnerStatus.get(), reason.get() != null ? reason.get() : "n/a"); PathCodeOwnerStatus pathCodeOwnerStatus = - PathCodeOwnerStatus.create(absolutePath, codeOwnerStatus.get(), reason.get()); + PathCodeOwnerStatus.create( + absolutePath, + codeOwnerStatus.get(), + reason.get(), + input.checkAllOwners() ? Optional.of(activeOwners.build()) : Optional.empty()); logger.atFine().log("pathCodeOwnerStatus = %s", pathCodeOwnerStatus); return pathCodeOwnerStatus; } @@ -793,31 +824,36 @@ } /** - * Checks whether the given path was implicitly or explicitly approved. + * Checks whether the given path was approved implicitly, explicitly or by sticky approvals. * + * @param absolutePath the absolute path for which it should be checked whether it is code owner + * approved * @param codeOwners users that own the path * @param codeOwnerKind the kind of the given {@code codeOwners} - * @param approverAccountIds the IDs of the accounts that have approved the change - * @param implicitApprover the ID of the account the could be an implicit approver (aka last patch - * set uploader) + * @param codeOwnersConfig the code-owners plugin configuration that applies to the project that + * contains the change for which the code owner statuses are checked + * @param changedFilesByPatchSetCache cache that allows to lookup changed files by patch set + * @param input input data for checking if a path is code owner approved * @param reason {@link AtomicReference} on which the reason is being set if the path is approved * @return whether the path was approved */ private boolean isApproved( + Path absolutePath, CodeOwnerResolverResult codeOwners, CodeOwnerKind codeOwnerKind, - ImmutableSet<Account.Id> approverAccountIds, - @Nullable Account.Id implicitApprover, + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + ChangedFilesByPatchSetCache changedFilesByPatchSetCache, + CodeOwnerApprovalCheckInput input, AtomicReference<String> reason) { - if (implicitApprover != null) { - if (codeOwners.codeOwnersAccountIds().contains(implicitApprover) + if (input.implicitApprover().isPresent()) { + if (codeOwners.codeOwnersAccountIds().contains(input.implicitApprover().get()) || codeOwners.ownedByAllUsers()) { // If the uploader of the patch set owns the path, there is an implicit code owner // approval from the patch set uploader so that the path is automatically approved. reason.set( String.format( "implicitly approved by the patch set uploader %s who is a %s%s", - AccountTemplateUtil.getAccountTemplate(implicitApprover), + AccountTemplateUtil.getAccountTemplate(input.implicitApprover().get()), codeOwnerKind.getDisplayName(), codeOwners.ownedByAllUsers() ? String.format(" (all users are %ss)", codeOwnerKind.getDisplayName()) @@ -826,13 +862,14 @@ } } - if (!Collections.disjoint(approverAccountIds, codeOwners.codeOwnersAccountIds()) - || (codeOwners.ownedByAllUsers() && !approverAccountIds.isEmpty())) { + ImmutableSet<Account.Id> approvers = input.approvers(); + if (!Collections.disjoint(approvers, codeOwners.codeOwnersAccountIds()) + || (codeOwners.ownedByAllUsers() && !approvers.isEmpty())) { // At least one of the code owners approved the change. Optional<Account.Id> approver = codeOwners.ownedByAllUsers() - ? approverAccountIds.stream().findAny() - : approverAccountIds.stream() + ? approvers.stream().findAny() + : approvers.stream() .filter(accountId -> codeOwners.codeOwnersAccountIds().contains(accountId)) .findAny(); checkState(approver.isPresent(), "no approver found"); @@ -847,6 +884,58 @@ return true; } + return codeOwnersConfig.areStickyApprovalsEnabled() + && isApprovedByStickyApproval( + absolutePath, codeOwners, codeOwnerKind, changedFilesByPatchSetCache, input, reason); + } + + /** + * Checks whether the given path is code owner approved by a sticky approval on a previous patch + * set. + */ + private boolean isApprovedByStickyApproval( + Path absolutePath, + CodeOwnerResolverResult codeOwners, + CodeOwnerKind codeOwnerKind, + ChangedFilesByPatchSetCache changedFilesByPatchSetCache, + CodeOwnerApprovalCheckInput input, + AtomicReference<String> reason) { + for (PatchSet.Id patchSetId : input.previouslyApprovedPatchSetsInReverseOrder()) { + if (changedFilesByPatchSetCache.get(patchSetId).stream() + .anyMatch( + changedFile -> + changedFile.hasNewPath(absolutePath) || changedFile.hasOldPath(absolutePath))) { + logger.atFine().log( + "previously approved patch set %d contains path %s", patchSetId.get(), absolutePath); + Optional<Account.Id> approver = + input.approversFromPreviousPatchSets().get(patchSetId).stream() + .filter( + accountId -> + codeOwners.codeOwnersAccountIds().contains(accountId) + || codeOwners.ownedByAllUsers()) + .findAny(); + if (!approver.isPresent()) { + logger.atFine().log( + "none of the approvals on previous patch set %d is from a user that owns path %s" + + " (approvers=%s)", + patchSetId.get(), + absolutePath, + input.approversFromPreviousPatchSets().get(patchSetId)); + continue; + } + + reason.set( + String.format( + "approved on patch set %d by %s who is a %s%s", + patchSetId.get(), + AccountTemplateUtil.getAccountTemplate(approver.get()), + codeOwnerKind.getDisplayName(), + codeOwners.ownedByAllUsers() + ? String.format(" (all users are %ss)", codeOwnerKind.getDisplayName()) + : "")); + return true; + } + } return false; } @@ -901,111 +990,6 @@ } /** - * Gets the IDs of the accounts that are reviewer on the given change. - * - * @param changeNotes the change notes - */ - private ImmutableSet<Account.Id> getReviewerAccountIds( - RequiredApproval requiredApproval, ChangeNotes changeNotes, Account.Id patchSetUploader) { - ImmutableSet<Account.Id> reviewerAccountIds = - changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER); - if (requiredApproval.labelType().isIgnoreSelfApproval() - && reviewerAccountIds.contains(patchSetUploader)) { - logger.atFine().log( - "Removing patch set uploader %s from reviewers since the label of the required" - + " approval (%s) is configured to ignore self approvals", - patchSetUploader, requiredApproval.labelType()); - return filterOutAccount(reviewerAccountIds, patchSetUploader); - } - return reviewerAccountIds; - } - - /** - * Gets the IDs of the accounts that posted a patch set approval on the given revisions that - * counts as code owner approval. - * - * @param requiredApproval approval that is required from code owners to approve the files in a - * change - */ - private ImmutableSet<Account.Id> getApproverAccountIds( - ImmutableList<PatchSetApproval> currentPatchSetApprovals, - RequiredApproval requiredApproval, - Account.Id patchSetUploader) { - ImmutableSet<Account.Id> approverAccountIds = - currentPatchSetApprovals.stream() - .filter(requiredApproval::isApprovedBy) - .map(PatchSetApproval::accountId) - .collect(toImmutableSet()); - - if (requiredApproval.labelType().isIgnoreSelfApproval() - && approverAccountIds.contains(patchSetUploader)) { - logger.atFine().log( - "Removing patch set uploader %s from approvers since the label of the required" - + " approval (%s) is configured to ignore self approvals", - patchSetUploader, requiredApproval.labelType()); - return filterOutAccount(approverAccountIds, patchSetUploader); - } - - return approverAccountIds; - } - - private ImmutableList<PatchSetApproval> getCurrentPatchSetApprovals(ChangeNotes changeNotes) { - try (Timer0.Context ctx = codeOwnerMetrics.computePatchSetApprovals.start()) { - return ImmutableList.copyOf( - approvalsUtil.byPatchSet(changeNotes, changeNotes.getCurrentPatchSet().id())); - } - } - - private ImmutableSet<Account.Id> filterOutAccount( - ImmutableSet<Account.Id> accountIds, Account.Id accountIdToFilterOut) { - return accountIds.stream() - .filter(accountId -> !accountId.equals(accountIdToFilterOut)) - .collect(toImmutableSet()); - } - - /** - * Gets the overrides that were applied on the change. - * - * @param overrideApprovals approvals that count as override for the code owners submit check. - * @param patchSetUploader account ID of the patch set uploader - * @return the overrides that were applied on the change - */ - private ImmutableSet<PatchSetApproval> getOverride( - ImmutableList<PatchSetApproval> currentPatchSetApprovals, - ImmutableSet<RequiredApproval> overrideApprovals, - Account.Id patchSetUploader) { - ImmutableSet<RequiredApproval> overrideApprovalsThatIgnoreSelfApprovals = - overrideApprovals.stream() - .filter(overrideApproval -> overrideApproval.labelType().isIgnoreSelfApproval()) - .collect(toImmutableSet()); - return currentPatchSetApprovals.stream() - .filter( - approval -> { - // If the approval is from the patch set uploader and if it matches any of the labels - // for which self approvals are ignored, filter it out. - if (approval.accountId().equals(patchSetUploader) - && overrideApprovalsThatIgnoreSelfApprovals.stream() - .anyMatch( - requiredApproval -> - requiredApproval - .labelType() - .getLabelId() - .equals(approval.key().labelId()))) { - logger.atFine().log( - "Filtered out self-override %s of patch set uploader", - LabelVote.create(approval.label(), approval.value())); - return false; - } - return true; - }) - .filter( - patchSetApproval -> - overrideApprovals.stream() - .anyMatch(overrideApproval -> overrideApproval.isApprovedBy(patchSetApproval))) - .collect(toImmutableSet()); - } - - /** * Gets the current revision of the destination branch of the given change. * * <p>This is the revision from which the code owner configs should be read when computing code
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInput.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInput.java new file mode 100644 index 0000000..9335e5b --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInput.java
@@ -0,0 +1,495 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; +import static java.util.Comparator.comparing; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Account; +import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.PatchSetApproval; +import com.google.gerrit.metrics.Timer0; +import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; +import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval; +import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; +import com.google.gerrit.server.approval.ApprovalsUtil; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.util.LabelVote; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Input for {@link CodeOwnerApprovalCheck}. + * + * <p>Provides all data that is needed to check the code owner approvals on a change. + */ +@AutoValue +public abstract class CodeOwnerApprovalCheckInput { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** + * Gets the IDs of the accounts of all reviewers that can possibly code owner approve the change + * (if they are code owners). + * + * <p>If self approvals are ignored the patch set uploader is filtered out since in this case the + * patch set uploader cannot approve the change even if they are a code owner. + */ + public abstract ImmutableSet<Account.Id> reviewers(); + + /** + * Gets the IDs of the accounts that have an approval on the current patch set that possibly + * counts as code owner approval (if they are code owners). + * + * <p>If self approvals are ignored the patch set uploader is filtered out since in this case the + * approval of the patch set uploader is ignored even if they are a code owner. + */ + public abstract ImmutableSet<Account.Id> approvers(); + + /** + * Gets a map of previous patch sets to the IDs of the accounts that have an approval on that + * patch set that is sticky and possibly counts as code owner approval (if they are code owners). + * + * <p>If self approvals are ignored the patch set uploader is filtered out for all patch sets + * since in this case the approval of the patch set uploader is ignored even if they are a code + * owner. + */ + public abstract ImmutableMultimap<PatchSet.Id, Account.Id> approversFromPreviousPatchSets(); + + @Memoized + public ImmutableSortedSet<PatchSet.Id> previouslyApprovedPatchSetsInReverseOrder() { + return ImmutableSortedSet.orderedBy(comparing(PatchSet.Id::get).reversed()) + .addAll(approversFromPreviousPatchSets().keySet()) + .build(); + } + + /** + * Account from which an implicit code owner approval should be assumed. + * + * @see CodeOwnersPluginProjectConfigSnapshot#areImplicitApprovalsEnabled() + * @return the account of the change owner if implicit approvals are enabled, otherwise {@link + * Optional#empty()} + */ + public abstract Optional<Account.Id> implicitApprover(); + + /** + * Gets the approvals from the current patch set that count as code owner overrides. + * + * <p>If self approvals are ignored an override of the patch set uploader is filtered out since it + * doesn't count as code owner override. + */ + public abstract ImmutableSet<PatchSetApproval> overrides(); + + /** Gets the configured global code owners. */ + public abstract CodeOwnerResolverResult globalCodeOwners(); + + /** Gets the policy that defines who owns paths for which no code owners are defined. */ + public abstract FallbackCodeOwners fallbackCodeOwners(); + + /** + * Whether all code owners should be checked. * + * + * <p>If {@code true} {@link PathCodeOwnerStatus#owners()} are expected to be set in {@link + * PathCodeOwnerStatus} instances that are created by {@link CodeOwnerApprovalCheck}. + */ + public abstract boolean checkAllOwners(); + + /** + * Creates a {@link CodeOwnerApprovalCheckInput} instance for computing the paths in a change that + * are owned by the given accounts. + * + * @param codeOwnersConfig the code-owners plugin configuration + * @param codeOwnerResolver the {@link CodeOwnerResolver} that should be used to resolve the + * configured global code owners + * @param changeNotes the notes of the change for which owned paths should be computed + * @param accounts the accounts for which the owned paths should be computed + * @return the created {@link CodeOwnerApprovalCheckInput} instance + */ + public static CodeOwnerApprovalCheckInput createForComputingOwnedPaths( + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + CodeOwnerResolver codeOwnerResolver, + ChangeNotes changeNotes, + ImmutableSet<Account.Id> accounts) { + CodeOwnerResolverResult globalCodeOwners = + codeOwnerResolver.resolveGlobalCodeOwners(changeNotes.getProjectName()); + logger.atFine().log("global code owners = %s", globalCodeOwners); + + FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners(); + logger.atFine().log("fallbackCodeOwner = %s", fallbackCodeOwners); + + return create( + /* reviewers= */ ImmutableSet.of(), + /* approvers= */ accounts, + /* approversFromPreviousPatchSets= */ ImmutableMultimap.of(), + // Do not check for implicit approvals since implicit approvals of other users + // should be ignored. For the given account we do not need to check for + // implicit approvals since all owned files are already covered by the + // explicit approval. + /* implicitApprover= */ Optional.empty(), + /* overrides= */ ImmutableSet.of(), + globalCodeOwners, + fallbackCodeOwners, + /* checkAllOwners= */ true); + } + + /** + * Creates a {@link CodeOwnerApprovalCheckInput} instance. + * + * @param reviewers the reviewers that can possibly code owner approve the change (if they are + * code owners) + * @param approvers the accounts that have an approval on the current patch set that possibly + * counts as code owner approval (if they are code owners) + * @param implicitApprover account from which an implicit code owner approval should be assumed + * @param overrides the approvals from the current patch set that count as code owner overrides + * @param globalCodeOwners the configured global code owners + * @param fallbackCodeOwners the policy that defines who owns paths for which no code owners are + * defined + * @param checkAllOwners Whether all code owners are checked. If {@code true} {@link + * PathCodeOwnerStatus#owners()} will be set in the the {@link PathCodeOwnerStatus} instances + * that are created by {@link CodeOwnerApprovalCheck}. Checking all owners means that no + * shortcuts can be applied, hence checking the code owner approvals with {@code + * checkAllOwners=true} is more expensive. + * @return the created {@link CodeOwnerApprovalCheckInput} instance + */ + private static CodeOwnerApprovalCheckInput create( + ImmutableSet<Account.Id> reviewers, + ImmutableSet<Account.Id> approvers, + ImmutableMultimap<PatchSet.Id, Account.Id> approversFromPreviousPatchSets, + Optional<Account.Id> implicitApprover, + ImmutableSet<PatchSetApproval> overrides, + CodeOwnerResolverResult globalCodeOwners, + FallbackCodeOwners fallbackCodeOwners, + boolean checkAllOwners) { + return new AutoValue_CodeOwnerApprovalCheckInput( + reviewers, + approvers, + approversFromPreviousPatchSets, + implicitApprover, + overrides, + globalCodeOwners, + fallbackCodeOwners, + checkAllOwners); + } + + /** + * Class to load all inputs that are required for checking the code owner approvals on a change. + */ + public static class Loader { + /** Factory to create the {@link Loader} with injected dependencies. */ + interface Factory { + Loader create( + CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + CodeOwnerResolver codeOwnerResolver, + ChangeNotes changeNotes); + } + + private final ApprovalsUtil approvalsUtil; + private final CodeOwnerMetrics codeOwnerMetrics; + private final CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig; + private final CodeOwnerResolver codeOwnerResolver; + private final ChangeNotes changeNotes; + + @Inject + Loader( + ApprovalsUtil approvalsUtil, + CodeOwnerMetrics codeOwnerMetrics, + @Assisted CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig, + @Assisted CodeOwnerResolver codeOwnerResolver, + @Assisted ChangeNotes changeNotes) { + this.approvalsUtil = approvalsUtil; + this.codeOwnerMetrics = codeOwnerMetrics; + this.codeOwnersConfig = codeOwnersConfig; + this.codeOwnerResolver = codeOwnerResolver; + this.changeNotes = changeNotes; + } + + CodeOwnerApprovalCheckInput load() { + logger.atFine().log( + "requiredApproval = %s, overrideApprovals = %s", + codeOwnersConfig.getRequiredApproval().formatForLogging(), + RequiredApproval.formatForLogging(codeOwnersConfig.getOverrideApprovals())); + return CodeOwnerApprovalCheckInput.create( + getReviewers(), + getApprovers(), + getApproversFromPreviousPatchSets(), + getImplicitApprover(), + getOverrides(), + getGlobalCodeOwners(), + getFallbackCodeOwners(), + /* checkAllOwners= */ false); + } + + /** + * Gets the IDs of the accounts of all reviewers that can possibly code owner approve the change + * (if they are code owners). + * + * <p>If self approvals are ignored the patch set uploader is filtered out since in this case + * the patch set uploader cannot approve the change even if they are a code owner. + */ + private ImmutableSet<Account.Id> getReviewers() { + ImmutableSet<Account.Id> reviewerAccountIds = + changeNotes.getReviewers().byState(ReviewerStateInternal.REVIEWER); + RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); + Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); + if (requiredApproval.labelType().isIgnoreSelfApproval() + && reviewerAccountIds.contains(patchSetUploader)) { + logger.atFine().log( + "Removing patch set uploader %s from reviewers since the label of the required" + + " approval (%s) is configured to ignore self approvals", + patchSetUploader, requiredApproval.labelType()); + return filterOutAccount(reviewerAccountIds, patchSetUploader); + } + logger.atFine().log("reviewers = %s", reviewerAccountIds); + return reviewerAccountIds; + } + + /** + * Gets the IDs of the accounts that have an approval on the current patch set that possibly + * counts as code owner approval (if they are code owners). + * + * <p>If self approvals are ignored the patch set uploader is filtered out since in this case + * the approval of the patch set uploader is ignored even if they are a code owner. + */ + private ImmutableSet<Account.Id> getApprovers() { + ImmutableList<PatchSetApproval> currentPatchSetApprovals = + getCurrentPatchSetApprovals(changeNotes); + RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); + ImmutableSet<Account.Id> approverAccountIds = + currentPatchSetApprovals.stream() + .filter(requiredApproval::isApprovedBy) + .map(PatchSetApproval::accountId) + .collect(toImmutableSet()); + Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); + if (requiredApproval.labelType().isIgnoreSelfApproval() + && approverAccountIds.contains(patchSetUploader)) { + logger.atFine().log( + "Removing patch set uploader %s from approvers since the label of the required" + + " approval (%s) is configured to ignore self approvals", + patchSetUploader, requiredApproval.labelType()); + return filterOutAccount(approverAccountIds, patchSetUploader); + } + logger.atFine().log("approvers = %s", approverAccountIds); + return approverAccountIds; + } + + private Optional<Account.Id> getImplicitApprover() { + Account.Id changeOwner = changeNotes.getChange().getOwner(); + Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); + boolean implicitApprovalConfig = codeOwnersConfig.areImplicitApprovalsEnabled(); + boolean enableImplicitApproval = + implicitApprovalConfig && changeOwner.equals(patchSetUploader); + logger.atFine().log( + "changeOwner = %d, patchSetUploader = %d, implict approval config = %s\n" + + "=> implicit approval is %s", + changeOwner.get(), + patchSetUploader.get(), + implicitApprovalConfig, + enableImplicitApproval ? "enabled" : "disabled"); + return enableImplicitApproval ? Optional.of(changeOwner) : Optional.empty(); + } + + /** + * Gets a map of previous patch sets to the IDs of the accounts that have an approval on that + * patch set that is sticky and possibly counts as code owner approval (if they are code + * owners). + * + * <p>If self approvals are ignored the patch set uploader is filtered out for all patch sets + * since in this case the approval of the patch set uploader is ignored even if they are a code + * owner. + */ + private ImmutableMultimap<PatchSet.Id, Account.Id> getApproversFromPreviousPatchSets() { + if (!codeOwnersConfig.areStickyApprovalsEnabled()) { + logger.atFine().log("sticky approvals are disabled"); + return ImmutableMultimap.of(); + } + + // Filter out approvals on the current patch set, since here we are only interested in code + // owner approvals on previous patch sets that should be considered as sticky. + PatchSet.Id currentPatchSetId = changeNotes.getCurrentPatchSet().id(); + ImmutableSetMultimap<PatchSet.Id, Account.Id> approversFromPreviousPatchSets = + getLastCodeOwnerApprovalsByAccount().values().stream() + .filter(psa -> psa.patchSetId().get() < currentPatchSetId.get()) + .collect( + toImmutableSetMultimap( + PatchSetApproval::patchSetId, PatchSetApproval::accountId)); + logger.atFine().log( + "sticky approvals are enabled, approversFromPreviousPatchSets=%s", + approversFromPreviousPatchSets); + return approversFromPreviousPatchSets; + } + + /** + * Returns the last code owner approvals by account. + * + * <p>The returned map contains for each user their last approval on the change that counts as a + * code owner approval. Approvals that are invalidated by code owner votes on newer patch sets + * are filtered out. + */ + private ImmutableMap<Account.Id, PatchSetApproval> getLastCodeOwnerApprovalsByAccount() { + RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); + + Map<Account.Id, PatchSetApproval> lastCodeOwnerVotesByAccount = new HashMap<>(); + ImmutableSetMultimap<PatchSet.Id, PatchSetApproval> allCodeOwnerApprovals = + changeNotes.getApprovals().all().entries().stream() + // Only look at approvals on the label that is configured for code owner approvals. + .filter(e -> e.getValue().label().equals(requiredApproval.labelType().getName())) + .collect(toImmutableSetMultimap(Map.Entry::getKey, Map.Entry::getValue)); + logger.atFine().log("allCodeOwnerApprovals=%s", allCodeOwnerApprovals); + // Iterate over the patch sets in reverse order (latest patch set first). + for (PatchSet.Id patchSetId : getPatchSetIdsInReverseOrder()) { + // Only store the code owner approval if we didn't find a code owner approval for that + // account on a newer patch set yet. + // If a code owner approval on a newer patch set exist, it invalidated the code owner + // approval on the older patch set and we can ignore it. + allCodeOwnerApprovals + .get(patchSetId) + .forEach(psa -> lastCodeOwnerVotesByAccount.putIfAbsent(psa.accountId(), psa)); + } + + ImmutableMap<Account.Id, PatchSetApproval> lastCodeOwnerApprovalsByAccount = + lastCodeOwnerVotesByAccount.entrySet().stream() + // Remove all approvals which do not count as a code owner approval because the voting + // value is insufficient. + .filter(e -> requiredApproval.isApprovedBy(e.getValue())) + .filter(filterOutSelfApprovalsIfSelfApprovalsAreIgnored()) + .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + logger.atFine().log( + "lastCodeOwnerApprovalsByAccount=%s, lastCodeOwnerVotesByAccount=%s", + lastCodeOwnerApprovalsByAccount, lastCodeOwnerVotesByAccount); + return lastCodeOwnerApprovalsByAccount; + } + + /** + * Creates a filter that filters out self approvals by the patch set uploader if self approvals + * are ignored + */ + private Predicate<Map.Entry<Account.Id, PatchSetApproval>> + filterOutSelfApprovalsIfSelfApprovalsAreIgnored() { + RequiredApproval requiredApproval = codeOwnersConfig.getRequiredApproval(); + if (!requiredApproval.labelType().isIgnoreSelfApproval()) { + logger.atFine().log("s"); + return e -> true; + } + + Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); + return e -> { + if (e.getKey().equals(patchSetUploader)) { + logger.atFine().log( + "Removing approvals of the patch set uploader %s since the label of the required" + + " approval (%s) is configured to ignore self approvals", + patchSetUploader, requiredApproval.labelType()); + return false; + } + return true; + }; + } + + private ImmutableSortedSet<PatchSet.Id> getPatchSetIdsInReverseOrder() { + return ImmutableSortedSet.orderedBy(comparing(PatchSet.Id::get).reversed()) + .addAll(changeNotes.getPatchSets().keySet()) + .build(); + } + + /** + * Gets the approvals from the current patch set that count as code owner overrides. + * + * <p>If self approvals are ignored an override of the patch set uploader is filtered out since + * it doesn't count as code owner override. + */ + private ImmutableSet<PatchSetApproval> getOverrides() { + ImmutableList<PatchSetApproval> currentPatchSetApprovals = + getCurrentPatchSetApprovals(changeNotes); + ImmutableSortedSet<RequiredApproval> overrideApprovals = + codeOwnersConfig.getOverrideApprovals(); + ImmutableSet<RequiredApproval> overrideApprovalsThatIgnoreSelfApprovals = + overrideApprovals.stream() + .filter(overrideApproval -> overrideApproval.labelType().isIgnoreSelfApproval()) + .collect(toImmutableSet()); + Account.Id patchSetUploader = changeNotes.getCurrentPatchSet().uploader(); + ImmutableSet<PatchSetApproval> overrides = + currentPatchSetApprovals.stream() + .filter( + approval -> { + // If the approval is from the patch set uploader and if it matches any of the + // labels + // for which self approvals are ignored, filter it out. + if (approval.accountId().equals(patchSetUploader) + && overrideApprovalsThatIgnoreSelfApprovals.stream() + .anyMatch( + requiredApproval -> + requiredApproval + .labelType() + .getLabelId() + .equals(approval.key().labelId()))) { + logger.atFine().log( + "Filtered out self-override %s of patch set uploader", + LabelVote.create(approval.label(), approval.value())); + return false; + } + return true; + }) + .filter( + patchSetApproval -> + overrideApprovals.stream() + .anyMatch( + overrideApproval -> overrideApproval.isApprovedBy(patchSetApproval))) + .collect(toImmutableSet()); + logger.atFine().log("hasOverride = %s (overrides = %s)", !overrides.isEmpty(), overrides); + return overrides; + } + + private CodeOwnerResolverResult getGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = + codeOwnerResolver.resolveGlobalCodeOwners(changeNotes.getProjectName()); + logger.atFine().log("global code owners = %s", globalCodeOwners); + return globalCodeOwners; + } + + private FallbackCodeOwners getFallbackCodeOwners() { + FallbackCodeOwners fallbackCodeOwners = codeOwnersConfig.getFallbackCodeOwners(); + logger.atFine().log("fallbackCodeOwners = %s", fallbackCodeOwners); + return fallbackCodeOwners; + } + + private ImmutableList<PatchSetApproval> getCurrentPatchSetApprovals(ChangeNotes changeNotes) { + try (Timer0.Context ctx = codeOwnerMetrics.computePatchSetApprovals.start()) { + return ImmutableList.copyOf( + approvalsUtil.byPatchSet(changeNotes, changeNotes.getCurrentPatchSet().id())); + } + } + + private static ImmutableSet<Account.Id> filterOutAccount( + ImmutableSet<Account.Id> accountIds, Account.Id accountIdToFilterOut) { + return accountIds.stream() + .filter(accountId -> !accountId.equals(accountIdToFilterOut)) + .collect(toImmutableSet()); + } + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java index 90fd878..0669b68 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFile.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; @@ -25,6 +26,7 @@ import com.google.gerrit.plugins.codeowners.util.JgitPath; import com.google.gerrit.server.git.meta.MetaDataUpdate; import com.google.gerrit.server.git.meta.VersionedMetaData; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.inject.Inject; import java.io.IOException; import java.util.Optional; @@ -260,8 +262,11 @@ update.getRepository().exactRef(getRefName()) != null, "branch %s does not exist", getRefName()); - - return super.commit(update); + // The commit goes to an ordinary branch (e.g. refs/heads/main). PLUGIN context is enough for + // such cases. + try(RefUpdateContext ctx = RefUpdateContext.open(PLUGIN)) { + return super.commit(update); + } } @Override
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java index 988121b..4f4c0c8 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; @@ -25,6 +26,7 @@ import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -101,7 +103,9 @@ "updating code owner files in branch %s of project %s", branchNameKey.branch(), branchNameKey.project()); - try (Repository repository = repoManager.openRepository(branchNameKey.project()); + try ( + RefUpdateContext ctx = RefUpdateContext.open(PLUGIN); + Repository repository = repoManager.openRepository(branchNameKey.project()); RevWalk rw = new RevWalk(repository); ObjectInserter oi = repository.newObjectInserter(); CodeOwnerConfigTreeWalk treeWalk =
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java new file mode 100644 index 0000000..a75c15b --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImport.java
@@ -0,0 +1,86 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import java.util.Optional; + +/** + * Information about an import of a {@link CodeOwnerConfig}. + * + * <p>Contains the keys of the importing and the imported code owner config, as well as the + * reference that the importing code owner config uses to reference the imported code owner config + * (contains the import mode). + * + * <p>It's possible that this class represents non-resolveable imports (e.g. an import of a + * non-existing code owner config). In this case an error message is contained that explains why the + * import couldn't be resolved. + */ +@AutoValue +public abstract class CodeOwnerConfigImport { + /** Key of the importing code owner config. */ + public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig(); + + /** Key of the imported code owner config. */ + public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig(); + + /** The code owner config reference that references the imported code owner config. */ + public abstract CodeOwnerConfigReference codeOwnerConfigReference(); + + /** + * If the import couldn't be resolved, a message explaining why the code owner config reference + * couldn't be resolved. + */ + public abstract Optional<String> errorMessage(); + + @Override + public final String toString() { + return MoreObjects.toStringHelper(this) + .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig()) + .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig()) + .add("codeOwnerConfigReference", codeOwnerConfigReference()) + .add("errorMessage", errorMessage()) + .toString(); + } + + /** Creates a {@link CodeOwnerConfigImport} instance for an unresolved import. */ + @VisibleForTesting + public static CodeOwnerConfigImport createUnresolvedImport( + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig, + CodeOwnerConfigReference codeOwnerConfigReference, + String errorMessage) { + return new AutoValue_CodeOwnerConfigImport( + keyOfImportingCodeOwnerConfig, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + Optional.of(errorMessage)); + } + + /** Creates a {@link CodeOwnerConfigImport} instance for a resolved import. */ + @VisibleForTesting + public static CodeOwnerConfigImport createResolvedImport( + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig, + CodeOwnerConfigReference codeOwnerConfigReference) { + return new AutoValue_CodeOwnerConfigImport( + keyOfImportingCodeOwnerConfig, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + Optional.empty()); + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java index 07894ce..427e5eb 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -50,7 +50,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -234,6 +233,7 @@ return resolve( pathCodeOwnersResult.get().getPathCodeOwners(), pathCodeOwnersResult.get().getAnnotations(), + pathCodeOwnersResult.get().resolvedImports(), pathCodeOwnersResult.get().unresolvedImports(), pathCodeOwnersResult.messages()); } @@ -260,6 +260,7 @@ return resolve( codeOwnerReferences, /* annotationsByCodeOwnerReference= */ ImmutableMultimap.of(), + /* resolvedImports= */ ImmutableList.of(), /* unresolvedImports= */ ImmutableList.of(), /* pathCodeOwnersMessages= */ ImmutableList.of()); } @@ -279,9 +280,11 @@ private CodeOwnerResolverResult resolve( Set<CodeOwnerReference> codeOwnerReferences, ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotationsByCodeOwnerReference, - List<UnresolvedImport> unresolvedImports, + ImmutableList<CodeOwnerConfigImport> resolvedImports, + ImmutableList<CodeOwnerConfigImport> unresolvedImports, ImmutableList<String> pathCodeOwnersMessages) { requireNonNull(codeOwnerReferences, "codeOwnerReferences"); + requireNonNull(resolvedImports, "resolvedImports"); requireNonNull(unresolvedImports, "unresolvedImports"); requireNonNull(pathCodeOwnersMessages, "pathCodeOwnersMessages"); @@ -313,7 +316,8 @@ annotationsByCodeOwner.build(), ownedByAllUsers.get(), hasUnresolvedCodeOwners.get(), - !unresolvedImports.isEmpty(), + resolvedImports, + unresolvedImports, messageBuilder.build()); logger.atFine().log("resolve result = %s", codeOwnerResolverResult); return codeOwnerResolverResult; @@ -837,7 +841,7 @@ // emails try { if (user != null) { - if (!permissionBackend.user(user).test(GlobalPermission.MODIFY_ACCOUNT)) { + if (!permissionBackend.user(user).test(GlobalPermission.VIEW_SECONDARY_EMAILS)) { transientCodeOwnerCache.cacheNonResolvable(email); messages.add( String.format( @@ -852,7 +856,7 @@ + " and user %s can see secondary emails", email, accountState.account().id(), user.getLoggableName())); return true; - } else if (!permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) { + } else if (!permissionBackend.currentUser().test(GlobalPermission.VIEW_SECONDARY_EMAILS)) { transientCodeOwnerCache.cacheNonResolvable(email); messages.add( String.format( @@ -871,7 +875,7 @@ } catch (PermissionBackendException ex) { throw newInternalServerError( String.format( - "failed to test the %s global capability", GlobalPermission.MODIFY_ACCOUNT), + "failed to test the %s global capability", GlobalPermission.VIEW_SECONDARY_EMAILS), ex); } };
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java index 7dd9e96..4db3a4c 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -52,8 +52,16 @@ /** Whether there are code owner references which couldn't be resolved. */ public abstract boolean hasUnresolvedCodeOwners(); + /** Imports which were successfully resolved. */ + public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports(); + + /** Imports which couldn't be resolved. */ + public abstract ImmutableList<CodeOwnerConfigImport> unresolvedImports(); + /** Whether there are imports which couldn't be resolved. */ - public abstract boolean hasUnresolvedImports(); + public boolean hasUnresolvedImports() { + return !unresolvedImports().isEmpty(); + } /** Gets messages that were collected while resolving the code owners. */ public abstract ImmutableList<String> messages(); @@ -76,7 +84,8 @@ .add("annotations", annotations()) .add("ownedByAllUsers", ownedByAllUsers()) .add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners()) - .add("hasUnresolvedImports", hasUnresolvedImports()) + .add("resolvedImports", resolvedImports()) + .add("unresolvedImports", unresolvedImports()) .add("messages", messages()) .toString(); } @@ -87,25 +96,16 @@ ImmutableMultimap<CodeOwner, CodeOwnerAnnotation> annotations, boolean ownedByAllUsers, boolean hasUnresolvedCodeOwners, - boolean hasUnresolvedImports, + ImmutableList<CodeOwnerConfigImport> resolvedImports, + ImmutableList<CodeOwnerConfigImport> unresolvedImports, List<String> messages) { return new AutoValue_CodeOwnerResolverResult( codeOwners, annotations, ownedByAllUsers, hasUnresolvedCodeOwners, - hasUnresolvedImports, + resolvedImports, + unresolvedImports, ImmutableList.copyOf(messages)); } - - /** Creates a empty {@link CodeOwnerResolverResult} instance. */ - public static CodeOwnerResolverResult createEmpty() { - return new AutoValue_CodeOwnerResolverResult( - /* codeOwners= */ ImmutableSet.of(), - /* annotations= */ ImmutableMultimap.of(), - /* ownedByAllUsers= */ false, - /* hasUnresolvedCodeOwners= */ false, - /* hasUnresolvedImports= */ false, - /* messages= */ ImmutableList.of()); - } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java index ae1f477..78f0056 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -14,6 +14,8 @@ package com.google.gerrit.plugins.codeowners.backend; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import static java.util.stream.Collectors.joining; import com.google.common.collect.ImmutableList; @@ -36,6 +38,7 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.gerrit.server.util.AccountTemplateUtil; import com.google.gerrit.server.util.ManualRequestContext; import com.google.gerrit.server.util.OneOffRequestContext; @@ -157,7 +160,9 @@ "addCodeOwnersMessageOnAddReviewer", updateFactory -> { try (BatchUpdate batchUpdate = - updateFactory.create(projectName, currentUser, when)) { + updateFactory.create(projectName, currentUser, when); + RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN); + RefUpdateContext changeCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) { batchUpdate.addOp(changeId, new Op(reviewers, maxPathsInChangeMessages)); batchUpdate.execute(); } @@ -221,7 +226,8 @@ changeNotes.getCurrentPatchSet(), reviewerAccountId, /* start= */ 0, - limit + 1)); + limit + 1, + /* checkReviewers= */ false)); if (ownedPaths.isEmpty()) { // this reviewer doesn't own any of the modified paths
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java b/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java index 86fc426..87b9910 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/FileCodeOwnerStatus.java
@@ -17,7 +17,9 @@ import static java.util.Objects.requireNonNull; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.Nullable; +import com.google.gerrit.entities.Account; import com.google.gerrit.plugins.codeowners.common.ChangedFile; import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus; import com.google.gerrit.plugins.codeowners.util.JgitPath; @@ -71,21 +73,33 @@ String path, CodeOwnerStatus codeOwnerStatus, @Nullable String reason) { requireNonNull(path, "path"); - return addition(JgitPath.of(path).getAsAbsolutePath(), codeOwnerStatus, reason); + return addition( + JgitPath.of(path).getAsAbsolutePath(), codeOwnerStatus, reason, Optional.empty()); } public static FileCodeOwnerStatus addition(Path path, CodeOwnerStatus codeOwnerStatus) { - return addition(path, codeOwnerStatus, /* reason= */ null); + return addition(path, codeOwnerStatus, /* reason= */ null, Optional.empty()); } public static FileCodeOwnerStatus addition( Path path, CodeOwnerStatus codeOwnerStatus, @Nullable String reason) { requireNonNull(path, "path"); + + return addition(path, codeOwnerStatus, reason, Optional.empty()); + } + + public static FileCodeOwnerStatus addition( + Path path, + CodeOwnerStatus codeOwnerStatus, + @Nullable String reason, + Optional<ImmutableSet<Account.Id>> owners) { + requireNonNull(path, "path"); requireNonNull(codeOwnerStatus, "codeOwnerStatus"); + requireNonNull(owners, "owners"); return create( ChangedFile.addition(path), - Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus, reason)), + Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus, reason, owners)), Optional.empty()); } @@ -104,6 +118,21 @@ Optional.empty()); } + public static FileCodeOwnerStatus modification( + Path path, + CodeOwnerStatus codeOwnerStatus, + @Nullable String reason, + ImmutableSet<Account.Id> owners) { + requireNonNull(path, "path"); + requireNonNull(codeOwnerStatus, "codeOwnerStatus"); + requireNonNull(owners, "owners"); + + return create( + ChangedFile.modification(path), + Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus, reason, Optional.of(owners))), + Optional.empty()); + } + public static FileCodeOwnerStatus deletion(String path, CodeOwnerStatus codeOwnerStatus) { return deletion(path, codeOwnerStatus, /* reason= */ null); } @@ -130,6 +159,22 @@ Optional.of(PathCodeOwnerStatus.create(path, codeOwnerStatus, reason))); } + public static FileCodeOwnerStatus deletion( + Path path, + CodeOwnerStatus codeOwnerStatus, + @Nullable String reason, + ImmutableSet<Account.Id> owners) { + requireNonNull(path, "path"); + requireNonNull(codeOwnerStatus, "codeOwnerStatus"); + requireNonNull(owners, "owners"); + + return create( + ChangedFile.deletion(path), + Optional.empty(), + Optional.of( + PathCodeOwnerStatus.create(path, codeOwnerStatus, reason, Optional.of(owners)))); + } + public static FileCodeOwnerStatus rename( String oldPath, CodeOwnerStatus oldPathCodeOwnerStatus, @@ -194,4 +239,28 @@ Optional.of(PathCodeOwnerStatus.create(newPath, newPathCodeOwnerStatus, reasonNewPath)), Optional.of(PathCodeOwnerStatus.create(oldPath, oldPathCodeOwnerStatus, reasonOldPath))); } + + public static FileCodeOwnerStatus rename( + Path oldPath, + CodeOwnerStatus oldPathCodeOwnerStatus, + @Nullable String reasonOldPath, + ImmutableSet<Account.Id> oldPathOwners, + Path newPath, + CodeOwnerStatus newPathCodeOwnerStatus, + @Nullable String reasonNewPath, + ImmutableSet<Account.Id> newPathOwners) { + requireNonNull(oldPath, "oldPath"); + requireNonNull(oldPathCodeOwnerStatus, "oldPathCodeOwnerStatus"); + requireNonNull(newPath, "newPath"); + requireNonNull(newPathCodeOwnerStatus, "newPathCodeOwnerStatus"); + + return create( + ChangedFile.rename(newPath, oldPath), + Optional.of( + PathCodeOwnerStatus.create( + newPath, newPathCodeOwnerStatus, reasonNewPath, Optional.of(newPathOwners))), + Optional.of( + PathCodeOwnerStatus.create( + oldPath, oldPathCodeOwnerStatus, reasonOldPath, Optional.of(oldPathOwners)))); + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java index 907f065..e7c03b1 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -15,6 +15,8 @@ package com.google.gerrit.plugins.codeowners.backend; import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION; +import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN; import com.google.common.collect.ImmutableList; import com.google.common.flogger.FluentLogger; @@ -25,14 +27,24 @@ import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval; import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; import com.google.gerrit.plugins.codeowners.util.JgitPath; +import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.restapi.change.OnPostReview; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.context.RefUpdateContext; import com.google.gerrit.server.util.AccountTemplateUtil; import com.google.gerrit.server.util.LabelVote; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; import com.google.inject.Inject; import com.google.inject.Singleton; import java.nio.file.Path; +import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -54,22 +66,38 @@ class OnCodeOwnerApproval implements OnPostReview { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final String TAG_ADD_CODE_OWNER_APPROVAL = + ChangeMessagesUtil.AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "code-owners:addCodeOwnerApproval"; + + private final WorkQueue workQueue; + private final OneOffRequestContext oneOffRequestContext; private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final CodeOwnerApprovalCheck codeOwnerApprovalCheck; private final CodeOwnerMetrics codeOwnerMetrics; + private final ChangeMessagesUtil changeMessageUtil; + private final RetryHelper retryHelper; @Inject OnCodeOwnerApproval( + WorkQueue workQueue, + OneOffRequestContext oneOffRequestContext, CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, CodeOwnerApprovalCheck codeOwnerApprovalCheck, - CodeOwnerMetrics codeOwnerMetrics) { + CodeOwnerMetrics codeOwnerMetrics, + ChangeMessagesUtil changeMessageUtil, + RetryHelper retryHelper) { + this.workQueue = workQueue; + this.oneOffRequestContext = oneOffRequestContext; this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.codeOwnerApprovalCheck = codeOwnerApprovalCheck; this.codeOwnerMetrics = codeOwnerMetrics; + this.changeMessageUtil = changeMessageUtil; + this.retryHelper = retryHelper; } @Override public Optional<String> getChangeMessageAddOn( + Instant when, IdentifiedUser user, ChangeNotes changeNotes, PatchSet patchSet, @@ -97,6 +125,33 @@ return Optional.empty(); } + if (codeOwnersConfig.enableAsyncMessageOnCodeOwnerApproval()) { + // post change message asynchronously to avoid adding latency to PostReview + logger.atFine().log("schedule asynchronous posting of the change message"); + @SuppressWarnings("unused") + WorkQueue.Task<?> possiblyIgnoredError = + (WorkQueue.Task<?>) + workQueue + .getDefaultQueue() + .submit( + () -> { + try (ManualRequestContext ignored = + oneOffRequestContext.openAs(user.getAccountId())) { + postChangeMessage( + when, + user, + changeNotes, + patchSet, + oldApprovals, + approvals, + requiredApproval, + maxPathsInChangeMessage); + } + }); + return Optional.empty(); + } + + logger.atFine().log("post change message synchronously"); try (Timer0.Context ctx = codeOwnerMetrics.extendChangeMessageOnPostReview.start()) { return buildMessageForCodeOwnerApproval( user, @@ -126,6 +181,58 @@ } } + private void postChangeMessage( + Instant when, + IdentifiedUser user, + ChangeNotes changeNotes, + PatchSet patchSet, + Map<String, Short> oldApprovals, + Map<String, Short> approvals, + RequiredApproval requiredApproval, + int maxPathsInChangeMessages) { + try (Timer0.Context ctx = codeOwnerMetrics.addChangeMessageOnCodeOwnerApproval.start()) { + retryHelper + .changeUpdate( + "addCodeOwnersMessageOnCodeOwnerApproval", + updateFactory -> { + try (BatchUpdate batchUpdate = + updateFactory.create(changeNotes.getProjectName(), user, when); + RefUpdateContext pluginCtx = RefUpdateContext.open(PLUGIN); + RefUpdateContext changeCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) { + batchUpdate.addOp( + changeNotes.getChangeId(), + new Op( + user, + changeNotes, + patchSet, + oldApprovals, + approvals, + requiredApproval, + maxPathsInChangeMessages)); + batchUpdate.execute(); + } + return null; + }) + .call(); + } catch (Exception e) { + Optional<? extends Exception> configurationError = + CodeOwnersExceptionHook.getCauseOfConfigurationError(e); + if (configurationError.isPresent()) { + logger.atWarning().log( + "Failed to post code-owners change message for code owner approval on change %s in" + + " project %s: %s", + changeNotes.getChangeId(), + changeNotes.getProjectName(), + configurationError.get().getMessage()); + } else { + logger.atSevere().withCause(e).log( + "Failed to post code-owners change message for code owner approval on change %s in" + + " project %s.", + changeNotes.getChangeId(), changeNotes.getProjectName()); + } + } + } + private Optional<String> buildMessageForCodeOwnerApproval( IdentifiedUser user, ChangeNotes changeNotes, @@ -144,7 +251,8 @@ changeNotes.getCurrentPatchSet(), user.getAccountId(), /* start= */ 0, - limit + 1)); + limit + 1, + /* checkReviewers= */ false)); if (ownedPaths.isEmpty()) { // the user doesn't own any of the modified paths @@ -289,4 +397,48 @@ approvals); return LabelVote.create(labelName, approvals.get(labelName)); } + + private class Op implements BatchUpdateOp { + private final IdentifiedUser user; + private final ChangeNotes changeNotes; + private final PatchSet patchSet; + private final Map<String, Short> oldApprovals; + private final Map<String, Short> approvals; + private final RequiredApproval requiredApproval; + private final int limit; + + Op( + IdentifiedUser user, + ChangeNotes changeNotes, + PatchSet patchSet, + Map<String, Short> oldApprovals, + Map<String, Short> approvals, + RequiredApproval requiredApproval, + int limit) { + this.user = user; + this.changeNotes = changeNotes; + this.patchSet = patchSet; + this.oldApprovals = oldApprovals; + this.approvals = approvals; + this.requiredApproval = requiredApproval; + this.limit = limit; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws Exception { + Optional<String> message = + buildMessageForCodeOwnerApproval( + user, changeNotes, patchSet, oldApprovals, approvals, requiredApproval, limit); + + if (message.isEmpty()) { + return false; + } + + changeMessageUtil.setChangeMessage( + ctx, + String.format("Patch Set %s: %s", patchSet.id().get(), message.get()), + TAG_ADD_CODE_OWNER_APPROVAL); + return true; + } + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java index ea9fa59..270b1ed 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerOverride.java
@@ -33,6 +33,7 @@ import com.google.gerrit.server.util.LabelVote; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -60,6 +61,7 @@ @Override public Optional<String> getChangeMessageAddOn( + Instant when, IdentifiedUser user, ChangeNotes changeNotes, PatchSet patchSet,
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java b/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java index c6283a3..f54182c 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/OwnedPath.java
@@ -15,18 +15,23 @@ package com.google.gerrit.plugins.codeowners.backend; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.entities.Account; import java.nio.file.Path; -/** Representation of a file path the may be owned by the user. */ +/** Representation of a file path and its code owners. */ @AutoValue public abstract class OwnedPath { - /** The path of the file that may be owned by the user. */ + /** The path of the file that may be owned. */ public abstract Path path(); - /** Whether the user owns this path. */ + /** Whether the specified user owns this path. */ public abstract boolean owned(); - public static OwnedPath create(Path path, boolean owned) { - return new AutoValue_OwnedPath(path, owned); + /** The owners for this path. */ + public abstract ImmutableSet<Account.Id> owners(); + + public static OwnedPath create(Path path, boolean owned, ImmutableSet<Account.Id> owners) { + return new AutoValue_OwnedPath(path, owned, owners); } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java index 2de39cd..6942268 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnerStatus.java
@@ -18,10 +18,13 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.Nullable; +import com.google.gerrit.entities.Account; import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus; import com.google.gerrit.plugins.codeowners.util.JgitPath; import java.nio.file.Path; +import java.util.Optional; /** Code owner status for a particular path that has been modified in a change. */ @AutoValue @@ -44,9 +47,19 @@ */ public abstract ImmutableList<String> reasons(); + /** + * List of code owners for this {@link #path()}. This field is only set if `checkAllOwners` is + * passed as true to {@link + * com.google.gerrit.plugins.codeowners.backend.CodeOwnerApprovalCheck#getPathCodeOwnerStatus}. + */ + public abstract Optional<ImmutableSet<Account.Id>> owners(); + /** Creates a builder for a {@link PathCodeOwnerStatus}. */ public static PathCodeOwnerStatus.Builder builder(Path path, CodeOwnerStatus codeOwnerStatus) { - return new AutoValue_PathCodeOwnerStatus.Builder().path(path).status(codeOwnerStatus); + return new AutoValue_PathCodeOwnerStatus.Builder() + .setPath(path) + .setStatus(codeOwnerStatus) + .setOwners(Optional.empty()); } /** Creates a builder for a {@link PathCodeOwnerStatus}. */ @@ -87,13 +100,35 @@ * * @param path the path to which the code owner status belongs * @param codeOwnerStatus the code owner status + * @param reason for the status + * @param owners owners for this path. + * @return the created {@link PathCodeOwnerStatus} instance + */ + public static PathCodeOwnerStatus create( + Path path, + CodeOwnerStatus codeOwnerStatus, + @Nullable String reason, + Optional<ImmutableSet<Account.Id>> owners) { + Builder builder = builder(path, codeOwnerStatus); + if (reason != null) { + builder.addReason(reason); + } + builder.setOwners(owners); + return builder.build(); + } + + /** + * Creates a {@link PathCodeOwnerStatus} instance. + * + * @param path the path to which the code owner status belongs + * @param codeOwnerStatus the code owner status * @return the created {@link PathCodeOwnerStatus} instance */ public static PathCodeOwnerStatus create(String path, CodeOwnerStatus codeOwnerStatus) { requireNonNull(path, "path"); requireNonNull(codeOwnerStatus, "codeOwnerStatus"); - return create(JgitPath.of(path).getAsAbsolutePath(), codeOwnerStatus); + return builder(path, codeOwnerStatus).build(); } /** Builder for a {@link PathCodeOwnerStatus}. */ @@ -104,14 +139,17 @@ * * @param path absolute path to which the status belongs */ - public abstract Builder path(Path path); + public abstract Builder setPath(Path path); /** Sets the code owner status of the path. */ - public abstract Builder status(CodeOwnerStatus codeOwnerStatus); + public abstract Builder setStatus(CodeOwnerStatus codeOwnerStatus); /** Gets a builder for adding reasons for this status. */ abstract ImmutableList.Builder<String> reasonsBuilder(); + /** Gets a builder for adding owners for this status. */ + abstract Builder setOwners(Optional<ImmutableSet<Account.Id>> owners); + /** Adds a reason for this status. */ public Builder addReason(String reason) { reasonsBuilder().add(reason);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java index a61c913..5e7a11f 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwners.java
@@ -252,17 +252,18 @@ } // Resolve global imports. - ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder(); - ImmutableSet<CodeOwnerConfigImport> globalImports = getGlobalImports(0, codeOwnerConfig); - OptionalResultWithMessages<List<UnresolvedImport>> unresolvedGlobalImports; + ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder(); + ImmutableSet<CodeOwnerImport> globalImports = getGlobalImports(0, codeOwnerConfig); + OptionalResultWithMessages<CodeOwnerConfigImports> globalImportedCodeOwnerConfigs; if (!globalCodeOwnersIgnored) { - unresolvedGlobalImports = + globalImportedCodeOwnerConfigs = resolveImports(codeOwnerConfig.key(), globalImports, resolvedCodeOwnerConfigBuilder); } else { // skip global import with mode GLOBAL_CODE_OWNER_SETS_ONLY, // since we already know that global code owners will be ignored, we do not need to resolve // these imports - unresolvedGlobalImports = + globalImportedCodeOwnerConfigs = resolveImports( codeOwnerConfig.key(), globalImports.stream() @@ -273,8 +274,9 @@ .collect(toImmutableSet()), resolvedCodeOwnerConfigBuilder); } - messages.addAll(unresolvedGlobalImports.messages()); - unresolvedImports.addAll(unresolvedGlobalImports.get()); + messages.addAll(globalImportedCodeOwnerConfigs.messages()); + resolvedImports.addAll(globalImportedCodeOwnerConfigs.get().resolved()); + unresolvedImports.addAll(globalImportedCodeOwnerConfigs.get().unresolved()); // Remove all global code owner sets if any per-file code owner set has the // ignoreGlobalAndParentCodeOwners flag set to true (as in this case they are ignored and @@ -317,18 +319,22 @@ } // Resolve per-file imports. - ImmutableSet<CodeOwnerConfigImport> perFileImports = + ImmutableSet<CodeOwnerImport> perFileImports = getPerFileImports( 0, codeOwnerConfig.key(), resolvedCodeOwnerConfigBuilder.codeOwnerSets()); - OptionalResultWithMessages<List<UnresolvedImport>> unresolvedPerFileImports = + OptionalResultWithMessages<CodeOwnerConfigImports> perFileImportedCodeOwnerConfigs = resolveImports(codeOwnerConfig.key(), perFileImports, resolvedCodeOwnerConfigBuilder); - messages.addAll(unresolvedPerFileImports.messages()); - unresolvedImports.addAll(unresolvedPerFileImports.get()); + messages.addAll(perFileImportedCodeOwnerConfigs.messages()); + resolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().resolved()); + unresolvedImports.addAll(perFileImportedCodeOwnerConfigs.get().unresolved()); this.pathCodeOwnersResult = OptionalResultWithMessages.create( PathCodeOwnersResult.create( - path, resolvedCodeOwnerConfigBuilder.build(), unresolvedImports.build()), + path, + resolvedCodeOwnerConfigBuilder.build(), + resolvedImports.build(), + unresolvedImports.build()), messages); logger.atFine().log("path code owners result = %s", pathCodeOwnersResult); return this.pathCodeOwnersResult; @@ -343,11 +349,12 @@ * @param resolvedCodeOwnerConfigBuilder the builder for the resolved code owner config * @return list of unresolved imports, empty list if all imports were successfully resolved */ - private OptionalResultWithMessages<List<UnresolvedImport>> resolveImports( + private OptionalResultWithMessages<CodeOwnerConfigImports> resolveImports( CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, - Set<CodeOwnerConfigImport> codeOwnerConfigImports, + Set<CodeOwnerImport> codeOwnerConfigImports, CodeOwnerConfig.Builder resolvedCodeOwnerConfigBuilder) { - ImmutableList.Builder<UnresolvedImport> unresolvedImports = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigImport> resolvedImports = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigImport> unresolvedImports = ImmutableList.builder(); StringBuilder messageBuilder = new StringBuilder(); try (Timer0.Context ctx = codeOwnerMetrics.resolveCodeOwnerConfigImports.start()) { logger.atFine().log("resolve imports of codeOwnerConfig %s", keyOfImportingCodeOwnerConfig); @@ -361,7 +368,7 @@ Map<BranchNameKey, ObjectId> revisionMap = new HashMap<>(); revisionMap.put(codeOwnerConfig.key().branchNameKey(), codeOwnerConfig.revision()); - Queue<CodeOwnerConfigImport> codeOwnerConfigsToImport = new ArrayDeque<>(); + Queue<CodeOwnerImport> codeOwnerConfigsToImport = new ArrayDeque<>(); codeOwnerConfigsToImport.addAll(codeOwnerConfigImports); if (!codeOwnerConfigsToImport.isEmpty()) { messageBuilder.append( @@ -370,7 +377,7 @@ keyOfImportingCodeOwnerConfig.format(codeOwners))); } while (!codeOwnerConfigsToImport.isEmpty()) { - CodeOwnerConfigImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll(); + CodeOwnerImport codeOwnerConfigImport = codeOwnerConfigsToImport.poll(); messageBuilder.append(codeOwnerConfigImport.format()); CodeOwnerConfigReference codeOwnerConfigReference = @@ -387,7 +394,7 @@ projectCache.get(keyOfImportedCodeOwnerConfig.project()); if (!projectState.isPresent()) { unresolvedImports.add( - UnresolvedImport.create( + CodeOwnerConfigImport.createUnresolvedImport( codeOwnerConfigImport.importingCodeOwnerConfig(), keyOfImportedCodeOwnerConfig, codeOwnerConfigReference, @@ -399,7 +406,7 @@ } if (!projectState.get().statePermitsRead()) { unresolvedImports.add( - UnresolvedImport.create( + CodeOwnerConfigImport.createUnresolvedImport( codeOwnerConfigImport.importingCodeOwnerConfig(), keyOfImportedCodeOwnerConfig, codeOwnerConfigReference, @@ -425,7 +432,7 @@ if (!mayBeImportedCodeOwnerConfig.isPresent()) { unresolvedImports.add( - UnresolvedImport.create( + CodeOwnerConfigImport.createUnresolvedImport( codeOwnerConfigImport.importingCodeOwnerConfig(), keyOfImportedCodeOwnerConfig, codeOwnerConfigReference, @@ -439,6 +446,13 @@ } CodeOwnerConfig importedCodeOwnerConfig = mayBeImportedCodeOwnerConfig.get(); + + resolvedImports.add( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigImport.importingCodeOwnerConfig(), + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + CodeOwnerConfigImportMode importMode = codeOwnerConfigReference.importMode(); logger.atFine().log("import mode = %s", importMode.name()); @@ -475,7 +489,7 @@ if (importMode.resolveImportsOfImport() && seenCodeOwnerConfigs.add(keyOfImportedCodeOwnerConfig)) { logger.atFine().log("resolve imports of imported code owner config"); - Set<CodeOwnerConfigImport> transitiveImports = new HashSet<>(); + Set<CodeOwnerImport> transitiveImports = new HashSet<>(); transitiveImports.addAll( getGlobalImports(codeOwnerConfigImport.importLevel() + 1, importedCodeOwnerConfig)); transitiveImports.addAll( @@ -495,7 +509,7 @@ transitiveImports.stream() .map( codeOwnerCfgImport -> - CodeOwnerConfigImport.create( + CodeOwnerImport.create( codeOwnerCfgImport.importLevel(), codeOwnerCfgImport.importingCodeOwnerConfig(), CodeOwnerConfigReference.copyWithNewImportMode( @@ -516,31 +530,31 @@ message = message.substring(0, message.length() - 1); } return OptionalResultWithMessages.create( - unresolvedImports.build(), + CodeOwnerConfigImports.create(resolvedImports.build(), unresolvedImports.build()), !message.isEmpty() ? ImmutableList.of(message) : ImmutableList.of()); } - private ImmutableSet<CodeOwnerConfigImport> getGlobalImports( + private ImmutableSet<CodeOwnerImport> getGlobalImports( int importLevel, CodeOwnerConfig codeOwnerConfig) { return codeOwnerConfig.imports().stream() .map( codeOwnerConfigReference -> - CodeOwnerConfigImport.create( + CodeOwnerImport.create( importLevel, codeOwnerConfig.key(), codeOwnerConfigReference)) .collect(toImmutableSet()); } - private ImmutableSet<CodeOwnerConfigImport> getPerFileImports( + private ImmutableSet<CodeOwnerImport> getPerFileImports( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, Set<CodeOwnerSet> codeOwnerSets) { - ImmutableSet.Builder<CodeOwnerConfigImport> codeOwnerConfigImports = ImmutableSet.builder(); + ImmutableSet.Builder<CodeOwnerImport> codeOwnerConfigImports = ImmutableSet.builder(); for (CodeOwnerSet codeOwnerSet : codeOwnerSets) { codeOwnerSet.imports().stream() .forEach( codeOwnerConfigReference -> codeOwnerConfigImports.add( - CodeOwnerConfigImport.create( + CodeOwnerImport.create( importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, @@ -623,7 +637,7 @@ } @AutoValue - abstract static class CodeOwnerConfigImport { + abstract static class CodeOwnerImport { /** * The import level. * @@ -683,7 +697,7 @@ return levels > 0 ? String.format("%" + (levels * 2) + "s", "") : ""; } - public static CodeOwnerConfigImport create( + public static CodeOwnerImport create( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, CodeOwnerConfigReference codeOwnerConfigReference) { @@ -691,7 +705,7 @@ importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, Optional.empty()); } - public static CodeOwnerConfigImport create( + public static CodeOwnerImport create( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, CodeOwnerConfigReference codeOwnerConfigReference, @@ -703,13 +717,28 @@ Optional.of(codeOwnerSet)); } - public static CodeOwnerConfigImport create( + public static CodeOwnerImport create( int importLevel, CodeOwnerConfig.Key importingCodeOwnerConfig, CodeOwnerConfigReference codeOwnerConfigReference, Optional<CodeOwnerSet> codeOwnerSet) { - return new AutoValue_PathCodeOwners_CodeOwnerConfigImport( + return new AutoValue_PathCodeOwners_CodeOwnerImport( importLevel, importingCodeOwnerConfig, codeOwnerConfigReference, codeOwnerSet); } } + + @AutoValue + abstract static class CodeOwnerConfigImports { + /** Imported code owner configs the could be resolved. */ + abstract ImmutableList<CodeOwnerConfigImport> resolved(); + + /** Imported code owner configs the could not be resolved. */ + abstract ImmutableList<CodeOwnerConfigImport> unresolved(); + + static CodeOwnerConfigImports create( + ImmutableList<CodeOwnerConfigImport> resolved, + ImmutableList<CodeOwnerConfigImport> unresolved) { + return new AutoValue_PathCodeOwners_CodeOwnerConfigImports(resolved, unresolved); + } + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java index 93a36be..5b262bf 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResult.java
@@ -36,8 +36,11 @@ /** Gets the resolved code owner config. */ abstract CodeOwnerConfig codeOwnerConfig(); + /** Gets a list of resolved imports. */ + public abstract ImmutableList<CodeOwnerConfigImport> resolvedImports(); + /** Gets a list of unresolved imports. */ - public abstract ImmutableList<UnresolvedImport> unresolvedImports(); + public abstract ImmutableList<CodeOwnerConfigImport> unresolvedImports(); /** Whether there are unresolved imports. */ public boolean hasUnresolvedImports() { @@ -104,14 +107,21 @@ return MoreObjects.toStringHelper(this) .add("path", path()) .add("codeOwnerConfig", codeOwnerConfig()) + .add("resolvedImports", resolvedImports()) .add("unresolvedImports", unresolvedImports()) .toString(); } /** Creates a {@link PathCodeOwnersResult} instance. */ public static PathCodeOwnersResult create( - Path path, CodeOwnerConfig codeOwnerConfig, List<UnresolvedImport> unresolvedImports) { + Path path, + CodeOwnerConfig codeOwnerConfig, + List<CodeOwnerConfigImport> resolvedImports, + List<CodeOwnerConfigImport> unresolvedImports) { return new AutoValue_PathCodeOwnersResult( - path, codeOwnerConfig, ImmutableList.copyOf(unresolvedImports)); + path, + codeOwnerConfig, + ImmutableList.copyOf(resolvedImports), + ImmutableList.copyOf(unresolvedImports)); } }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java deleted file mode 100644 index bea1e08..0000000 --- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImport.java +++ /dev/null
@@ -1,57 +0,0 @@ -// Copyright (C) 2020 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.plugins.codeowners.backend; - -import com.google.auto.value.AutoValue; -import com.google.common.base.MoreObjects; - -/** Information about an unresolved import. */ -@AutoValue -public abstract class UnresolvedImport { - /** Key of the importing code owner config. */ - public abstract CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig(); - - /** Key of the imported code owner config. */ - public abstract CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig(); - - /** The code owner config reference that was attempted to be resolved. */ - public abstract CodeOwnerConfigReference codeOwnerConfigReference(); - - /** Message explaining why the code owner config reference couldn't be resolved. */ - public abstract String message(); - - @Override - public final String toString() { - return MoreObjects.toStringHelper(this) - .add("keyOfImportingCodeOwnerConfig", keyOfImportingCodeOwnerConfig()) - .add("keyOfImportedCodeOwnerConfig", keyOfImportedCodeOwnerConfig()) - .add("codeOwnerConfigReference", codeOwnerConfigReference()) - .add("message", message()) - .toString(); - } - - /** Creates a {@link UnresolvedImport} instance. */ - static UnresolvedImport create( - CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig, - CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig, - CodeOwnerConfigReference codeOwnerConfigReference, - String message) { - return new AutoValue_UnresolvedImport( - keyOfImportingCodeOwnerConfig, - keyOfImportedCodeOwnerConfig, - codeOwnerConfigReference, - message); - } -}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java index 52958d9..7946468 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportFormatter.java
@@ -20,7 +20,7 @@ import com.google.inject.Inject; import java.nio.file.Path; -/** Class to format an {@link UnresolvedImport} as a user-readable string. */ +/** Class to format an {@link CodeOwnerConfigImport} as a user-readable string. */ public class UnresolvedImportFormatter { private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final ProjectCache projectCache; @@ -37,7 +37,7 @@ } /** Returns a user-readable string representation of the given unresolved import. */ - public String format(UnresolvedImport unresolvedImport) { + public String format(CodeOwnerConfigImport unresolvedImport) { return String.format( "The import of %s:%s:%s in %s:%s:%s cannot be resolved: %s", unresolvedImport.keyOfImportedCodeOwnerConfig().project(), @@ -46,10 +46,16 @@ unresolvedImport.keyOfImportingCodeOwnerConfig().project(), unresolvedImport.keyOfImportingCodeOwnerConfig().shortBranchName(), getFilePath(unresolvedImport.keyOfImportingCodeOwnerConfig()), - unresolvedImport.message()); + unresolvedImport + .errorMessage() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "unresolved import %s must have an error message", unresolvedImport)))); } - private Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) { + public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) { return getBackend(codeOwnerConfigKey).getFilePath(codeOwnerConfigKey); } @@ -60,6 +66,10 @@ * returned. */ private CodeOwnerBackend getBackend(CodeOwnerConfig.Key codeOwnerConfigKey) { + // For unresolved imports the project may not exist. Trying to get the project config for + // non-existing projects fails, hence check whether the project exists before trying to access + // the project config and fall back to the default code owner backend if the project doesn't + // exist. if (projectCache.get(codeOwnerConfigKey.project()).isPresent()) { return codeOwnersPluginConfiguration .getProjectConfig(codeOwnerConfigKey.project())
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java index 5b2ab1c..786d3e4 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
@@ -84,6 +84,7 @@ @Nullable private FallbackCodeOwners fallbackCodeOwners; @Nullable private Integer maxPathsInChangeMessages; @Nullable private Boolean enableAsyncMessageOnAddReviewer; + @Nullable private Boolean enableAsyncMessageOnCodeOwnerApproval; @Nullable private ImmutableSet<CodeOwnerReference> globalCodeOwners; @Nullable private ImmutableSet<Account.Id> exemptedAccounts; @Nullable private Optional<String> overrideInfoUrl; @@ -95,6 +96,7 @@ private Map<String, Optional<PathExpressions>> pathExpressionsByBranch = new HashMap<>(); @Nullable private Optional<PathExpressions> pathExpressions; @Nullable private Boolean implicitApprovalsEnabled; + @Nullable private Boolean stickyApprovalsEnabled; @Nullable private RequiredApproval requiredApproval; @Nullable private ImmutableSortedSet<RequiredApproval> overrideApprovals; @@ -332,6 +334,18 @@ return enableAsyncMessageOnAddReviewer; } + /** + * Gets whether applying a code owner approval should post a change message asynchronously instead + * of synchronously extending the change message that is posted by Gerrit core. + */ + public boolean enableAsyncMessageOnCodeOwnerApproval() { + if (enableAsyncMessageOnCodeOwnerApproval == null) { + enableAsyncMessageOnCodeOwnerApproval = + generalConfig.enableAsyncMessageOnCodeOwnerApproval(projectName, pluginConfig); + } + return enableAsyncMessageOnCodeOwnerApproval; + } + /** Gets the global code owners. */ public ImmutableSet<CodeOwnerReference> getGlobalCodeOwners() { if (globalCodeOwners == null) { @@ -580,7 +594,7 @@ /** * Checks whether implicit code owner approvals are enabled. * - * <p>If enabled, an implict code owner approval from the change owner is assumed if the last + * <p>If enabled, an implicit code owner approval from the change owner is assumed if the last * patch set was uploaded by the change owner. */ public boolean areImplicitApprovalsEnabled() { @@ -618,6 +632,27 @@ } /** + * Checks whether sticky code owner approvals are enabled. + * + * <p>If enabled, a code owner approval on a previous patch set is sticky (if the approver didn't + * alter or remove it on a later patch set). + */ + public boolean areStickyApprovalsEnabled() { + if (stickyApprovalsEnabled == null) { + stickyApprovalsEnabled = readStickyApprovalsEnabled(); + } + return stickyApprovalsEnabled; + } + + private boolean readStickyApprovalsEnabled() { + boolean enableStickyApprovals = generalConfig.enableStickyApprovals(projectName, pluginConfig); + logger.atFine().log( + "sticky approvals on project %s are %s", + projectName, enableStickyApprovals ? "enabled" : "disabled"); + return enableStickyApprovals; + } + + /** * Returns the approval that is required from code owners to approve the files in a change. * * <p>Defines which approval counts as code owner approval.
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java index 00574bc..ada9105 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfig.java
@@ -65,6 +65,8 @@ public static final String KEY_FILE_EXTENSION = "fileExtension"; public static final String KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER = "enableAsyncMessageOnAddReviewer"; + public static final String KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL = + "enableAsyncMessageOnCodeOwnerApproval"; public static final String KEY_ENABLE_CODE_OWNER_CONFIG_FILES_WITH_FILE_EXTENSIONS = "enableCodeOwnerConfigFilesWithFileExtensions"; public static final String KEY_READ_ONLY = "readOnly"; @@ -80,6 +82,7 @@ public static final String KEY_GLOBAL_CODE_OWNER = "globalCodeOwner"; public static final String KEY_EXEMPTED_USER = "exemptedUser"; public static final String KEY_ENABLE_IMPLICIT_APPROVALS = "enableImplicitApprovals"; + public static final String KEY_ENABLE_STICKY_APPROVALS = "enableStickyApprovals"; public static final String KEY_OVERRIDE_INFO_URL = "overrideInfoUrl"; public static final String KEY_INVALID_CODE_OWNER_CONFIG_INFO_URL = "invalidCodeOwnerConfigInfoUrl"; @@ -475,6 +478,25 @@ } /** + * Gets whether applying a code owner approval should post a change message asynchronously instead + * of synchronously extending the change message that is posted by Gerrit core. + * + * @param project the project for which the enable async message on code owner approval + * configuration should be read + * @param pluginConfig the plugin config from which the enable async message on code owner + * approval configuration should be read + * @return whether applying a code owner approval should post a change message asynchronously + * instead of synchronously extending the change message that is posted by Gerrit core + */ + boolean enableAsyncMessageOnCodeOwnerApproval(Project.NameKey project, Config pluginConfig) { + return getBooleanConfig( + project, + pluginConfig, + KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + /* defaultValue= */ true); + } + + /** * Gets the enable validation on branch creation configuration from the given plugin config for * the specified project with fallback to {@code gerrit.config} and default to {@code true}. * @@ -875,6 +897,22 @@ } /** + * Gets whether sticky code owner approvals are enabled from the given plugin config with fallback + * to {@code gerrit.config}. + * + * <p>If enabled, a code owner approval on a previous patch set is sticky (if the approver didn't + * alter or remove it on a later patch set). + * + * @param project the name of the project for which the configuration should be read + * @param pluginConfig the plugin config from which the configuration should be read. + * @return whether a sticky code owner approvals are enabled + */ + boolean enableStickyApprovals(Project.NameKey project, Config pluginConfig) { + return getBooleanConfig( + project, pluginConfig, KEY_ENABLE_STICKY_APPROVALS, /* defaultValue= */ false); + } + + /** * Gets the users which are configured as global code owners from the given plugin config with * fallback to {@code gerrit.config}. *
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java index 4cbdba0..9dd283d 100644 --- a/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java +++ b/java/com/google/gerrit/plugins/codeowners/backend/config/RequiredApproval.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Objects.requireNonNull; import com.google.auto.value.AutoValue; @@ -25,6 +26,7 @@ import com.google.gerrit.entities.LabelType; import com.google.gerrit.entities.PatchSetApproval; import com.google.gerrit.server.project.ProjectState; +import java.util.Collection; import java.util.Optional; /** @@ -69,6 +71,18 @@ return labelType().getName() + "+" + value(); } + public final String formatForLogging() { + return String.format( + "%s (ignoreSelfApproval = %s)", toString(), labelType().isIgnoreSelfApproval()); + } + + public static String formatForLogging(Collection<RequiredApproval> requiredApprovals) { + return requiredApprovals.stream() + .map(RequiredApproval::formatForLogging) + .collect(toImmutableList()) + .toString(); + } + /** * Parses a string-representation of a {@link RequiredApproval}. *
diff --git a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java index d6ac638..4a6c37d 100644 --- a/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java +++ b/java/com/google/gerrit/plugins/codeowners/metrics/CodeOwnerMetrics.java
@@ -33,8 +33,9 @@ public class CodeOwnerMetrics { // latency metrics public final Timer1<String> addChangeMessageOnAddReviewer; + public final Timer0 addChangeMessageOnCodeOwnerApproval; public final Timer0 computeFileStatus; - public final Timer0 computeFileStatuses; + public final Timer1<Boolean> computeFileStatuses; public final Timer0 computeOwnedPaths; public final Timer0 computePatchSetApprovals; public final Timer0 extendChangeMessageOnPostReview; @@ -85,12 +86,20 @@ .description( "Whether the change message was posted synchronously or asynchronously.") .build()); + this.addChangeMessageOnCodeOwnerApproval = + createTimer( + "add_change_message_on_code_owner_approval", + "Latency for asynchronously adding a change message with the owned path when a code" + + " owner approval is applied"); this.computeFileStatus = createTimer("compute_file_status", "Latency for computing the file status of one file"); this.computeFileStatuses = createTimer( "compute_file_statuses", - "Latency for computing file statuses for all files in a change"); + "Latency for computing file statuses for all files in a change", + Field.ofBoolean("sticky_approvals", (metadataBuilder, stickyApprovals) -> {}) + .description("Whether sticky approvals on file level are enabled.") + .build()); this.computeOwnedPaths = createTimer( "compute_owned_paths", @@ -102,8 +111,8 @@ this.extendChangeMessageOnPostReview = createTimer( "extend_change_message_on_post_review", - "Latency for extending the change message with the owned path when a code owner" - + " approval is applied"); + "Latency for synchronously extending the change message with the owned path when a" + + " code owner approval is applied"); this.getChangedFiles = createTimer("get_changed_files", "Latency for getting changed files from diff cache"); this.prepareFileStatusComputation =
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java index 677e67c..0fbb01d 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -38,6 +38,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo; import com.google.gerrit.plugins.codeowners.backend.CodeOwner; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerAnnotation; @@ -92,6 +93,7 @@ private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy; private final Provider<CodeOwnerResolver> codeOwnerResolver; private final CodeOwnerJson.Factory codeOwnerJsonFactory; + private final CodeOwnerConfigFileJson codeOwnerConfigFileJson; private final EnumSet<ListAccountsOption> options; private final Set<String> hexOptions; @@ -167,7 +169,8 @@ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, Provider<CodeOwnerResolver> codeOwnerResolver, - CodeOwnerJson.Factory codeOwnerJsonFactory) { + CodeOwnerJson.Factory codeOwnerJsonFactory, + CodeOwnerConfigFileJson codeOwnerConfigFileJson) { this.accountVisibility = accountVisibility; this.accounts = accounts; this.accountControlFactory = accountControlFactory; @@ -178,6 +181,7 @@ this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy; this.codeOwnerResolver = codeOwnerResolver; this.codeOwnerJsonFactory = codeOwnerJsonFactory; + this.codeOwnerConfigFileJson = codeOwnerConfigFileJson; this.options = EnumSet.noneOf(ListAccountsOption.class); this.hexOptions = new HashSet<>(); } @@ -213,6 +217,8 @@ ListMultimap<CodeOwner, CodeOwnerAnnotation> annotations = LinkedListMultimap.create(); AtomicBoolean ownedByAllUsers = new AtomicBoolean(false); ImmutableList.Builder<String> debugLogsBuilder = ImmutableList.builder(); + ImmutableList.Builder<CodeOwnerConfigFileInfo> codeOwnerConfigFileInfosBuilder = + ImmutableList.builder(); codeOwnerConfigHierarchy.visit( rsrc.getBranch(), rsrc.getRevision(), @@ -221,6 +227,12 @@ CodeOwnerResolverResult pathCodeOwners = codeOwnerResolver.get().resolvePathCodeOwners(codeOwnerConfig, rsrc.getPath()); + codeOwnerConfigFileInfosBuilder.add( + codeOwnerConfigFileJson.format( + codeOwnerConfig.key(), + pathCodeOwners.resolvedImports(), + pathCodeOwners.unresolvedImports())); + debugLogsBuilder.addAll(pathCodeOwners.messages()); codeOwners.addAll(pathCodeOwners.codeOwners()); annotations.putAll(pathCodeOwners.annotations()); @@ -308,7 +320,7 @@ ImmutableMap<CodeOwner, Double> scoredCodeOwners = codeOwnerScorings.getScorings(filteredCodeOwners); - ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(rsrc, scoredCodeOwners); + ImmutableList<CodeOwner> sortedAndLimitedCodeOwners = sortAndLimit(scoredCodeOwners); if (highestScoreOnly) { Optional<Double> highestScore = @@ -325,7 +337,7 @@ codeOwnersInfo.codeOwners = codeOwnerJsonFactory.create(getFillOptions()).format(sortedAndLimitedCodeOwners); codeOwnersInfo.ownedByAllUsers = ownedByAllUsers.get() ? true : null; - + codeOwnersInfo.codeOwnerConfigs = codeOwnerConfigFileInfosBuilder.build(); ImmutableList<String> debugLogs = debugLogsBuilder.build(); codeOwnersInfo.debugLogs = debug ? debugLogs : null; logger.atFine().log("debug logs: %s", debugLogs); @@ -473,16 +485,15 @@ if (options.contains(ListAccountsOption.ALL_EMAILS)) { // Secondary emails are only visible to users that have the 'Modify Account' global // capability. - permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT); + permissionBackend.currentUser().check(GlobalPermission.VIEW_SECONDARY_EMAILS); fillOptions.add(FillOptions.EMAIL); fillOptions.add(FillOptions.SECONDARY_EMAILS); } return fillOptions; } - private ImmutableList<CodeOwner> sortAndLimit( - R rsrc, ImmutableMap<CodeOwner, Double> scoredCodeOwners) { - return sortCodeOwners(rsrc, seed, scoredCodeOwners).limit(limit).collect(toImmutableList()); + private ImmutableList<CodeOwner> sortAndLimit(ImmutableMap<CodeOwner, Double> scoredCodeOwners) { + return sortCodeOwners(seed, scoredCodeOwners).limit(limit).collect(toImmutableList()); } /** @@ -492,13 +503,12 @@ * * <p>The order of code owners with the same score is random. * - * @param rsrc resource on which this REST endpoint is invoked * @param seed seed that should be used to randomize the order * @param scoredCodeOwners the code owners with their scores * @return the sorted code owners */ private Stream<CodeOwner> sortCodeOwners( - R rsrc, Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) { + Optional<Long> seed, ImmutableMap<CodeOwner, Double> scoredCodeOwners) { return randomizeOrder(seed, scoredCodeOwners.keySet()) .sorted(Comparator.comparingDouble(scoredCodeOwners::get).reversed()); }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java new file mode 100644 index 0000000..fe17a3f --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJson.java
@@ -0,0 +1,119 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.restapi; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImport; +import com.google.gerrit.plugins.codeowners.backend.UnresolvedImportFormatter; +import com.google.inject.Inject; +import java.util.List; + +/** Collection of routines to populate {@link CodeOwnerConfigFileInfo}. */ +public class CodeOwnerConfigFileJson { + private final UnresolvedImportFormatter unresolvedImportFormatter; + + @Inject + CodeOwnerConfigFileJson(UnresolvedImportFormatter unresolvedImportFormatter) { + this.unresolvedImportFormatter = unresolvedImportFormatter; + } + + /** + * Formats the provided code owner config file information as a {@link CodeOwnerConfigFileInfo}. + * + * @param codeOwnerConfigKey the key of the code owner config file as {@link + * CodeOwnerConfigFileInfo} + * @param resolvedImports code owner config files which have been successfully imported directly + * or indirectly + * @param unresolvedImports code owner config files which are imported directly or indirectly but + * couldn't be resolved + * @return the provided {@link com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig.Key} + * as {@link CodeOwnerConfigFileInfo} + */ + public CodeOwnerConfigFileInfo format( + CodeOwnerConfig.Key codeOwnerConfigKey, + List<CodeOwnerConfigImport> resolvedImports, + List<CodeOwnerConfigImport> unresolvedImports) { + requireNonNull(codeOwnerConfigKey, "codeOwnerConfigKey"); + requireNonNull(resolvedImports, "resolvedImports"); + requireNonNull(unresolvedImports, "unresolvedImports"); + + CodeOwnerConfigFileInfo info = new CodeOwnerConfigFileInfo(); + + info.project = codeOwnerConfigKey.project().get(); + info.branch = codeOwnerConfigKey.branchNameKey().branch(); + info.path = unresolvedImportFormatter.getFilePath(codeOwnerConfigKey).toString(); + + ImmutableList<CodeOwnerConfigFileInfo> unresolvedImportInfos = + unresolvedImports.stream() + .filter( + unresolvedImport -> + unresolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey)) + .map( + unresolvedImport -> { + CodeOwnerConfigFileInfo unresolvedCodeOwnerConfigFileInfo = + format( + unresolvedImport.keyOfImportedCodeOwnerConfig(), + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of()); + unresolvedCodeOwnerConfigFileInfo.importMode = + unresolvedImport.codeOwnerConfigReference().importMode(); + unresolvedCodeOwnerConfigFileInfo.unresolvedErrorMessage = + unresolvedImport + .errorMessage() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "unresolved import %s must have an error message", + unresolvedImport))); + return unresolvedCodeOwnerConfigFileInfo; + }) + .collect(toImmutableList()); + info.unresolvedImports = !unresolvedImportInfos.isEmpty() ? unresolvedImportInfos : null; + + ImmutableList<CodeOwnerConfigFileInfo> importInfos = + resolvedImports.stream() + .filter( + resolvedImport -> + resolvedImport.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey)) + .map( + resolvedImport -> { + CodeOwnerConfigFileInfo resolvedCodeOwnerConfigFileInfo = + format( + resolvedImport.keyOfImportedCodeOwnerConfig(), + removeImportEntriesFor(resolvedImports, codeOwnerConfigKey), + removeImportEntriesFor(unresolvedImports, codeOwnerConfigKey)); + resolvedCodeOwnerConfigFileInfo.importMode = + resolvedImport.codeOwnerConfigReference().importMode(); + return resolvedCodeOwnerConfigFileInfo; + }) + .collect(toImmutableList()); + info.imports = !importInfos.isEmpty() ? importInfos : null; + + return info; + } + + private ImmutableList<CodeOwnerConfigImport> removeImportEntriesFor( + List<CodeOwnerConfigImport> imports, CodeOwnerConfig.Key codeOwnerConfigKey) { + return imports.stream() + .filter(i -> !i.keyOfImportingCodeOwnerConfig().equals(codeOwnerConfigKey)) + .collect(toImmutableList()); + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java index e0a9145..85410e9 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInBranch.java
@@ -82,6 +82,7 @@ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, Provider<CodeOwnerResolver> codeOwnerResolver, CodeOwnerJson.Factory codeOwnerJsonFactory, + CodeOwnerConfigFileJson codeOwnerConfigFileJson, GitRepositoryManager repoManager) { super( accountVisibility, @@ -93,7 +94,8 @@ codeOwnersPluginConfiguration, codeOwnerConfigHierarchy, codeOwnerResolver, - codeOwnerJsonFactory); + codeOwnerJsonFactory, + codeOwnerConfigFileJson); this.repoManager = repoManager; }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java index 3ed9a51..b7149c5 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetCodeOwnersForPathInChange.java
@@ -73,7 +73,8 @@ CodeOwnerConfigHierarchy codeOwnerConfigHierarchy, Provider<CodeOwnerResolver> codeOwnerResolver, ServiceUserClassifier serviceUserClassifier, - CodeOwnerJson.Factory codeOwnerJsonFactory) { + CodeOwnerJson.Factory codeOwnerJsonFactory, + CodeOwnerConfigFileJson codeOwnerConfigFileJson) { super( accountVisibility, accounts, @@ -84,7 +85,8 @@ codeOwnersPluginConfiguration, codeOwnerConfigHierarchy, codeOwnerResolver, - codeOwnerJsonFactory); + codeOwnerJsonFactory, + codeOwnerConfigFileJson); this.serviceUserClassifier = serviceUserClassifier; }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java b/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java index c6a447b..bab4ac7 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/GetOwnedPaths.java
@@ -20,6 +20,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.gerrit.entities.Account; +import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.Response; @@ -35,7 +36,6 @@ import com.google.gerrit.server.change.RevisionResource; import com.google.inject.Inject; import java.io.IOException; -import java.nio.file.Path; import org.eclipse.jgit.errors.ConfigInvalidException; import org.kohsuke.args4j.Option; @@ -47,13 +47,20 @@ */ public class GetOwnedPaths implements RestReadView<RevisionResource> { @VisibleForTesting public static final int DEFAULT_LIMIT = 50; - private final AccountResolver accountResolver; private final CodeOwnerApprovalCheck codeOwnerApprovalCheck; private int start; private int limit = DEFAULT_LIMIT; private String user; + private boolean checkReviewers; + + @Option( + name = "--check_reviewers", + usage = "whether or not to also compute which reviewers are owners") + public void setCheckReviewers(boolean checkReviewers) { + this.checkReviewers = checkReviewers; + } @Option( name = "--limit", @@ -99,7 +106,8 @@ revisionResource.getPatchSet(), accountId, start, - limit + 1); + limit + 1, + checkReviewers); OwnedPathsInfo ownedPathsInfo = new OwnedPathsInfo(); ownedPathsInfo.more = ownedChangedFiles.size() > limit ? true : null; @@ -108,10 +116,6 @@ .limit(limit) .map(GetOwnedPaths::toOwnedChangedFileInfo) .collect(toImmutableList()); - ownedPathsInfo.ownedPaths = - OwnedChangedFile.asPathStream(ownedChangedFiles.stream().limit(limit)) - .map(Path::toString) - .collect(toImmutableList()); return Response.ok(ownedPathsInfo); } @@ -149,6 +153,12 @@ OwnedPathInfo info = new OwnedPathInfo(); info.path = ownedPath.path().toString(); info.owned = ownedPath.owned() ? true : null; + info.owners = + ownedPath.owners().stream().map(GetOwnedPaths::toAccountInfo).collect(toImmutableList()); return info; } + + private static AccountInfo toAccountInfo(Account.Id accountId) { + return new AccountInfo(accountId.get()); + } }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java new file mode 100644 index 0000000..4d09a10 --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerConfigFileInfoSubject.java
@@ -0,0 +1,188 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.testing; + +import static com.google.common.truth.Truth.assertAbout; +import static com.google.gerrit.truth.ListSubject.elements; + +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; +import com.google.gerrit.truth.ListSubject; + +/** {@link Subject} for doing assertions on {@link CodeOwnerConfigFileInfo}s. */ +public class CodeOwnerConfigFileInfoSubject extends Subject { + /** + * Starts fluent chain to do assertions on a {@link CodeOwnerConfigFileInfo}. + * + * @param codeOwnerConfigFileInfo the code owner config file info on which assertions should be + * done + * @return the created {@link CodeOwnerConfigFileInfoSubject} + */ + public static CodeOwnerConfigFileInfoSubject assertThat( + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo) { + return assertAbout(codeOwnerConfigFileInfos()).that(codeOwnerConfigFileInfo); + } + + public static Factory<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> + codeOwnerConfigFileInfos() { + return CodeOwnerConfigFileInfoSubject::new; + } + + private final CodeOwnerConfigFileInfo codeOwnerConfigFileInfo; + + private CodeOwnerConfigFileInfoSubject( + FailureMetadata metadata, CodeOwnerConfigFileInfo codeOwnerConfigFileInfo) { + super(metadata, codeOwnerConfigFileInfo); + this.codeOwnerConfigFileInfo = codeOwnerConfigFileInfo; + } + + /** Returns a subject for the project of the code owner config file info. */ + public StringSubject hasProjectThat() { + return check("project()").that(codeOwnerConfigFileInfo().project); + } + + /** Returns a subject for the branch of the code owner config file info. */ + public StringSubject hasBranchThat() { + return check("branch()").that(codeOwnerConfigFileInfo().branch); + } + + /** Returns a subject for the path of the code owner config file info. */ + public StringSubject hasPathThat() { + return check("path()").that(codeOwnerConfigFileInfo().path); + } + + /** + * Returns a {@link ListSubject} for the (resolved) imports of the code owner config file info. + */ + public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> hasImportsThat() { + return check("imports()") + .about(elements()) + .thatCustom(codeOwnerConfigFileInfo().imports, codeOwnerConfigFileInfos()); + } + + /** + * Returns a {@link ListSubject} for the unresolved imports of the code owner config file info. + */ + public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> + hasUnresolvedImportsThat() { + return check("unresolvedimports()") + .about(elements()) + .thatCustom(codeOwnerConfigFileInfo().unresolvedImports, codeOwnerConfigFileInfos()); + } + + /** Returns a subject for the import mode of the code owner config file info. */ + public Subject hasImportModeThat() { + return check("importMode()").that(codeOwnerConfigFileInfo().importMode); + } + + /** Returns a subject for the unresolved error message of the code owner config file info. */ + public Subject hasUnresolvedErrorMessageThat() { + return check("unresolvedErrorMessage()").that(codeOwnerConfigFileInfo().unresolvedErrorMessage); + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertKey( + CodeOwnerBackend codeOwnerBackend, CodeOwnerConfig.Key codeOwnerConfigKey) { + hasProjectThat().isEqualTo(codeOwnerConfigKey.project().get()); + hasBranchThat().isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + hasPathThat().isEqualTo(codeOwnerBackend.getFilePath(codeOwnerConfigKey).toString()); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoResolvedImports() { + hasImportsThat().isNull(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertResolvedImport( + CodeOwnerBackend codeOwnerBackend, + CodeOwnerConfig.Key codeOwnerConfigKey, + CodeOwnerConfigImportMode importMode) { + hasImportsThat().hasSize(1); + CodeOwnerConfigFileInfoSubject subjectForResolvedImport = hasImportsThat().element(0); + subjectForResolvedImport + .assertKey(codeOwnerBackend, codeOwnerConfigKey) + .assertImportMode(importMode) + .assertNoUnresolvedErrorMessage(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoUnresolvedImports() { + hasUnresolvedImportsThat().isNull(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertUnresolvedImport( + CodeOwnerBackend codeOwnerBackend, + CodeOwnerConfig.Key codeOwnerConfigKey, + CodeOwnerConfigImportMode importMode, + String unresolvedErrorMessage) { + hasUnresolvedImportsThat().hasSize(1); + CodeOwnerConfigFileInfoSubject subjectForUnresolvedImport = + hasUnresolvedImportsThat().element(0); + subjectForUnresolvedImport + .assertKey(codeOwnerBackend, codeOwnerConfigKey) + .assertImportMode(importMode) + .assertUnresolvedErrorMessage(unresolvedErrorMessage); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoImports() { + assertNoResolvedImports(); + assertNoUnresolvedImports(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertImportMode(CodeOwnerConfigImportMode importMode) { + hasImportModeThat().isEqualTo(importMode); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoImportMode() { + hasImportModeThat().isNull(); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertUnresolvedErrorMessage( + String unresolvedErrorMessage) { + hasUnresolvedErrorMessageThat().isEqualTo(unresolvedErrorMessage); + return this; + } + + @CanIgnoreReturnValue + public CodeOwnerConfigFileInfoSubject assertNoUnresolvedErrorMessage() { + hasUnresolvedErrorMessageThat().isNull(); + return this; + } + + private CodeOwnerConfigFileInfo codeOwnerConfigFileInfo() { + isNotNull(); + return codeOwnerConfigFileInfo; + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java index 11433e2..9e5fb44 100644 --- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java +++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnersInfoSubject.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.testing; import static com.google.common.truth.Truth.assertAbout; +import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigFileInfoSubject.codeOwnerConfigFileInfos; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.codeOwnerInfos; import static com.google.gerrit.truth.ListSubject.elements; @@ -22,6 +23,7 @@ import com.google.common.truth.FailureMetadata; import com.google.common.truth.IterableSubject; import com.google.common.truth.Subject; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwnerInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo; import com.google.gerrit.truth.ListSubject; @@ -65,6 +67,13 @@ } } + public ListSubject<CodeOwnerConfigFileInfoSubject, CodeOwnerConfigFileInfo> + hasCodeOwnerConfigsThat() { + return check("codeOwnerConfigs()") + .about(elements()) + .thatCustom(codeOwnersInfo().codeOwnerConfigs, codeOwnerConfigFileInfos()); + } + public IterableSubject hasDebugLogsThat() { return check("debugLogs").that(codeOwnersInfo().debugLogs); }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java index 7897058..10276d6 100644 --- a/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java +++ b/java/com/google/gerrit/plugins/codeowners/testing/OwnedChangedFileInfoSubject.java
@@ -15,6 +15,7 @@ package com.google.gerrit.plugins.codeowners.testing; import static com.google.common.truth.Truth.assertAbout; +import static com.google.gerrit.plugins.codeowners.testing.OwnedPathInfoSubject.ownedPathInfos; import com.google.common.truth.FailureMetadata; import com.google.common.truth.Subject; @@ -45,21 +46,31 @@ this.ownedChangedFileInfo = ownedChangedFileInfo; } + public OwnedPathInfoSubject hasNewPathThat(String expectedNewPath) { + check("newPath").that(ownedChangedFileInfo().newPath).isNotNull(); + check("newPath").that(ownedChangedFileInfo().newPath.path).isEqualTo(expectedNewPath); + return check("newPath").about(ownedPathInfos()).that(ownedChangedFileInfo().newPath); + } + public void hasEmptyNewPath() { - check("ownedNewPath").that(ownedChangedFileInfo().newPath).isNull(); + check("newPath").that(ownedChangedFileInfo().newPath).isNull(); + } + + public OwnedPathInfoSubject hasOldPathThat(String expectedOldPath) { + check("oldPath").that(ownedChangedFileInfo().oldPath).isNotNull(); + check("oldPath").that(ownedChangedFileInfo().oldPath.path).isEqualTo(expectedOldPath); + return check("oldPath").about(ownedPathInfos()).that(ownedChangedFileInfo().newPath); } public void hasOwnedNewPath(String expectedOwnedNewPath) { - check("ownedNewPath").that(ownedChangedFileInfo().newPath).isNotNull(); - check("ownedNewPath").that(ownedChangedFileInfo().newPath.path).isEqualTo(expectedOwnedNewPath); + @SuppressWarnings("unused") + var unused = hasNewPathThat(expectedOwnedNewPath); check("ownedNewPath").that(ownedChangedFileInfo().newPath.owned).isTrue(); } public void hasNonOwnedNewPath(String expectedNonOwnedNewPath) { - check("ownedNewPath").that(ownedChangedFileInfo().newPath).isNotNull(); - check("ownedNewPath") - .that(ownedChangedFileInfo().newPath.path) - .isEqualTo(expectedNonOwnedNewPath); + @SuppressWarnings("unused") + var unused = hasNewPathThat(expectedNonOwnedNewPath); check("ownedNewPath").that(ownedChangedFileInfo().newPath.owned).isNull(); } @@ -68,16 +79,14 @@ } public void hasOwnedOldPath(String expectedOwnedOldPath) { - check("ownedOldPath").that(ownedChangedFileInfo().oldPath).isNotNull(); - check("ownedOldPath").that(ownedChangedFileInfo().oldPath.path).isEqualTo(expectedOwnedOldPath); + @SuppressWarnings("unused") + var unused = hasOldPathThat(expectedOwnedOldPath); check("ownedOldPath").that(ownedChangedFileInfo().oldPath.owned).isTrue(); } public void hasNonOwnedOldPath(String expectedNonOwnedOldPath) { - check("ownedOldPath").that(ownedChangedFileInfo().oldPath).isNotNull(); - check("ownedOldPath") - .that(ownedChangedFileInfo().oldPath.path) - .isEqualTo(expectedNonOwnedOldPath); + @SuppressWarnings("unused") + var unused = hasOldPathThat(expectedNonOwnedOldPath); check("ownedOldPath").that(ownedChangedFileInfo().oldPath.owned).isNull(); }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathInfoSubject.java new file mode 100644 index 0000000..7d28060 --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathInfoSubject.java
@@ -0,0 +1,67 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.testing; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.collect.ImmutableList; +import com.google.common.truth.FailureMetadata; +import com.google.common.truth.IterableSubject; +import com.google.common.truth.Subject; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.plugins.codeowners.api.OwnedPathInfo; + +/** {@link Subject} for doing assertions on {@link OwnedPathInfo}s. */ +public class OwnedPathInfoSubject extends Subject { + /** + * Starts fluent chain to do assertions on a {@link OwnedPathInfo}. + * + * @param ownedPathInfo the owned path info on which assertions should be done + * @return the created {@link OwnedPathInfoSubject} + */ + public static OwnedPathInfoSubject assertThat(OwnedPathInfo ownedPathInfo) { + return assertAbout(ownedPathInfos()).that(ownedPathInfo); + } + + public static Factory<OwnedPathInfoSubject, OwnedPathInfo> ownedPathInfos() { + return OwnedPathInfoSubject::new; + } + + private final OwnedPathInfo ownedPathInfo; + + private OwnedPathInfoSubject(FailureMetadata metadata, OwnedPathInfo ownedPathInfo) { + super(metadata, ownedPathInfo); + this.ownedPathInfo = ownedPathInfo; + } + + /** Returns a {@link IterableSubject} for the code owner owners. */ + public IterableSubject hasOwnersThat() { + return check("owners").that(ownedPathInfo().owners); + } + + public void hasOwners(TestAccount... testAccounts) { + ImmutableList.Builder<AccountInfo> accounts = ImmutableList.builder(); + for (TestAccount testAccount : testAccounts) { + accounts.add(new AccountInfo(testAccount.id().get())); + } + hasOwnersThat().containsExactlyElementsIn(accounts.build()); + } + + private OwnedPathInfo ownedPathInfo() { + isNotNull(); + return ownedPathInfo; + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java index 08b762a..58f294f 100644 --- a/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java +++ b/java/com/google/gerrit/plugins/codeowners/testing/OwnedPathsInfoSubject.java
@@ -45,10 +45,6 @@ this.ownedPathsInfo = ownedPathsInfo; } - public IterableSubject hasOwnedPathsThat() { - return check("ownedPaths()").that(ownedPathsInfo().ownedPaths); - } - public IterableSubject hasOwnedChangedFilesThat() { return check("ownedChangedFiles()").that(ownedPathsInfo().ownedChangedFiles); }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/PathCodeOwnerStatusSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/PathCodeOwnerStatusSubject.java index 5e2f58c..5fbda01 100644 --- a/java/com/google/gerrit/plugins/codeowners/testing/PathCodeOwnerStatusSubject.java +++ b/java/com/google/gerrit/plugins/codeowners/testing/PathCodeOwnerStatusSubject.java
@@ -16,10 +16,14 @@ import static com.google.common.truth.PathSubject.paths; +import com.google.common.collect.ImmutableSet; import com.google.common.truth.ComparableSubject; import com.google.common.truth.FailureMetadata; +import com.google.common.truth.IterableSubject; import com.google.common.truth.PathSubject; import com.google.common.truth.Subject; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.entities.Account; import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnerStatus; import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus; @@ -44,12 +48,25 @@ /** Returns a {@link ComparableSubject} for the path. */ public PathSubject hasPathThat() { - return check("path()").about(paths()).that(pathCodeOwnerStatus().path()); + return check("path").about(paths()).that(pathCodeOwnerStatus().path()); } /** Returns a {@link ComparableSubject} for the code owner status. */ public ComparableSubject<CodeOwnerStatus> hasStatusThat() { - return check("status()").that(pathCodeOwnerStatus().status()); + return check("status").that(pathCodeOwnerStatus().status()); + } + + /** Returns a {@link ComparableSubject} for the code owner owners. */ + public IterableSubject hasOwnersThat() { + return check("owners.get").that(pathCodeOwnerStatus.owners().get()); + } + + public void hasOwners(TestAccount... testAccounts) { + ImmutableSet.Builder<Account.Id> accounts = ImmutableSet.builder(); + for (TestAccount testAccount : testAccounts) { + accounts.add(testAccount.id()); + } + hasOwnersThat().containsExactlyElementsIn(accounts.build()); } private PathCodeOwnerStatus pathCodeOwnerStatus() {
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java index f763a2c..0083497 100644 --- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java +++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -31,6 +31,7 @@ import com.google.gerrit.entities.PatchSet; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.plugins.codeowners.backend.ChangedFiles; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend; @@ -53,6 +54,7 @@ import com.google.gerrit.plugins.codeowners.util.JgitPath; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.config.UrlFormatter; import com.google.gerrit.server.events.CommitReceivedEvent; import com.google.gerrit.server.events.RefReceivedEvent; import com.google.gerrit.server.git.CodeReviewCommit; @@ -151,6 +153,7 @@ private final IdentifiedUser.GenericFactory userFactory; private final SkipCodeOwnerConfigValidationPushOption skipCodeOwnerConfigValidationPushOption; private final CodeOwnerMetrics codeOwnerMetrics; + private final DynamicItem<UrlFormatter> urlFormatter; @Inject CodeOwnerConfigValidator( @@ -165,7 +168,8 @@ PatchSetUtil patchSetUtil, IdentifiedUser.GenericFactory userFactory, SkipCodeOwnerConfigValidationPushOption skipCodeOwnerConfigValidationPushOption, - CodeOwnerMetrics codeOwnerMetrics) { + CodeOwnerMetrics codeOwnerMetrics, + DynamicItem<UrlFormatter> urlFormatter) { this.pluginName = pluginName; this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.repoManager = repoManager; @@ -178,6 +182,7 @@ this.userFactory = userFactory; this.skipCodeOwnerConfigValidationPushOption = skipCodeOwnerConfigValidationPushOption; this.codeOwnerMetrics = codeOwnerMetrics; + this.urlFormatter = urlFormatter; } @Override @@ -521,6 +526,7 @@ // validate the code owner config files return Optional.of( ValidationResult.create( + urlFormatter, pluginName, codeOwnerConfigFilesToValidate.stream() .flatMap( @@ -1326,12 +1332,23 @@ } static ValidationResult create( - String pluginName, Stream<CommitValidationMessage> validationMessagesStream) { + DynamicItem<UrlFormatter> urlFormatter, + String pluginName, + Stream<CommitValidationMessage> validationMessagesStream) { ImmutableList<CommitValidationMessage> validationMessages = validationMessagesStream.collect(toImmutableList()); + Optional<String> helpPage = + urlFormatter + .get() + .getPluginDocUrl( + pluginName, "validation.html", "validation-checks-for-code-owner-config-files"); return new AutoValue_CodeOwnerConfigValidator_ValidationResult( pluginName, - validationMessages.isEmpty() ? NO_ISSUES_MSG : INVALID_MSG, + validationMessages.isEmpty() + ? NO_ISSUES_MSG + : (helpPage.isPresent() + ? String.format("%s (see %s for help)", INVALID_MSG, helpPage.get()) + : INVALID_MSG), validationMessages); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java index 74c9eb2..1ca5fd9 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/AbstractGetCodeOwnersForPathIT.java
@@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; +import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigFileInfoSubject.assertThat; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountName; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat; @@ -25,6 +26,7 @@ import static java.util.stream.Collectors.toList; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.account.AccountOperations; @@ -44,13 +46,16 @@ import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; import com.google.gerrit.plugins.codeowners.api.CodeOwners; import com.google.gerrit.plugins.codeowners.api.CodeOwnersInfo; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet; +import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig; import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerCapability; import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnersForPathInBranch; import com.google.inject.Inject; @@ -77,11 +82,14 @@ @Inject private GroupOperations groupOperations; @Inject private ProjectOperations projectOperations; + private CodeOwnerBackend backend; + protected TestPathExpressions testPathExpressions; @Before public void setup() throws Exception { testPathExpressions = plugin.getSysInjector().getInstance(TestPathExpressions.class); + backend = plugin.getSysInjector().getInstance(BackendConfig.class).getDefaultBackend(); } /** Must return the {@link CodeOwners} API against which the tests should be run. */ @@ -98,7 +106,10 @@ @Test public void getCodeOwnersWhenNoCodeOwnerConfigsExist() throws Exception { - assertThat(queryCodeOwners("/foo/bar/baz.md")).hasCodeOwnersThat().isEmpty(); + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty(); + assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty(); } @Test @@ -115,6 +126,7 @@ CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty(); assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty(); } @Test @@ -136,29 +148,32 @@ private void testGetCodeOwners(boolean useAbsolutePath) throws Exception { TestAccount user2 = accountCreator.user2(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(admin.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(admin.email()) + .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(user.email()) + .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user2.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey3 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(user2.email()) + .create(); CodeOwnersInfo codeOwnersInfo = queryCodeOwners(useAbsolutePath ? "/foo/bar/baz.md" : "foo/bar/baz.md"); @@ -172,6 +187,27 @@ .comparingElementsUsing(hasAccountName()) .containsExactly(null, null, null); assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(3); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo3 = codeOwnersInfo.codeOwnerConfigs.get(0); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo2 = codeOwnersInfo.codeOwnerConfigs.get(1); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo1 = codeOwnersInfo.codeOwnerConfigs.get(2); + assertThat(codeOwnerConfigFileInfo3) + .assertKey(backend, codeOwnerConfigKey3) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnerConfigFileInfo2) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnerConfigFileInfo1) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo).hasDebugLogsThat().isNull(); } @@ -184,24 +220,26 @@ // 1. code owner config that makes "user2" a code owner, inheriting code owners from parent code // owner configs is enabled by default - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user2.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(user2.email()) + .create(); // 2. code owner config that makes "user" a code owner, code owners from parent code owner // configs are ignored - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/") - .ignoreParentCodeOwners() - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .ignoreParentCodeOwners() + .addCodeOwnerEmail(user.email()) + .create(); // 3. code owner config that makes "admin" a code owner and assigns code ownership to all users, // but for this test this code owner config is ignored, since the 2. code owner config ignores @@ -227,36 +265,71 @@ .containsExactly(user2.id(), user.id()) .inOrder(); assertThat(codeOwnersInfo).hasOwnedByAllUsersThat().isNull(); + + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo1 = codeOwnersInfo.codeOwnerConfigs.get(0); + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo2 = codeOwnersInfo.codeOwnerConfigs.get(1); + assertThat(codeOwnerConfigFileInfo1) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnerConfigFileInfo2) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test public void getPerFileCodeOwners() throws Exception { - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression(testPathExpressions.matchFileType("txt")) - .addCodeOwnerEmail(admin.email()) - .build()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression(testPathExpressions.matchFileType("md")) - .addCodeOwnerEmail(user.email()) - .build()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression(testPathExpressions.matchFileType("txt")) + .addCodeOwnerEmail(admin.email()) + .build()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression(testPathExpressions.matchFileType("md")) + .addCodeOwnerEmail(user.email()) + .build()) + .create(); - assertThat(queryCodeOwners("/foo/bar/config.txt")) + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/config.txt"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(admin.id()); - assertThat(queryCodeOwners("/foo/bar/baz.md")) + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + + codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(user.id()); - assertThat(queryCodeOwners("/foo/bar/main.config")).hasCodeOwnersThat().isEmpty(); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + + codeOwnersInfo = queryCodeOwners("/foo/bar/main.config"); + assertThat(codeOwnersInfo).hasCodeOwnersThat().isEmpty(); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test @@ -314,8 +387,9 @@ } @Test - public void cannotGetCodeOwnersWithSecondaryEmailsWithoutModifyAccountCapability() - throws Exception { + public void + cannotGetCodeOwnersWithSecondaryEmailsWithoutViewSecondaryEmailAndWithoutModifyAccountCapability() + throws Exception { // create a code owner config codeOwnerConfigOperations .newCodeOwnerConfig() @@ -339,7 +413,7 @@ queryCodeOwners( getCodeOwnersApi().query().withOptions(ListAccountsOption.ALL_EMAILS), "/foo/bar/baz.md")); - assertThat(exception).hasMessageThat().isEqualTo("modify account not permitted"); + assertThat(exception).hasMessageThat().isEqualTo("view secondary emails not permitted"); } @Test @@ -548,22 +622,24 @@ TestAccount user2 = accountCreator.user2(); // create some code owner configs - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(admin.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail(admin.email()) + .create(); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/bar/") - .addCodeOwnerEmail(user.email()) - .addCodeOwnerEmail(user2.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(user.email()) + .addCodeOwnerEmail(user2.email()) + .create(); // get code owners with different limits CodeOwnersInfo codeOwnersInfo = @@ -576,18 +652,51 @@ .element(0) .hasAccountIdThat() .isAnyOf(user.id(), user2.id()); + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(0)) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(1)) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query().withLimit(2), "/foo/bar/baz.md"); assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(user.id(), user2.id()); + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(0)) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(1)) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); codeOwnersInfo = getCodeOwnersApi().query().withLimit(3).get("/foo/bar/baz.md"); assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(admin.id(), user.id(), user2.id()); + assertThat(codeOwnersInfo.codeOwnerConfigs).hasSize(2); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(0)) + .assertKey(backend, codeOwnerConfigKey2) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); + assertThat(codeOwnersInfo.codeOwnerConfigs.get(1)) + .assertKey(backend, codeOwnerConfigKey1) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test @@ -632,10 +741,12 @@ public void getGlobalCodeOwners() throws Exception { TestAccount globalOwner = accountCreator.create("global_owner", "global.owner@example.com", "Global Owner", null); - assertThat(queryCodeOwners("/foo/bar/baz.md")) + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(globalOwner.id()); + assertThat(codeOwnersInfo).hasCodeOwnerConfigsThat().isEmpty(); } @Test @@ -767,18 +878,25 @@ @Test public void getDefaultCodeOwners() throws Exception { // Create default code owner config file in refs/meta/config. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(RefNames.REFS_CONFIG) - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch(RefNames.REFS_CONFIG) + .folderPath("/") + .addCodeOwnerEmail(user.email()) + .create(); - assertThat(queryCodeOwners("/foo/bar/baz.md")) + CodeOwnersInfo codeOwnersInfo = queryCodeOwners("/foo/bar/baz.md"); + assertThat(codeOwnersInfo) .hasCodeOwnersThat() .comparingElementsUsing(hasAccountId()) .containsExactly(user.id()); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, codeOwnerConfigKey) + .assertNoImports() + .assertNoImportMode() + .assertNoUnresolvedErrorMessage(); } @Test @@ -1643,4 +1761,168 @@ } assertThat(foundDifferentOrder).isTrue(); } + + @Test + public void getCodeOwnersWithUnresolvedImport() throws Exception { + skipTestIfImportsNotSupportedByCodeOwnersBackend(); + + Project.NameKey nonExistingProject = Project.nameKey("non-existing"); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(nonExistingProject, "master", "/", "OWNERS"); + CodeOwnerConfigReference nonResolvableCodeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addImport(nonResolvableCodeOwnerConfigReference) + .addCodeOwnerEmail(admin.email()) + .addCodeOwnerEmail(user.email()) + .create(); + + String path = "/foo/bar/baz.md"; + CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(user.id(), admin.id()); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, keyOfImportingCodeOwnerConfig) + .assertNoResolvedImports() + .assertUnresolvedImport( + backend, + keyOfImportedCodeOwnerConfig, + nonResolvableCodeOwnerConfigReference.importMode(), + String.format("project %s not found", nonExistingProject)); + } + + @Test + public void getCodeOwnersWithResolvedImport() throws Exception { + skipTestIfImportsNotSupportedByCodeOwnersBackend(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addImport(codeOwnerConfigReference) + .addCodeOwnerEmail(user.email()) + .create(); + + String path = "/foo/bar/baz.md"; + CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(user.id(), admin.id()); + assertThat(Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs)) + .assertKey(backend, keyOfImportingCodeOwnerConfig) + .assertNoUnresolvedImports() + .assertResolvedImport( + backend, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference.importMode()); + } + + @Test + public void getCodeOwnersWithNestedImport() throws Exception { + skipTestIfImportsNotSupportedByCodeOwnersBackend(); + + TestAccount user2 = accountCreator.user2(); + + Project.NameKey nonExistingProject = Project.nameKey("non-existing"); + CodeOwnerConfig.Key keyOfUnresolvableCodeOwnerConfig = + CodeOwnerConfig.Key.create(nonExistingProject, "master", "/", "OWNERS"); + CodeOwnerConfigReference unresolvableCodeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfUnresolvableCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .addImport(unresolvableCodeOwnerConfigReference) + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .addImport(codeOwnerConfigReference2) + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig1); + + CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addImport(codeOwnerConfigReference1) + .addCodeOwnerEmail(user.email()) + .create(); + + String path = "/foo/bar/baz.md"; + CodeOwnersInfo codeOwnersInfo = queryCodeOwners(getCodeOwnersApi().query(), path); + assertThat(codeOwnersInfo) + .hasCodeOwnersThat() + .comparingElementsUsing(hasAccountId()) + .containsExactly(user.id(), user2.id(), admin.id()); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + Iterables.getOnlyElement(codeOwnersInfo.codeOwnerConfigs); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, keyOfImportingCodeOwnerConfig) + .assertNoImportMode() + .assertNoUnresolvedErrorMessage() + .assertNoUnresolvedImports() + .assertResolvedImport( + backend, keyOfImportedCodeOwnerConfig1, codeOwnerConfigReference1.importMode()); + + codeOwnerConfigFileInfo = Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, keyOfImportedCodeOwnerConfig1) + .assertImportMode(codeOwnerConfigReference1.importMode()) + .assertNoUnresolvedErrorMessage() + .assertNoUnresolvedImports() + .assertResolvedImport( + backend, keyOfImportedCodeOwnerConfig2, codeOwnerConfigReference2.importMode()); + + codeOwnerConfigFileInfo = Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(codeOwnerConfigFileInfo) + .assertKey(backend, keyOfImportedCodeOwnerConfig2) + .assertImportMode(codeOwnerConfigReference2.importMode()) + .assertNoUnresolvedErrorMessage() + .assertNoResolvedImports() + .assertUnresolvedImport( + backend, + keyOfUnresolvableCodeOwnerConfig, + unresolvableCodeOwnerConfigReference.importMode(), + String.format("project %s not found", nonExistingProject)); + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java index 83eb13f..66928ef 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; @@ -100,7 +101,7 @@ @Test public void checkCodeOwnerForSymbolicRefPointingToAnUnbornBranch() throws Exception { try (Repository repo = repoManager.openRepository(project)) { - repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"); + testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing")); } RestResponse response = adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java index 5b7ab3d..51b52da 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -40,6 +40,7 @@ import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.api.changes.CherryPickInput; import com.google.gerrit.extensions.api.changes.RebaseInput; +import com.google.gerrit.extensions.api.changes.RevertInput; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.client.ChangeStatus; @@ -47,6 +48,7 @@ import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; import com.google.gerrit.extensions.common.MergeInput; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.git.ObjectIds; @@ -67,6 +69,7 @@ import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy; import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationCapability; import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationPushOption; +import com.google.gerrit.server.config.UrlFormatter; import com.google.gerrit.server.submit.IntegrationConflictException; import com.google.inject.Inject; import java.nio.file.Path; @@ -91,6 +94,7 @@ @Inject private RequestScopeOperations requestScopeOperations; @Inject private ProjectOperations projectOperations; + @Inject private DynamicItem<UrlFormatter> urlFormatter; private FindOwnersCodeOwnerConfigParser findOwnersCodeOwnerConfigParser; private ProtoCodeOwnerConfigParser protoCodeOwnerConfigParser; @@ -730,9 +734,11 @@ .isEqualTo( String.format( "Failed to submit 1 change due to the following problems:\n" - + "Change %d: [code-owners] invalid code owner config files:\n" + + "Change %d: [code-owners] invalid code owner config files" + + " (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", r.getChange().getId().get(), + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName())); @@ -2465,6 +2471,114 @@ } @Test + public void skipValidationForRevert() throws Exception { + // Make admin a code owner so that admin can code-owner approve changes. + setAsRootCodeOwners(admin); + + // Create a code owner config file with a non-resolvable code owner. + CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/foo/"); + String unknownEmail = "non-existing-email@example.com"; + PushOneCommit push = + pushFactory.create( + admin.newIdent(), + testRepo, + "Add code owner config file with non-resolvable code owner", + codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(), + format( + CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION) + .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail)) + .build())); + push.setPushOptions( + ImmutableList.of( + String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME))); + PushOneCommit.Result r = push.to("refs/for/master"); + assertOkWithHints( + r, + "skipping validation of code owner config files", + String.format( + "the validation is skipped due to the --code-owners~%s push option", + SkipCodeOwnerConfigValidationPushOption.NAME)); + approve(r.getChangeId()); + gApi.changes().id(r.getChangeId()).current().submit(); + assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED); + + // Fix the code owner config file. + PushOneCommit.Result r2 = + createChange( + "Fix code owner config file", + codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(), + format( + CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION) + .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email())) + .build())); + assertOkWithHints(r2, "code owner config files validated, no issues found"); + approve(r2.getChangeId()); + gApi.changes().id(r2.getChangeId()).current().submit(); + assertThat(gApi.changes().id(r2.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED); + + // Try reverting the fix, expect failure due to invalid code owner config file. + RevertInput revertInput = new RevertInput(); + ResourceConflictException exception = + assertThrows( + ResourceConflictException.class, + () -> gApi.changes().id(r2.getChangeId()).revert(revertInput)); + assertThat(exception) + .hasMessageThat() + .isEqualTo( + String.format( + "[code-owners] invalid code owner config files (see %s for help):\n" + + " [code-owners] code owner email '%s' in '%s' cannot be resolved for %s", + getHelpPage(), + unknownEmail, + codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), + admin.username())); + + // Revert the fix and skip the validation + revertInput.validationOptions = + ImmutableMap.of( + String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true"); + gApi.changes().id(r.getChangeId()).revert(revertInput); + } + + @Test + public void userWithoutCapabilitySkipValidationCannotSkipValidationWithRevert() throws Exception { + // Make admin a code owner so that admin can code-owner approve changes. + setAsRootCodeOwners(admin); + + // Create a code owner config without issues. + CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/foo/"); + PushOneCommit.Result r = + createChange( + "Add code owners", + codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(), + format( + CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION) + .addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email())) + .build())); + assertOkWithHints(r, "code owner config files validated, no issues found"); + approve(r.getChangeId()); + gApi.changes().id(r.getChangeId()).current().submit(); + + // Trying to use the skip validation option on revert is rejected because user has no permission + // to skip the validation. + requestScopeOperations.setApiUser(user.id()); + RevertInput revertInput = new RevertInput(); + revertInput.validationOptions = + ImmutableMap.of( + String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true"); + ResourceConflictException resourceConflictException = + assertThrows( + ResourceConflictException.class, + () -> gApi.changes().id(r.getChangeId()).revert(revertInput)); + assertThat(resourceConflictException) + .hasMessageThat() + .contains( + String.format( + "[code-owners] %s for plugin code-owners not permitted", + SkipCodeOwnerConfigValidationCapability.ID)); + } + + @Test @GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true") public void cannotCreateBranchWithInvalidCodeOwnerConfigFileViaRestApi() throws Exception { // Add a non code owner config file to verify that it is not validated as code owner config file @@ -2505,10 +2619,11 @@ .isEqualTo( String.format( "Validation for creation of ref 'refs/heads/new' in project %s failed:\n" - + "[code-owners] invalid code owner config files:\n" + + "[code-owners] invalid code owner config files (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", project, + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName(), @@ -2540,7 +2655,9 @@ () -> gApi.projects().name(project.get()).branch(input.ref).create(input)); assertThat(exception) .hasMessageThat() - .contains("[code-owners] invalid code owner config files:"); + .contains( + String.format( + "[code-owners] invalid code owner config files (see %s for help):", getHelpPage())); input.validationOptions = ImmutableMap.of( @@ -2635,10 +2752,11 @@ "refs/heads/new", String.format( "Validation for creation of ref 'refs/heads/new' in project %s failed:\n" - + "[code-owners] invalid code owner config files:\n" + + "[code-owners] invalid code owner config files (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", project, + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName(), @@ -2677,9 +2795,10 @@ "refs/heads/new", String.format( "Validation for creation of ref 'refs/heads/new' in project %s failed:\n" - + "[code-owners] invalid code owner config files:\n" + + "[code-owners] invalid code owner config files (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", project, + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName())); @@ -2797,9 +2916,10 @@ assertThat(r.getMessages()) .contains( String.format( - "ERROR: [code-owners] invalid code owner config files\n" + "ERROR: [code-owners] invalid code owner config files (see %s for help)\n" + "ERROR: [code-owners] code owner email '%s' in '%s' cannot be resolved for" + " %s", + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName())); @@ -3208,4 +3328,10 @@ pushResult.assertNotMessage("warning"); pushResult.assertNotMessage("hint"); } + + private String getHelpPage() { + return urlFormatter.get().getWebUrl().get() + + "plugins/code-owners/Documentation/validation.html" + + "#validation-checks-for-code-owner-config-files"; + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java index b92d0e0..75843f3 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorOnSubmitIT.java
@@ -26,6 +26,7 @@ import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.entities.Permission; import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; @@ -35,6 +36,7 @@ import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersCodeOwnerConfigParser; import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend; import com.google.gerrit.plugins.codeowners.backend.proto.ProtoCodeOwnerConfigParser; +import com.google.gerrit.server.config.UrlFormatter; import com.google.inject.Inject; import org.eclipse.jgit.lib.ObjectId; import org.junit.Before; @@ -50,6 +52,7 @@ @Inject private RequestScopeOperations requestScopeOperations; @Inject private ProjectOperations projectOperations; + @Inject private DynamicItem<UrlFormatter> urlFormatter; private FindOwnersCodeOwnerConfigParser findOwnersCodeOwnerConfigParser; private ProtoCodeOwnerConfigParser protoCodeOwnerConfigParser; @@ -126,9 +129,11 @@ .isEqualTo( String.format( "Failed to submit 1 change due to the following problems:\n" - + "Change %d: [code-owners] invalid code owner config files:\n" + + "Change %d: [code-owners] invalid code owner config files" + + " (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", r.getChange().getId().get(), + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName())); @@ -181,9 +186,11 @@ .isEqualTo( String.format( "Failed to submit 1 change due to the following problems:\n" - + "Change %d: [code-owners] invalid code owner config files:\n" + + "Change %d: [code-owners] invalid code owner config files" + + " (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", r.getChange().getId().get(), + getHelpPage(), admin.email(), codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), identifiedUserFactory.create(user2.id()).getLoggableName())); @@ -309,9 +316,11 @@ .isEqualTo( String.format( "Failed to submit 1 change due to the following problems:\n" - + "Change %d: [code-owners] invalid code owner config files:\n" + + "Change %d: [code-owners] invalid code owner config files" + + " (see %s for help):\n" + " ERROR: code owner email '%s' in '%s' cannot be resolved for %s", r.getChange().getId().get(), + getHelpPage(), unknownEmail, codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(), identifiedUserFactory.create(admin.id()).getLoggableName())); @@ -375,4 +384,10 @@ "unknown code owner backend: %s", backendConfig.getDefaultBackend().getClass().getName())); } + + private String getHelpPage() { + return urlFormatter.get().getWebUrl().get() + + "plugins/code-owners/Documentation/validation.html" + + "#validation-checks-for-code-owner-config-files"; + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java index f3e7d1b..2d2c8c7 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerSubmitRuleIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; @@ -716,7 +717,7 @@ try (Repository repo = repoManager.openRepository(project)) { RefUpdate ru = repo.updateRef(RefNames.refsCacheAutomerge(mergeCommit.name())); ru.setForceUpdate(true); - assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); + assertThat(testRefAction(() -> ru.delete())).isEqualTo(RefUpdate.Result.FORCED); assertThat(repo.exactRef(RefNames.refsCacheAutomerge(mergeCommit.name()))).isNull(); } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java index 70fb164..f7385ad 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.entities.BranchNameKey; @@ -453,6 +454,7 @@ }); } + @CanIgnoreReturnValue private <T> T assertAsync(Callable<T> assertion) throws Exception { return RetryerBuilder.<T>newBuilder() .retryIfException(t -> true)
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java index b2a6630..b0caf0a 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersPluginConfigValidatorIT.java
@@ -631,6 +631,27 @@ } @Test + public void configureEnableAsyncMessageOnCodeOwnerApproval() throws Exception { + fetchRefsMetaConfig(); + + Config cfg = new Config(); + cfg.setBoolean( + CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS, + /* subsection= */ null, + GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + false); + setCodeOwnersConfig(cfg); + + PushResult r = pushRefsMetaConfig(); + assertThat(r.getRemoteUpdate(RefNames.REFS_CONFIG).getStatus()).isEqualTo(Status.OK); + assertThat( + codeOwnersPluginConfiguration + .getProjectConfig(project) + .enableAsyncMessageOnCodeOwnerApproval()) + .isFalse(); + } + + @Test public void cannotSetInvalidMaxPathsInChangeMessages() throws Exception { fetchRefsMetaConfig();
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java index 5ddd85f..843b656 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnerConfigForPathInBranchIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerConfigInfoSubject.assertThatOptional; import static com.google.gerrit.testing.GerritJUnit.assertThrows; @@ -54,7 +55,7 @@ @GerritConfig(name = "plugin.code-owners.enableExperimentalRestEndpoints", value = "true") public void getCodeOwnerConfigFromSymbolicRefPointingToAnUnbornBranch() throws Exception { try (Repository repo = repoManager.openRepository(project)) { - repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"); + testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing")); } RestResponse response = adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java index a78d420..34d2271 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetCodeOwnersForPathInBranchIT.java
@@ -14,6 +14,7 @@ package com.google.gerrit.plugins.codeowners.acceptance.api; +import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerInfoSubject.hasAccountId; import static com.google.gerrit.plugins.codeowners.testing.CodeOwnersInfoSubject.assertThat; @@ -73,7 +74,7 @@ @Test public void getCodeOwnersFromSymbolicRefPointingToAnUnbornBranch() throws Exception { try (Repository repo = repoManager.openRepository(project)) { - repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"); + testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing")); } RestResponse response = adminRestSession.get(
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java index 5b19b9a..54a94a1 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/GetOwnedPathsIT.java
@@ -34,9 +34,6 @@ import com.google.gerrit.plugins.codeowners.restapi.GetOwnedPaths; import com.google.gerrit.plugins.codeowners.util.JgitPath; import com.google.inject.Inject; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import org.junit.Test; /** @@ -144,8 +141,6 @@ assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasEmptyOldPath(); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasEmptyOldPath(); - - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2).inOrder(); assertThat(ownedPathsInfo).hasMoreThat().isNull(); } @@ -198,7 +193,6 @@ assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasEmptyNewPath(); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedOldPath(path2); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2).inOrder(); assertThat(ownedPathsInfo).hasMoreThat().isNull(); } @@ -279,10 +273,6 @@ assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasNonOwnedNewPath(newPath3); assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedOldPath(oldPath3); - List<String> ownedPaths = Arrays.asList(newPath1, oldPath1, newPath2, oldPath3); - Collections.sort(ownedPaths); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths).inOrder(); - assertThat(ownedPathsInfo).hasMoreThat().isNull(); } @@ -305,7 +295,9 @@ OwnedPathsInfo ownedPathsInfo = changeCodeOwnersApiFactory.change(changeId).current().getOwnedPaths().forUser("self").get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2).inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2); } @Test @@ -335,7 +327,135 @@ .getOwnedPaths() .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().isEmpty(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().isEmpty(); + } + + @Test + public void getOwnedPathsForNonCodeOwnerWithCodeOwnerAsReviewer() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsCodeOwners("/foo/", codeOwner); + + String path1 = "/foo/bar/baz.md"; + String path2 = "/foo/baz/bar.md"; + String path3 = "/bar/foo.md"; + + String changeId = + createChange( + "test change", + ImmutableMap.of( + JgitPath.of(path1).get(), + "file content", + JgitPath.of(path2).get(), + "file content", + JgitPath.of(path3).get(), + "file content")) + .getChangeId(); + + // Add a Code-Review+1 (= code owner approval) from the code owner. + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + OwnedPathsInfo ownedPathsInfo = + changeCodeOwnersApiFactory + .change(changeId) + .current() + .getOwnedPaths() + .forUser(user.email()) + .get(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().isEmpty(); + } + + @Test + public void getOwnedPathsForNonCodeOwnerWithCodeOwnerAsReviewerCheckReviewers() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsCodeOwners("/foo/", codeOwner); + + String path1 = "/foo/bar/baz.md"; + String path2 = "/foo/baz/bar.md"; + String path3 = "/bar/foo.md"; + + String changeId = + createChange( + "test change", + ImmutableMap.of( + JgitPath.of(path1).get(), + "file content", + JgitPath.of(path2).get(), + "file content", + JgitPath.of(path3).get(), + "file content")) + .getChangeId(); + + // Add a Code-Review+1 (= code owner approval) from the code owner. + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + OwnedPathsInfo ownedPathsInfo = + changeCodeOwnersApiFactory + .change(changeId) + .current() + .getOwnedPaths() + .forUser(user.email()) + .withCheckReviewers(true) + .get(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasNewPathThat(path1).hasOwners(codeOwner); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNewPathThat(path2).hasOwners(codeOwner); + } + + @Test + public void getOwnedPathsForCodeOwnerWithCodeOwnerAsReviewerCheckReviewers() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsCodeOwners("/foo/bar/", codeOwner); + TestAccount codeOwner2 = + accountCreator.create( + "codeOwner2", "codeOwner2@example.com", "CodeOwner2", /* displayName= */ null); + setAsCodeOwners("/foo/", codeOwner2); + setAsCodeOwners("/bar/", user); + + String path1 = "/foo/bar/baz.md"; + String path2 = "/foo/baz/bar.md"; + String path3 = "/bar/foo.md"; + + String changeId = + createChange( + "test change", + ImmutableMap.of( + JgitPath.of(path1).get(), + "file content", + JgitPath.of(path2).get(), + "file content", + JgitPath.of(path3).get(), + "file content")) + .getChangeId(); + + // Add a Code-Review+1 (= code owner approval) from the code owner. + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + // Add a Code-Review+1 (= code owner approval) from the code owner. + requestScopeOperations.setApiUser(codeOwner2.id()); + recommend(changeId); + + OwnedPathsInfo ownedPathsInfo = + changeCodeOwnersApiFactory + .change(changeId) + .current() + .getOwnedPaths() + .forUser(user.email()) + .withCheckReviewers(true) + .get(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasNewPathThat(path3).hasOwners(user); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)) + .hasNewPathThat(path1) + .hasOwners(codeOwner, codeOwner2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasNewPathThat(path2).hasOwners(codeOwner2); } @Test @@ -369,10 +489,11 @@ .withStart(0) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo) - .hasOwnedPathsThat() - .containsExactly(path1, path2, path3, path4) - .inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(4); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedNewPath(path3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(3)).hasOwnedNewPath(path4); ownedPathsInfo = changeCodeOwnersApiFactory @@ -382,7 +503,10 @@ .withStart(1) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path2, path3, path4).inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedNewPath(path4); ownedPathsInfo = changeCodeOwnersApiFactory @@ -392,7 +516,9 @@ .withStart(2) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path3, path4).inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path4); ownedPathsInfo = changeCodeOwnersApiFactory @@ -402,7 +528,8 @@ .withStart(3) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path4); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path4); ownedPathsInfo = changeCodeOwnersApiFactory @@ -412,7 +539,7 @@ .withStart(4) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().isEmpty(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().isEmpty(); } @Test @@ -446,7 +573,8 @@ .withLimit(1) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1); assertThat(ownedPathsInfo).hasMoreThat().isTrue(); ownedPathsInfo = @@ -457,7 +585,9 @@ .withLimit(2) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2).inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2); assertThat(ownedPathsInfo).hasMoreThat().isTrue(); ownedPathsInfo = @@ -468,7 +598,10 @@ .withLimit(3) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path1, path2, path3).inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedNewPath(path3); assertThat(ownedPathsInfo).hasMoreThat().isTrue(); ownedPathsInfo = @@ -479,10 +612,11 @@ .withLimit(4) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo) - .hasOwnedPathsThat() - .containsExactly(path1, path2, path3, path4) - .inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(4); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path1); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedNewPath(path3); + assertThat(ownedPathsInfo.ownedChangedFiles.get(3)).hasOwnedNewPath(path4); assertThat(ownedPathsInfo).hasMoreThat().isNull(); } @@ -518,7 +652,9 @@ .withLimit(2) .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactly(path2, path3).inOrder(); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(path2); + assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(path3); assertThat(ownedPathsInfo).hasMoreThat().isTrue(); } @@ -594,9 +730,6 @@ assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(1); assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedNewPath(newPath1); assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(oldPath1); - List<String> ownedPaths = Arrays.asList(newPath1, oldPath1); - Collections.sort(ownedPaths); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths); assertThat(ownedPathsInfo).hasMoreThat().isTrue(); ownedPathsInfo = @@ -612,9 +745,6 @@ assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasOwnedOldPath(oldPath1); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedNewPath(newPath2); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedOldPath(oldPath2); - ownedPaths = Arrays.asList(newPath1, oldPath1, newPath2); - Collections.sort(ownedPaths); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths).inOrder(); assertThat(ownedPathsInfo).hasMoreThat().isTrue(); ownedPathsInfo = @@ -632,9 +762,6 @@ assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedOldPath(oldPath2); assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasNonOwnedNewPath(newPath3); assertThat(ownedPathsInfo.ownedChangedFiles.get(2)).hasOwnedOldPath(oldPath3); - ownedPaths = Arrays.asList(newPath1, oldPath1, newPath2, oldPath3); - Collections.sort(ownedPaths); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths).inOrder(); assertThat(ownedPathsInfo).hasMoreThat().isNull(); } @@ -713,9 +840,6 @@ assertThat(ownedPathsInfo.ownedChangedFiles.get(0)).hasNonOwnedOldPath(oldPath2); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasNonOwnedNewPath(newPath3); assertThat(ownedPathsInfo.ownedChangedFiles.get(1)).hasOwnedOldPath(oldPath3); - List<String> ownedPaths = Arrays.asList(newPath2, oldPath3); - Collections.sort(ownedPaths); - assertThat(ownedPathsInfo).hasOwnedPathsThat().containsExactlyElementsIn(ownedPaths); assertThat(ownedPathsInfo).hasMoreThat().isNull(); } @@ -737,7 +861,7 @@ .getOwnedPaths() .forUser(user.email()) .get(); - assertThat(ownedPathsInfo).hasOwnedPathsThat().hasSize(GetOwnedPaths.DEFAULT_LIMIT); + assertThat(ownedPathsInfo).hasOwnedChangedFilesThat().hasSize(GetOwnedPaths.DEFAULT_LIMIT); } @Test
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java index d3fe19a..06e4c15 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
@@ -16,11 +16,15 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate; @@ -34,12 +38,22 @@ import com.google.gerrit.server.util.AccountTemplateUtil; import com.google.gerrit.testing.FakeEmailSender.Message; import com.google.inject.Inject; +import java.time.Duration; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.concurrent.Callable; import org.junit.Test; -/** Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerApproval}. */ +/** + * Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.OnCodeOwnerApproval}. + * + * <p>For tests the change message that is posted when a code owner approval is applied, is added + * synchronously by default (see {@link AbstractCodeOwnersIT #defaultConfig()}). Tests that want to + * verify the asynchronous posting of this change message need to set {@code + * plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval=true} in {@code gerrit.config} + * explicitly (by using the {@link GerritConfig} annotation). + */ public class OnCodeOwnerApprovalIT extends AbstractCodeOwnersIT { @Inject private RequestScopeOperations requestScopeOperations; @Inject private ProjectOperations projectOperations; @@ -1143,4 +1157,50 @@ + "* %s\n", user.fullName(), user.email(), path)); } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "true") + public void changeMessageListsNewlyApprovedPaths_async() throws Exception { + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(admin.email()) + .create(); + + String path = "foo/bar.baz"; + String changeId = createChange("Test Change", path, "file content").getChangeId(); + + recommend(changeId); + + assertAsyncChangeMessage( + changeId, + String.format( + "Patch Set 1: " + + "By voting Code-Review+1 the following files are now code-owner approved by" + + " %s:\n" + + "* %s\n", + AccountTemplateUtil.getAccountTemplate(admin.id()), path)); + } + + private void assertAsyncChangeMessage(String changeId, String expectedChangeMessage) + throws Exception { + assertAsync( + () -> { + Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages; + assertThat(Iterables.getLast(messages).message).isEqualTo(expectedChangeMessage); + return null; + }); + } + + @CanIgnoreReturnValue + private <T> T assertAsync(Callable<T> assertion) throws Exception { + return RetryerBuilder.<T>newBuilder() + .retryIfException(t -> true) + .withStopStrategy( + StopStrategies.stopAfterDelay(Duration.ofSeconds(1).toMillis(), MILLISECONDS)) + .build() + .call(() -> assertion.call()); + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java index 8a920dd..4b1432a 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/RenameEmailIT.java
@@ -298,7 +298,7 @@ projectOperations .project(allProjects) .forUpdate() - .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS)) + .add(allowCapability(GlobalCapability.VIEW_SECONDARY_EMAILS).group(REGISTERED_USERS)) .update(); String secondaryEmail = "admin-foo@example.com";
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java index 17d481c..977af94 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/batch/CodeOwnerSubmitRuleBatchIT.java
@@ -19,13 +19,13 @@ import static com.google.gerrit.plugins.codeowners.testing.LegacySubmitRequirementInfoSubject.assertThatCollection; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; -import com.google.gerrit.acceptance.LightweightPluginDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations; import com.google.gerrit.plugins.codeowners.testing.LegacySubmitRequirementInfoSubject; import com.google.inject.Inject; @@ -42,14 +42,14 @@ @TestPlugin( name = "code-owners", sysModule = "com.google.gerrit.plugins.codeowners.acceptance.TestBatchModule") -public class CodeOwnerSubmitRuleBatchIT extends LightweightPluginDaemonTest { +public class CodeOwnerSubmitRuleBatchIT extends AbstractCodeOwnersTest { @Inject private ProjectOperations projectOperations; @Inject private RequestScopeOperations requestScopeOperations; private CodeOwnerConfigOperations codeOwnerConfigOperations; @Before - public void testSetup() throws Exception { + public void setUpCodeOwnersPlugin() throws Exception { codeOwnerConfigOperations = plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckForAccountTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckForAccountTest.java index 506d204..499f42f 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckForAccountTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckForAccountTest.java
@@ -17,11 +17,11 @@ import static com.google.gerrit.plugins.codeowners.testing.FileCodeOwnerStatusSubject.assertThatStream; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; -import com.google.gerrit.entities.Account; import com.google.gerrit.entities.Change; import com.google.gerrit.entities.PatchSet; import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; @@ -39,8 +39,8 @@ import org.junit.Test; /** - * Tests for {@link CodeOwnerApprovalCheck#getFileStatusesForAccount(ChangeNotes, PatchSet, - * Account.Id)}. + * Tests for {@link CodeOwnerApprovalCheck#getFileStatusesForAccounts(ChangeNotes, PatchSet, + * ImmutableSet)}. */ public class CodeOwnerApprovalCheckForAccountTest extends AbstractCodeOwnersTest { @Inject private ChangeNotes.Factory changeNotesFactory; @@ -65,8 +65,8 @@ // Verify that the file would not be approved by the user. Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( - changeNotes, changeNotes.getCurrentPatchSet(), user.id()); + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, changeNotes.getCurrentPatchSet(), ImmutableSet.of(user.id())); FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); @@ -75,6 +75,7 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwnersThat().isEmpty(); } @Test @@ -102,8 +103,8 @@ // Verify that the file would not be approved by the user. Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( - changeNotes, changeNotes.getCurrentPatchSet(), user.id()); + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, changeNotes.getCurrentPatchSet(), ImmutableSet.of(user.id())); FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); @@ -112,6 +113,92 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwnersThat().isEmpty(); + } + + @Test + public void approvalFromOtherCodeOwnerAsReviewerIsReturned() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(codeOwner.email()) + .create(); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + ChangeNotes changeNotes = getChangeNotes(changeId); + + // Add a Code-Review+1 (= code owner approval) from the code owner. + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // Verify that the file would not be approved by the user. + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, + changeNotes.getCurrentPatchSet(), + ImmutableSet.of(codeOwner.id(), user.id())); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.APPROVED); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(codeOwner); + } + + @Test + @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "globalCodeOwner@example.com") + public void approvalFromGlobalCodeOwnerAsReviewerIsReturned() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + TestAccount globalCodeOwner = + accountCreator.create( + "globalCodeOwner", + "globalCodeOwner@example.com", + "GlobalCodeOwner", + /* displayName= */ null); + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/") + .addCodeOwnerEmail(codeOwner.email()) + .create(); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + ChangeNotes changeNotes = getChangeNotes(changeId); + + // Add a Code-Review+1 (= code owner approval) from the code owner. + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, + changeNotes.getCurrentPatchSet(), + ImmutableSet.of(codeOwner.id(), user.id(), globalCodeOwner.id())); + FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = + assertThatStream(fileCodeOwnerStatuses).onlyElement(); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); + fileCodeOwnerStatusSubject + .hasNewPathStatus() + .value() + .hasStatusThat() + .isEqualTo(CodeOwnerStatus.APPROVED); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(codeOwner, globalCodeOwner); } @Test @@ -131,8 +218,8 @@ // Verify that the file would be approved by the user. Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( - changeNotes, changeNotes.getCurrentPatchSet(), user.id()); + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, changeNotes.getCurrentPatchSet(), ImmutableSet.of(user.id())); FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); @@ -141,6 +228,7 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.APPROVED); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(user); } @Test @@ -174,10 +262,10 @@ // Verify that the file in patch set 1 would be approved by the user. Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( + codeOwnerApprovalCheck.getFileStatusesForAccounts( changeNotes, changeNotes.getPatchSets().get(PatchSet.id(changeNotes.getChangeId(), 1)), - user.id()); + ImmutableSet.of(user.id())); FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path1); @@ -186,13 +274,13 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.APPROVED); - + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(user); // Verify that both files in patch set 2 would be approved by the user. fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( + codeOwnerApprovalCheck.getFileStatusesForAccounts( changeNotes, changeNotes.getPatchSets().get(PatchSet.id(changeNotes.getChangeId(), 2)), - user.id()); + ImmutableSet.of(user.id())); ListSubject<FileCodeOwnerStatusSubject, FileCodeOwnerStatus> fileCodeOwnerStatusListSubject = assertThatStream(fileCodeOwnerStatuses); fileCodeOwnerStatusListSubject.hasSize(2); @@ -203,6 +291,7 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.APPROVED); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(user); fileCodeOwnerStatusSubject = fileCodeOwnerStatusListSubject.element(1); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path2); fileCodeOwnerStatusSubject @@ -210,6 +299,7 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.APPROVED); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(user); } @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS") @@ -222,8 +312,8 @@ // Verify that the file would be approved by the user since the user is a fallback code owner. Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( - changeNotes, changeNotes.getCurrentPatchSet(), user.id()); + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, changeNotes.getCurrentPatchSet(), ImmutableSet.of(user.id())); FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); @@ -232,6 +322,7 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.APPROVED); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwners(user); } @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS") @@ -257,8 +348,8 @@ // Verify that the file would not be approved by the user since fallback code owners do not // apply. Stream<FileCodeOwnerStatus> fileCodeOwnerStatuses = - codeOwnerApprovalCheck.getFileStatusesForAccount( - changeNotes, changeNotes.getCurrentPatchSet(), user.id()); + codeOwnerApprovalCheck.getFileStatusesForAccounts( + changeNotes, changeNotes.getCurrentPatchSet(), ImmutableSet.of(user.id())); FileCodeOwnerStatusSubject fileCodeOwnerStatusSubject = assertThatStream(fileCodeOwnerStatuses).onlyElement(); fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasPathThat().isEqualTo(path); @@ -267,6 +358,7 @@ .value() .hasStatusThat() .isEqualTo(CodeOwnerStatus.INSUFFICIENT_REVIEWERS); + fileCodeOwnerStatusSubject.hasNewPathStatus().value().hasOwnersThat().isEmpty(); } private ChangeNotes getChangeNotes(String changeId) throws Exception {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInputTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInputTest.java new file mode 100644 index 0000000..0054f2e --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckInputTest.java
@@ -0,0 +1,854 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.entities.Account; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.PatchSet; +import com.google.gerrit.entities.PatchSetApproval; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.api.changes.ChangeApi; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.common.LabelDefinitionInput; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; +import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; +import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginProjectConfigSnapshot; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.inject.Inject; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link CodeOwnerApprovalCheckInput}. */ +public class CodeOwnerApprovalCheckInputTest extends AbstractCodeOwnersTest { + @Inject private ChangeNotes.Factory changeNotesFactory; + @Inject private ProjectOperations projectOperations; + @Inject private RequestScopeOperations requestScopeOperations; + + private CodeOwnerApprovalCheckInput.Loader.Factory inputLoaderFactory; + private CodeOwnerResolver codeOwnerResolver; + private CodeOwnersPluginProjectConfigSnapshot codeOwnersConfig; + private TestAccount user2; + private Change.Id changeId; + private Change.Key changeKey; + private Account.Id changeOwner; + + @Before + public void setUpCodeOwnersPlugin() throws Exception { + inputLoaderFactory = + plugin.getSysInjector().getInstance(CodeOwnerApprovalCheckInput.Loader.Factory.class); + codeOwnerResolver = plugin.getSysInjector().getInstance(CodeOwnerResolver.class); + CodeOwnersPluginConfiguration codeOwnersPluginConfiguration = + plugin.getSysInjector().getInstance(CodeOwnersPluginConfiguration.class); + codeOwnersConfig = codeOwnersPluginConfiguration.getProjectConfig(project); + } + + @Before + public void setUp() throws Exception { + user2 = accountCreator.user2(); + ChangeData changeData = createChange().getChange(); + changeId = changeData.getId(); + changeKey = changeData.change().getKey(); + changeOwner = admin.id(); + } + + @Test + public void noReviewers() { + assertThat(loadInput().reviewers()).isEmpty(); + } + + @Test + public void withReviewers() throws Exception { + changeApi().addReviewer(user.email()); + changeApi().addReviewer(user2.email()); + + // make the change owner a reviewer: + // the change owner cannot be added as a reviewer, but the change owner becomes a reviewer when + // they vote on the change + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + assertThat(loadInput().reviewers()).containsExactly(user.id(), user2.id(), changeOwner); + } + + @Test + public void withReviewers_selfApprovalsIgnored() throws Exception { + disableSelfCodeReviewApprovals(); + + changeApi().addReviewer(user.email()); + changeApi().addReviewer(user2.email()); + + // make the change owner a reviewer: + // the change owner cannot be added as a reviewer, but the change owner becomes a reviewer when + // they vote on the change + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + assertThat(loadInput().reviewers()).containsExactly(user.id(), user2.id()); + } + + @Test + public void noApprovers() { + assertThat(loadInput().approvers()).isEmpty(); + } + + @Test + public void withApprovers() throws Exception { + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + assertThat(loadInput().approvers()).containsExactly(changeOwner, user.id(), user2.id()); + } + + @Test + public void withApprovers_selfApprovalsIgnored() throws Exception { + disableSelfCodeReviewApprovals(); + + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + assertThat(loadInput().approvers()).containsExactly(user.id(), user2.id()); + } + + /** Test that current approvals do not count for computing previous approvers. */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void noPreviousApprovers() throws Exception { + // self approve current patch set + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user current patch set + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 current patch set + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** Test that previous approvals on other labels do not count for computing previous approvers. */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void noPreviousApproversIfApprovalIsOnUnrelatedLabel() throws Exception { + // Create Foo-Review label. + LabelDefinitionInput input = new LabelDefinitionInput(); + input.values = ImmutableMap.of("+1", "Approved", " 0", "Not Approved"); + gApi.projects().name(project.get()).label("Foo-Review").create(input).get(); + + // Allow to vote on the Foo-Review label. + projectOperations + .project(project) + .forUpdate() + .add( + TestProjectUpdate.allowLabel("Foo-Review") + .range(0, 1) + .ref("refs/heads/*") + .group(REGISTERED_USERS) + .build()) + .update(); + + // approve on Foo-Review label + requestScopeOperations.setApiUser(user.id()); + gApi.changes().id(changeId.get()).current().review(new ReviewInput().label("Foo-Review", 1)); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** + * Test that previous votes with insufficient values do not count for computing previous + * approvers. + */ + @Test + @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2") + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void noPreviousApproversIfVoteIsNotAnApproval() throws Exception { + // vote with Code-Review+1, but only Code-Review+2 counts as a code owner approval + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** Test that previous approvals are ignored if sticky approvals are disabled. */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "false") + public void noPreviousApproversIfEnableStickyApprovalsDisabled() throws Exception { + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** Test that the approvals on the previous patch set count for computing previous approvers. */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void withPreviousApprovers() throws Exception { + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.putAll( + PatchSet.id(changeId, 1), ImmutableSet.of(changeOwner, user.id(), user2.id())); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + } + + /** + * Test that a self-approval on the previous patch set is ignored for computing previous + * approvers. + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void withPreviousApprovers_selfApprovalsIgnored() throws Exception { + disableSelfCodeReviewApprovals(); + + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.putAll( + PatchSet.id(changeId, 1), ImmutableSet.of(user.id(), user2.id())); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + } + + /** + * Test that the approvals on different previous patch sets count for computing previous + * approvers. + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void withPreviousApproversOnDifferentPatchSets() throws Exception { + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a third patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a 4th patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.put(PatchSet.id(changeId, 1), changeOwner); + expectedPreviousApprovers.put(PatchSet.id(changeId, 2), user.id()); + expectedPreviousApprovers.put(PatchSet.id(changeId, 3), user2.id()); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + } + + /** Test that a self-approval on an old patch set is ignored for computing previous approvers. */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void withPreviousApproversOnDifferentPatchSets_selfApprovalsIgnored() throws Exception { + disableSelfCodeReviewApprovals(); + + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a third patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a 4th patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.put(PatchSet.id(changeId, 2), user.id()); + expectedPreviousApprovers.put(PatchSet.id(changeId, 3), user2.id()); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + } + + /** + * Test that sticky approvals do not count for computing previous approvers (because if the + * approval is sticky it's a current approval). + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void noPreviousApproversIfApprovalIsCopied() throws Exception { + // Make Code-Review approvals sticky + LabelDefinitionInput input = new LabelDefinitionInput(); + input.copyCondition = "is:ANY"; + gApi.projects().name(allProjects.get()).label("Code-Review").update(input); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** + * Test that a previous approval still counts for computing previous approvers if the approver + * comments on the current patch set without applying a vote. + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void previousApproversIsPreservedWhenThePreviousApproverCommentsOnTheChange() + throws Exception { + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // check that the previous approver on patch set 1 is found + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.put(PatchSet.id(changeId, 1), user.id()); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + + // comment on the change + requestScopeOperations.setApiUser(user.id()); + ReviewInput reviewInput = ReviewInput.noScore(); + reviewInput.message = "a comment"; + gApi.changes().id(changeId.get()).current().review(reviewInput); + + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + } + + /** + * Test that a previous approval doesn't count for computing previous approvers if the approver + * downgrades the vote. + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2") + public void noPreviousApproversIfApprovalIsDowngraded() throws Exception { + // Allow all users to vote with Code-Review+2. + projectOperations + .project(project) + .forUpdate() + .add( + TestProjectUpdate.allowLabel("Code-Review") + .range(0, 2) + .ref("refs/heads/*") + .group(REGISTERED_USERS) + .build()) + .update(); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + approve(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // check that the previous approver on patch set 1 is found + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.put(PatchSet.id(changeId, 1), user.id()); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + + // change vote from Code-Review+2 to Code-Review+1 as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // the Code-Review+1 vote on the current patch set overrode the previous approval + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + + // create a third patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // the Code-Review+1 vote on the previous patch set overrode the previous approval + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** + * Test that a previous approval doesn't count for computing previous approvers if the approver + * re-applies the approval (because now it's a current approval). + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void noPreviousApproversIfApprovalIsReapplied() throws Exception { + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // check that the previous approver on patch set 1 is found + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.put(PatchSet.id(changeId, 1), user.id()); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + + // re-apply the approval + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + assertThat(loadInput().approversFromPreviousPatchSets()).isEmpty(); + } + + /** + * Test that only the last previous approval of a user counts for computing previous approvers. + */ + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void onlyLastPreviousApprovalOfAUserIsConsideredForComputingPreviousApprovers() + throws Exception { + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // re-approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a third patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + ArrayListMultimap<PatchSet.Id, Account.Id> expectedPreviousApprovers = + ArrayListMultimap.create(); + expectedPreviousApprovers.put(PatchSet.id(changeId, 2), user.id()); + assertThat(loadInput().approversFromPreviousPatchSets()) + .containsExactlyEntriesIn(expectedPreviousApprovers); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void noPreviouslyApprovedPatchSets() throws Exception { + // approve current patch set + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + assertThat(loadInput().previouslyApprovedPatchSetsInReverseOrder()).isEmpty(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void previouslyApprovedPatchSetsAreReturnedInReverseOrder() throws Exception { + // create a second patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // create a third patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // approve as user2, ignored since overridden on patch set 4 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a 4th patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + // re- approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // create a 5th patch set + requestScopeOperations.setApiUser(changeOwner); + amendChange(changeKey.get()).assertOkStatus(); + + assertThat(loadInput().previouslyApprovedPatchSetsInReverseOrder()) + .containsExactly(PatchSet.id(changeId, 4), PatchSet.id(changeId, 2)); + } + + @Test + public void noImplicitApprover() { + assertThat(loadInput().implicitApprover()).isEmpty(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") + public void withImplicitApprover() { + // If implicit approvals are enabled, an implicit approval of the current uploader is assumed. + // Since the change has only 1 patch set the current uploader is the change owner. + assertThat(loadInput().implicitApprover()).hasValue(changeOwner); + } + + @Test + public void noOverrides() { + assertThat(loadInput().overrides()).isEmpty(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1") + public void withOverrides() throws Exception { + createOwnersOverrideLabel(); + + ReviewInput reviewInput = new ReviewInput().label("Owners-Override", 1); + + // self override + requestScopeOperations.setApiUser(changeOwner); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // override as user + requestScopeOperations.setApiUser(user.id()); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // override as user2 + requestScopeOperations.setApiUser(user2.id()); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + assertThat( + loadInput().overrides().stream() + .map(PatchSetApproval::accountId) + .collect(toImmutableSet())) + .containsExactly(changeOwner, user.id(), user2.id()); + } + + @Test + @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1") + public void withOverrides_selfApprovalsIgnored() throws Exception { + createOwnersOverrideLabel(); + disableSelfOwnersOverrideApprovals(); + + ReviewInput reviewInput = new ReviewInput().label("Owners-Override", 1); + + // self override + requestScopeOperations.setApiUser(changeOwner); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // override as user + requestScopeOperations.setApiUser(user.id()); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // override as user2 + requestScopeOperations.setApiUser(user2.id()); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + assertThat( + loadInput().overrides().stream() + .map(PatchSetApproval::accountId) + .collect(toImmutableSet())) + .containsExactly(user.id(), user2.id()); + } + + @Test + public void noGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = loadInput().globalCodeOwners(); + assertThat(globalCodeOwners.codeOwnersAccountIds()).isEmpty(); + assertThat(globalCodeOwners.ownedByAllUsers()).isFalse(); + } + + @Test + @GerritConfig( + name = "plugin.code-owners.globalCodeOwner", + values = {"user1@example.com", "user2@example.com"}) + public void withGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = loadInput().globalCodeOwners(); + assertThat(globalCodeOwners.codeOwnersAccountIds()).containsExactly(user.id(), user2.id()); + assertThat(globalCodeOwners.ownedByAllUsers()).isFalse(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*") + public void withAllUsersAsGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = loadInput().globalCodeOwners(); + assertThat(globalCodeOwners.codeOwnersAccountIds()).isEmpty(); + assertThat(globalCodeOwners.ownedByAllUsers()).isTrue(); + } + + @Test + public void withFallbackCodeOwnersNone() { + assertThat(loadInput().fallbackCodeOwners()).isEqualTo(FallbackCodeOwners.NONE); + } + + @Test + @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS") + public void withFallbackCodeOwnersAllUsers() { + assertThat(loadInput().fallbackCodeOwners()).isEqualTo(FallbackCodeOwners.ALL_USERS); + } + + @Test + public void checkAllOwnersIsFalse() { + assertThat(loadInput().checkAllOwners()).isFalse(); + } + + @Test + public void createInputForComputingOwnedPaths_noReviewers() throws Exception { + changeApi().addReviewer(user.email()); + changeApi().addReviewer(user2.email()); + + // CodeOwnerApprovalCheckInput#createForComputingOwnedPaths never sets reviewers + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).reviewers()).isEmpty(); + } + + @Test + public void createInputForComputingOwnedPaths_noApprovers() throws Exception { + // self approve + requestScopeOperations.setApiUser(changeOwner); + recommend(changeId.toString()); + + // approve as user + requestScopeOperations.setApiUser(user.id()); + recommend(changeId.toString()); + + // approve as user2 + requestScopeOperations.setApiUser(user2.id()); + recommend(changeId.toString()); + + // CodeOwnerApprovalCheckInput#createForComputingOwnedPaths always sets approvers to the given + // accounts + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).approvers()).isEmpty(); + } + + @Test + public void createInputForComputingOwnedPaths_withApprovers() { + // CodeOwnerApprovalCheckInput#createForComputingOwnedPaths always sets approvers to the given + // accounts + assertThat( + createInputForComputingOwnedPaths(ImmutableSet.of(admin.id(), user.id(), user2.id())) + .approvers()) + .containsExactly(admin.id(), user.id(), user2.id()); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") + public void createInputForComputingOwnedPaths_noImplicitApprover() { + // CodeOwnerApprovalCheckInput#createForComputingOwnedPaths never sets an implicit approver + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).implicitApprover()).isEmpty(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.overrideApproval", value = "Owners-Override+1") + public void createInputForComputingOwnedPaths_noOverride() throws Exception { + createOwnersOverrideLabel(); + + ReviewInput reviewInput = new ReviewInput().label("Owners-Override", 1); + + // self override + requestScopeOperations.setApiUser(changeOwner); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // override as user + requestScopeOperations.setApiUser(user.id()); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // override as user2 + requestScopeOperations.setApiUser(user2.id()); + gApi.changes().id(changeId.toString()).current().review(reviewInput); + + // CodeOwnerApprovalCheckInput#createForComputingOwnedPaths never sets overrides + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).overrides()).isEmpty(); + } + + @Test + public void createInputForComputingOwnedPaths_noGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = + createInputForComputingOwnedPaths(ImmutableSet.of()).globalCodeOwners(); + assertThat(globalCodeOwners.codeOwnersAccountIds()).isEmpty(); + assertThat(globalCodeOwners.ownedByAllUsers()).isFalse(); + } + + @Test + @GerritConfig( + name = "plugin.code-owners.globalCodeOwner", + values = {"user1@example.com", "user2@example.com"}) + public void createInputForComputingOwnedPaths_withGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = + createInputForComputingOwnedPaths(ImmutableSet.of()).globalCodeOwners(); + assertThat(globalCodeOwners.codeOwnersAccountIds()).containsExactly(user.id(), user2.id()); + assertThat(globalCodeOwners.ownedByAllUsers()).isFalse(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*") + public void createInputForComputingOwnedPaths_withAllUsersAsGlobalCodeOwners() { + CodeOwnerResolverResult globalCodeOwners = + createInputForComputingOwnedPaths(ImmutableSet.of()).globalCodeOwners(); + assertThat(globalCodeOwners.codeOwnersAccountIds()).isEmpty(); + assertThat(globalCodeOwners.ownedByAllUsers()).isTrue(); + } + + @Test + public void createInputForComputingOwnedPaths_withFallbackCodeOwnersNone() { + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).fallbackCodeOwners()) + .isEqualTo(FallbackCodeOwners.NONE); + } + + @Test + @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS") + public void createInputForComputingOwnedPaths_withFallbackCodeOwnersAllUsers() { + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).fallbackCodeOwners()) + .isEqualTo(FallbackCodeOwners.ALL_USERS); + } + + @Test + public void createInputForComputingOwnedPaths_checkAllOwnersIsTrue() { + assertThat(createInputForComputingOwnedPaths(ImmutableSet.of()).checkAllOwners()).isTrue(); + } + + private void disableSelfCodeReviewApprovals() throws Exception { + disableSelfApprovals(allProjects, "Code-Review"); + } + + private void disableSelfOwnersOverrideApprovals() throws Exception { + disableSelfApprovals(project, "Owners-Override"); + } + + private void disableSelfApprovals(Project.NameKey project, String labelName) throws Exception { + LabelDefinitionInput input = new LabelDefinitionInput(); + input.ignoreSelfApproval = true; + gApi.projects().name(project.get()).label(labelName).update(input); + } + + private ChangeApi changeApi() throws RestApiException { + return gApi.changes().id(changeId.get()); + } + + private CodeOwnerApprovalCheckInput loadInput() { + ChangeNotes changeNotes = changeNotesFactory.create(project, changeId); + return inputLoaderFactory.create(codeOwnersConfig, codeOwnerResolver, changeNotes).load(); + } + + private CodeOwnerApprovalCheckInput createInputForComputingOwnedPaths( + ImmutableSet<Account.Id> accounts) { + ChangeNotes changeNotes = changeNotesFactory.create(project, changeId); + return CodeOwnerApprovalCheckInput.createForComputingOwnedPaths( + codeOwnersConfig, codeOwnerResolver, changeNotes, accounts); + } +}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java index 915a108..54d4e67 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckTest.java
@@ -59,8 +59,8 @@ * * <p>Further tests with fallback code owners are implemented in {@link * CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest} and the functionality of {@link - * CodeOwnerApprovalCheck#getFileStatusesForAccount(ChangeNotes, - * com.google.gerrit.entities.PatchSet, com.google.gerrit.entities.Account.Id)} is covered by {@link + * CodeOwnerApprovalCheck#getFileStatusesForAccounts(ChangeNotes, + * com.google.gerrit.entities.PatchSet, ImmutableSet)} is covered by {@link * CodeOwnerApprovalCheckForAccountTest}. */ public class CodeOwnerApprovalCheckTest extends AbstractCodeOwnersTest {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java index 5c31eed..b6c3a43 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithAllUsersAsFallbackCodeOwnersTest.java
@@ -52,7 +52,7 @@ /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ @ConfigSuite.Default public static Config defaultConfig() { - Config cfg = new Config(); + Config cfg = AbstractCodeOwnersTest.defaultConfig(); cfg.setEnum( "plugin", "code-owners",
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java index a665ad4..43ab903 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithSelfApprovalsIgnoredTest.java
@@ -46,7 +46,7 @@ /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ @ConfigSuite.Default public static Config defaultConfig() { - Config cfg = new Config(); + Config cfg = AbstractCodeOwnersTest.defaultConfig(); cfg.setString( "plugin", "code-owners", OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL, "Owners-Override+1"); return cfg;
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java new file mode 100644 index 0000000..28f83f5 --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheckWithStickyApprovalsTest.java
@@ -0,0 +1,696 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import static com.google.gerrit.plugins.codeowners.testing.FileCodeOwnerStatusSubject.assertThatCollection; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate; +import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; +import com.google.gerrit.entities.Change; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; +import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations; +import com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig; +import com.google.gerrit.plugins.codeowners.common.CodeOwnerStatus; +import com.google.gerrit.plugins.codeowners.util.JgitPath; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.util.AccountTemplateUtil; +import com.google.gerrit.testing.ConfigSuite; +import com.google.inject.Inject; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import org.eclipse.jgit.lib.Config; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link CodeOwnerApprovalCheck} with sticky approvals enabled. */ +public class CodeOwnerApprovalCheckWithStickyApprovalsTest extends AbstractCodeOwnersTest { + @Inject private ChangeNotes.Factory changeNotesFactory; + @Inject private RequestScopeOperations requestScopeOperations; + @Inject private ProjectOperations projectOperations; + + private CodeOwnerApprovalCheck codeOwnerApprovalCheck; + private CodeOwnerConfigOperations codeOwnerConfigOperations; + + /** Returns a {@code gerrit.config} that configures all users as fallback code owners. */ + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = AbstractCodeOwnersTest.defaultConfig(); + cfg.setBoolean( + "plugin", "code-owners", GeneralConfig.KEY_ENABLE_STICKY_APPROVALS, /* value= */ true); + return cfg; + } + + @Before + public void setUpCodeOwnersPlugin() throws Exception { + codeOwnerApprovalCheck = plugin.getSysInjector().getInstance(CodeOwnerApprovalCheck.class); + codeOwnerConfigOperations = + plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class); + } + + @Test + public void notApproved_noStickyApproval() throws Exception { + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // Verify that the file is not approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition(path, CodeOwnerStatus.INSUFFICIENT_REVIEWERS)); + + // create a second patch set so that there is a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is not approved. + fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition(path, CodeOwnerStatus.INSUFFICIENT_REVIEWERS)); + } + + @Test + public void notApproved_byPreviousApprovalOfNonCodeOwner() throws Exception { + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve as user who is not a code owner + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is not approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition(path, CodeOwnerStatus.INSUFFICIENT_REVIEWERS)); + } + + @Test + public void approved_byStickyApprovalOnPreviousPatchSet() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + public void approved_byStickyApprovalOnOldPatchSet() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // create several new patch sets so that the approval becomes an approval on an old patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "newer file content") + .assertOkStatus(); + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "newest file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + public void approved_byStickyApprovalsOfDifferentUsersOnDifferentPreviousPatchSets() + throws Exception { + TestAccount codeOwner1 = + accountCreator.create( + "codeOwner1", "codeOwner1@example.com", "CodeOwner1", /* displayName= */ null); + setAsCodeOwners("/foo/", codeOwner1); + TestAccount codeOwner2 = + accountCreator.create( + "codeOwner2", "codeOwner2@example.com", "CodeOwner2", /* displayName= */ null); + setAsCodeOwners("/bar/", codeOwner2); + + Path path1 = Paths.get("/foo/bar.baz"); + Path path2 = Paths.get("/bar/foo.baz"); + String changeId = + createChange( + "Change Adding A File", + ImmutableMap.of( + JgitPath.of(path1).get(), + "file content", + JgitPath.of(path2).get(), + "file content")) + .getChangeId(); + + // code owner approve first path + requestScopeOperations.setApiUser(codeOwner1.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange( + changeId, + "Change Adding A File", + ImmutableMap.of( + JgitPath.of(path1).get(), + "new file content", + JgitPath.of(path2).get(), + "new file content")) + .assertOkStatus(); + + // Verify that the path1 is approved, but path2 isn't. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path1, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner1.id()))), + FileCodeOwnerStatus.addition(path2, CodeOwnerStatus.INSUFFICIENT_REVIEWERS)); + + // code owner approve second path + requestScopeOperations.setApiUser(codeOwner2.id()); + recommend(changeId); + + // create another patch set so that the second approval becomes an approval on a previous patch + // set + amendChange( + changeId, + "Change Adding A File", + ImmutableMap.of( + JgitPath.of(path1).get(), + "newer file content", + JgitPath.of(path2).get(), + "newer file content")) + .assertOkStatus(); + + // Verify that both paths approved now. + fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path1, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner1.id()))), + FileCodeOwnerStatus.addition( + path2, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 2 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner2.id())))); + } + + @Test + public void notApproved_byPreviousApprovalThatHasBeenDeleted() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // delete the approval + adminRestSession + .delete( + "/changes/" + + changeId + + "/reviewers/" + + codeOwner.id().toString() + + "/votes/Code-Review") + .assertNoContent(); + + // create a second patch set so that the deleted approval becomes an approval on a previous + // patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is not approved. The expected status is PENDING since the code owner is + // a reviewer now. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.PENDING, + String.format( + "reviewer %s is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + @GerritConfig(name = "plugin.code-owners.requiredApproval", value = "Code-Review+2") + public void notApproved_byPreviousApprovalThatHasBeenDowngraded() throws Exception { + // Allow all users to vote with Code-Review+2. + projectOperations + .project(project) + .forUpdate() + .add( + TestProjectUpdate.allowLabel("Code-Review") + .range(0, 2) + .ref("refs/heads/*") + .group(REGISTERED_USERS) + .build()) + .update(); + + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + approve(changeId); + + // create a second patch set so that the deleted approval becomes an approval on a previous + // patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // downgrade approval + recommend(changeId); + + // Verify that the file is not approved. The expected status is PENDING since the code owner is + // a reviewer now. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.PENDING, + String.format( + "reviewer %s is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + + // create another patch set so that the downgraded approval becomes an approval on a previous + // patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "newer file content") + .assertOkStatus(); + + fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.PENDING, + String.format( + "reviewer %s is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + public void approved_reapprovalTrumpsPreviousApproval() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // create a second patch set so that the deleted approval becomes an approval on a previous + // patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // re-approve + recommend(changeId); + + // Verify that the file is approved by the current approval. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + + // create another patch set so that the re-approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "newer file content") + .assertOkStatus(); + + fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 2 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableImplicitApprovals", value = "true") + public void approved_implicitApprovalTrumpsPreviousApproval() throws Exception { + TestAccount implicitCodeOwner = admin; // the changes is created by the admit user + setAsRootCodeOwners(implicitCodeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(implicitCodeOwner.id()); + recommend(changeId); + + // create a second patch set so that the deleted approval becomes an approval on a previous + // patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved by the current implicit approval. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "implicitly approved by the patch set uploader %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(implicitCodeOwner.id())))); + } + + @Test + public void notApproved_fileThatIsNotPresentInApprovedPatchSetIsNotCoveredByTheApproval() + throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsRootCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // create a second patch set that adds a new file + Path path2 = Paths.get("/foo/abc.xyz"); + amendChange( + changeId, + "Change Adding A File", + ImmutableMap.of( + JgitPath.of(path).get(), + "new file content", + JgitPath.of(path2).get(), + "file content")) + .assertOkStatus(); + + // Verify that the new file is not approved. The expected status is PENDING since the code owner + // is a reviewer. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id()))), + FileCodeOwnerStatus.addition( + path2, + CodeOwnerStatus.PENDING, + String.format( + "reviewer %s is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + + // re-approve to cover all files + recommend(changeId); + + fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id()))), + FileCodeOwnerStatus.addition( + path2, + CodeOwnerStatus.APPROVED, + String.format( + "approved by %s who is a code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + public void approved_byStickyApprovalOnPreviousPatchSet_everyoneIsCodeOwner() throws Exception { + // Create a code owner config file that makes everyone a code owner. + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .addCodeOwnerEmail("*") + .create(); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(user.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a code owner (all users are code owners)", + AccountTemplateUtil.getAccountTemplate(user.id())))); + } + + @Test + public void approved_byStickyApprovalOfDefaultCodeOnPreviousPatchSet() throws Exception { + TestAccount codeOwner = + accountCreator.create( + "codeOwner", "codeOwner@example.com", "CodeOwner", /* displayName= */ null); + setAsDefaultCodeOwners(codeOwner); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(codeOwner.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a default code owner", + AccountTemplateUtil.getAccountTemplate(codeOwner.id())))); + } + + @Test + public void approved_byStickyApprovalOfDefaultCodeOnPreviousPatchSet_everyoneIsDefaultCodeOwner() + throws Exception { + // Create a code owner config file that makes everyone a default code owner. + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch(RefNames.REFS_CONFIG) + .folderPath("/") + .addCodeOwnerEmail("*") + .create(); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(user.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a default code owner" + + " (all users are default code owners)", + AccountTemplateUtil.getAccountTemplate(user.id())))); + } + + @Test + @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "bot@example.com") + public void approved_byStickyApprovalOfGlobalCodeOnPreviousPatchSet() throws Exception { + // Create a bot user that is a global code owner. + TestAccount bot = + accountCreator.create("bot", "bot@example.com", "Bot", /* displayName= */ null); + + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(bot.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a global code owner", + AccountTemplateUtil.getAccountTemplate(bot.id())))); + } + + @Test + @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "*") + public void approved_byStickyApprovalOfGlobalCodeOnPreviousPatchSet_everyoneIsGlobalCodeOwner() + throws Exception { + Path path = Paths.get("/foo/bar.baz"); + String changeId = + createChange("Change Adding A File", JgitPath.of(path).get(), "file content").getChangeId(); + + // code owner approve + requestScopeOperations.setApiUser(user.id()); + recommend(changeId); + + // create a second patch set so that the approval becomes an approval on a previous patch set + amendChange(changeId, "Change Adding A File", JgitPath.of(path).get(), "new file content") + .assertOkStatus(); + + // Verify that the file is approved. + ImmutableSet<FileCodeOwnerStatus> fileCodeOwnerStatuses = getFileCodeOwnerStatuses(changeId); + assertThatCollection(fileCodeOwnerStatuses) + .containsExactly( + FileCodeOwnerStatus.addition( + path, + CodeOwnerStatus.APPROVED, + String.format( + "approved on patch set 1 by %s who is a global code owner" + + " (all users are global code owners)", + AccountTemplateUtil.getAccountTemplate(user.id())))); + } + + private ImmutableSet<FileCodeOwnerStatus> getFileCodeOwnerStatuses(String changeId) + throws Exception { + return codeOwnerApprovalCheck.getFileStatusesAsSet( + getChangeNotes(changeId), /* start= */ 0, /* limit= */ 0); + } + + private ChangeNotes getChangeNotes(String changeId) throws Exception { + return changeNotesFactory.create(project, Change.id(gApi.changes().id(changeId).get()._number)); + } + + private PushOneCommit.Result amendChange( + String changeId, String subject, Map<String, String> files) throws Exception { + PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, files, changeId); + return push.to("refs/for/master"); + } +}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java new file mode 100644 index 0000000..b6efbc6 --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigImportTest.java
@@ -0,0 +1,41 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.backend; + +import org.junit.Test; + +/** Tests for {@link CodeOwnerConfigImport}. */ +public class CodeOwnerConfigImportTest extends AbstractAutoValueTest { + @Test + public void toStringIncludesAllData_resolvedImport() throws Exception { + CodeOwnerConfigImport resolvedImport = + CodeOwnerConfigImport.createResolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")); + assertThatToStringIncludesAllData(resolvedImport, CodeOwnerConfigImport.class); + } + + @Test + public void toStringIncludesAllData_unresolvedImport() throws Exception { + CodeOwnerConfigImport unresolvedImport = + CodeOwnerConfigImport.createUnresolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"), + "test message"); + assertThatToStringIncludesAllData(unresolvedImport, CodeOwnerConfigImport.class); + } +}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java index 18ea017..deab817 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResultTest.java
@@ -29,7 +29,17 @@ /* annotations= */ ImmutableMultimap.of(), /* ownedByAllUsers= */ false, /* hasUnresolvedCodeOwners= */ false, - /* hasUnresolvedImports= */ false, + ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"))), + ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + CodeOwnerConfig.Key.create(project, "master", "/"), + CodeOwnerConfig.Key.create(project, "master", "/bar/"), + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"), + "test message")), ImmutableList.of("test message")); assertThatToStringIncludesAllData(codeOwnerResolverResult, CodeOwnerResolverResult.class); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java index d270fdc..d9b3a71 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersResultTest.java
@@ -27,19 +27,27 @@ @Test public void toStringIncludesAllData() throws Exception { CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/"); - CodeOwnerConfigReference codeOwnerConfigReference = + CodeOwnerConfigReference resolvableCodeOwnerConfigReference = CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"); + CodeOwnerConfigReference unresolvableCodeOwnerConfigReference = + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS"); PathCodeOwnersResult pathCodeOwnersResult = PathCodeOwnersResult.create( Paths.get("/foo/bar/baz.md"), CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION) - .addImport(codeOwnerConfigReference) + .addImport(resolvableCodeOwnerConfigReference) + .addImport(unresolvableCodeOwnerConfigReference) .build(), ImmutableList.of( - UnresolvedImport.create( + CodeOwnerConfigImport.createResolvedImport( codeOwnerConfigKey, CodeOwnerConfig.Key.create(project, "master", "/bar/"), - codeOwnerConfigReference, + resolvableCodeOwnerConfigReference)), + ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + codeOwnerConfigKey, + CodeOwnerConfig.Key.create(project, "master", "/baz/"), + unresolvableCodeOwnerConfigReference, "test message"))); assertThatToStringIncludesAllData(pathCodeOwnersResult, PathCodeOwnersResult.class); }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java index 9563ade..04df442 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/PathCodeOwnersTest.java
@@ -239,7 +239,8 @@ pathCodeOwnersFactory.createWithoutCache( emptyCodeOwnerConfig, Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty(); - assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().hasUnresolvedImports()).isFalse(); + assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().resolvedImports()).isEmpty(); + assertThat(pathCodeOwners.resolveCodeOwnerConfig().get().unresolvedImports()).isEmpty(); } @Test @@ -471,6 +472,11 @@ @Test public void nonResolveableImportIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "master", "/non-existing/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with non-resolveable import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -478,9 +484,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.ALL, "/non-existing/OWNERS")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -494,24 +498,40 @@ // Expectation: we get the global code owner from the importing code owner config, the // non-resolveable import is silently ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test public void importOfNonCodeOwnerConfigFileIsIgnored() throws Exception { // create a file that looks like a code owner config file, but which has a name that is not // allowed as code owner config file - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .fileName("FOO") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName("FOO") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create config with import of non code owner config file CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -520,7 +540,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/FOO")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -538,7 +558,19 @@ assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test @@ -548,14 +580,17 @@ // parameter fileExtension) or file extensions are enabled for code owner config files (config // paramater enableCodeOwnerConfigFilesWithFileExtensions). Both is not the case here, hence any // import of this file in another code owner config file should get ignored. - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .fileName("OWNERS.foo") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName("OWNERS.foo") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create the importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -564,8 +599,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS.FOO")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -584,7 +618,19 @@ assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test @@ -595,14 +641,17 @@ // Create a code owner config file with a file extension. This file is considered as a code // owner config file since file extensions for code owner config files are enabled (paramater // enableCodeOwnerConfigFilesWithFileExtensions). - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/") - .fileName("OWNERS.FOO") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/") + .fileName("OWNERS.FOO") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create the importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -611,8 +660,7 @@ .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS.FOO")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build()) .create(); @@ -630,7 +678,13 @@ assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwnersResult.hasUnresolvedImports()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -644,6 +698,19 @@ } private void testImportGlobalCodeOwners(CodeOwnerConfigImportMode importMode) throws Exception { + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -652,18 +719,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport(CodeOwnerConfigReference.create(importMode, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -674,17 +732,40 @@ // Expectation: we get the global code owners from the importing and the imported code owner // config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importPerFileCodeOwners_importModeAll() throws Exception { + // create imported config with matching per-file code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with matching per-file code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -695,44 +776,53 @@ .addPathExpression("*.md") .addCodeOwnerEmail(admin.email()) .build()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the matching per-file code owners from the importing and the imported // code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void nonMatchingPerFileCodeOwnersAreNotImported_importModeAll() throws Exception { + // create imported config with non-matching per-file code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.txt") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with matching per-file code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -743,27 +833,13 @@ .addPathExpression("*.md") .addCodeOwnerEmail(admin.email()) .build()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with non-matching per-file code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.txt") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -771,17 +847,41 @@ // Expectation: we only get the matching per-file code owners from the importing code owner // config, the per-file code owners from the imported code owner config are not relevant since // they do not match - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void perFileCodeOwnersAreNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception { + // create imported config with matching per-file code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with matching per-file code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -792,28 +892,13 @@ .addPathExpression("*.md") .addCodeOwnerEmail(admin.email()) .build()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -821,48 +906,57 @@ // Expectation: we only get the matching per-file code owners from the importing code owner // config, the matching per-file code owners from the imported code owner config are not // relevant with import mode GLOBAL_CODE_OWNER_SETS_ONLY - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importIgnoreGlobalAndParentCodeOwnersFlagFromMatchingPerFileCodeOwnerSet_importModeAll() throws Exception { + // create imported config with matching per-file code owner that has the + // ignoreGlobalAndParentCodeOwners flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .setIgnoreGlobalAndParentCodeOwners() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner that has the - // ignoreGlobalAndParentCodeOwners flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .setIgnoreGlobalAndParentCodeOwners() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -872,50 +966,58 @@ // the matching per-file code owner set in the imported code owner config has the // ignoreGlobalAndParentCodeOwners flag set to true which causes global code owners to be // ignored, in addition this flag causes parent code owners to be ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isTrue(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void ignoreGlobalAndParentCodeOwnersFlagIsNotImportedFromNonMatchingPerFileCodeOwnerSet_importModeAll() throws Exception { + // create imported config with non-matching per-file code owner that has the + // ignoreGlobalAndParentCodeOwners flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .setIgnoreGlobalAndParentCodeOwners() + .addPathExpression("*.txt") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with non-matching per-file code owner that has the - // ignoreGlobalAndParentCodeOwners flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .setIgnoreGlobalAndParentCodeOwners() - .addPathExpression("*.txt") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -924,50 +1026,58 @@ // per-file code owners from the imported code owner config and its // ignoreGlobalAndParentCodeOwners flag are not relevant since the per-file code owner set does // not match - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void ignoreGlobalAndParentCodeOwnersFlagIsNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception { + // create imported config with matching per-file code owner that has the + // ignoreGlobalAndParentCodeOwners flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .setIgnoreGlobalAndParentCodeOwners() + .addPathExpression("*.md") + .addCodeOwnerEmail(user.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with matching per-file code owner that has the - // ignoreGlobalAndParentCodeOwners flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .setIgnoreGlobalAndParentCodeOwners() - .addPathExpression("*.md") - .addCodeOwnerEmail(user.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -976,89 +1086,114 @@ // matching per-file code owners from the imported code owner config and its // ignoreGlobalAndParentCodeOwners flag are not relevant with import mode // GLOBAL_CODE_OWNER_SETS_ONLY - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importIgnoreParentCodeOwnersFlag_importModeAll() throws Exception { + // create imported config with the ignoreParentCodeOnwers flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .ignoreParentCodeOwners() + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with the ignoreParentCodeOnwers flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .ignoreParentCodeOwners() - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: ignoreParentCodeOwners is true because the ignoreParentCodeOwners flag in the // imported code owner config is set to true - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isTrue(); + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void ignoreParentCodeOwnersFlagNotImported_importModeGlobalCodeOwnerSetsOnly() throws Exception { + // create imported config with the ignoreParentCodeOnwers flag set to true + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .ignoreParentCodeOwners() + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with the ignoreParentCodeOnwers flag set to true - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .ignoreParentCodeOwners() - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: ignoreParentCodeOwners is false because the ignoreParentCodeOwners flag in the // imported code owner config is not relevant with import mode GLOBAL_CODE_OWNER_SETS_ONLY - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1077,51 +1212,69 @@ throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with global code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig2); + + // create imported config with global code owner and import + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference(importMode, keyOfImportedCodeOwnerConfig1); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport(CodeOwnerConfigReference.create(importMode, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with global code owner and import - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(importMode, "/baz/OWNERS")) - .create(); - - // create config with global code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerEmail(user2.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config, the imported code // owner config and the code owner config that is imported by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1130,47 +1283,54 @@ throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with per file code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("foo.md") + .addCodeOwnerEmail(user2.email()) + .build()) + .create(); + + // create imported config with global code owner and import with import mode ALL + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport( + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig2)) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with global code owner and import with import mode // GLOBAL_CODE_OWNER_SETS_ONLY - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with global code owner and import with import mode ALL - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS")) - .create(); - - // create config with per file code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("foo.md") - .addCodeOwnerEmail(user2.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -1178,11 +1338,23 @@ // Expectation: we get the global owners from the importing code owner config and the imported // code owner config but not the per file code owner from the code owner config that is imported // by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfImportedCodeOwnerConfig2))); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1197,6 +1369,21 @@ private void testImportCodeOwnerConfigWithNameExtension(String nameOfImportedCodeOwnerConfig) throws Exception { + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .fileName(nameOfImportedCodeOwnerConfig) + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -1205,22 +1392,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, - "/bar/" + nameOfImportedCodeOwnerConfig)) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .fileName(nameOfImportedCodeOwnerConfig) - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -1231,47 +1405,74 @@ // Expectation: we get the global code owners from the importing and the imported code owner // config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void cyclicImports() throws Exception { + CodeOwnerConfigReference codeOwnerConfigReference2 = + CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS"); + + // create imported config with global code owner and that imports the importing config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") + .fileName("OWNERS") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with global code owner and that imports the importing config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/OWNERS")) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig, + importingCodeOwnerConfigKey, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1327,72 +1528,91 @@ @Test public void importWithRelativePath() throws Exception { + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/baz/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/foo/bar/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "../baz/OWNERS")) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/foo/baz/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importFromNonExistingProjectIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(Project.nameKey("non-existing"), "master", "/bar/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import from non-existing project - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(Project.nameKey("non-existing")) - .build()) + .addImport(codeOwnerConfigReference) .create(); Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format("project %s not found", keyOfImportedCodeOwnerConfig.project()))); } @Test @@ -1402,32 +1622,33 @@ ConfigInput configInput = new ConfigInput(); configInput.state = ProjectState.HIDDEN; gApi.projects().name(hiddenProject.get()).config(configInput); - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(hiddenProject) - .branch("master") - .folderPath("/") - .addCodeOwnerEmail(user.email()) - .create(); + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(hiddenProject) + .branch("master") + .folderPath("/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); // create importing config with global code owner and import from the hidden project - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/OWNERS") - .setProject(hiddenProject) - .build()) + .addImport(codeOwnerConfigReference) .create(); Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); @@ -1435,85 +1656,111 @@ // Expectation: we get the global owners from the importing code owner config, the global code // owners from the imported code owner config are ignored since the project that contains the // code owner config is hidden - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "state of project %s doesn't permit read", + keyOfImportedCodeOwnerConfig.project()))); } @Test public void importFromNonExistingBranchIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "non-existing", "/bar/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import from non-existing branch - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(project) - .setBranch("non-existing") - .build()) + .addImport(codeOwnerConfigReference) .create(); Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + "code owner config does not exist (revision = current)")); } @Test public void importFromOtherProject() throws Exception { Project.NameKey otherProject = projectOperations.newProject().create(); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1527,43 +1774,52 @@ // Create other branches in other project. createBranch(BranchNameKey.create(otherProject, branchName)); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch(branchName) + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") + .setProject(otherProject) + .build(); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch(branchName) .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch(branchName) - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead(branchName), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1572,44 +1828,50 @@ String otherBranch = "foo"; createBranch(BranchNameKey.create(project, otherBranch)); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch(otherBranch) + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(project) - .setBranch(otherBranch) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch(otherBranch) - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -1620,48 +1882,60 @@ String otherBranch = "foo"; createBranch(BranchNameKey.create(otherProject, otherBranch)); + // create imported config with global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch(otherBranch) + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config with global code owner and import with relative path - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS") - .setProject(otherProject) - .setBranch(otherBranch) - .build()) + .addImport(codeOwnerConfigReference) .create(); - // create imported config with global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch(otherBranch) - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo/bar/baz.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing and the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void nonResolveablePerFileImportIsIgnored() throws Exception { + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "master", "/non-existing/", "OWNERS"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with non-resolveable per file import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -1673,10 +1947,7 @@ CodeOwnerSet.builder() .addCodeOwnerEmail(admin.email()) .addPathExpression("foo.md") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, - "/non-existing/OWNERS")) + .addImport(codeOwnerConfigReference) .build()) .create(); @@ -1690,16 +1961,50 @@ // Expectation: we get the per file code owner from the importing code owner config, the // non-resolveable per file import is silently ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()).isTrue(); + assertThat(pathCodeOwnersResult.resolvedImports()).isEmpty(); + assertThat(pathCodeOwnersResult.unresolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createUnresolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + String.format( + "code owner config does not exist (revision = %s)", + projectOperations + .project(keyOfImportedCodeOwnerConfig.project()) + .getHead(keyOfImportedCodeOwnerConfig.branchNameKey().branch()) + .name()))); } @Test public void perFileImport() throws Exception { TestAccount user2 = accountCreator.user2(); + // create imported config with ignoreParentCodeOwners = true, a global code owner and a per file + // code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .ignoreParentCodeOwners() + .addCodeOwnerEmail(user.email()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("foo.md") + .addCodeOwnerEmail(user2.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config with per code owner and per file import CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -1711,28 +2016,10 @@ CodeOwnerSet.builder() .addPathExpression("foo.md") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .build()) .create(); - // create imported config with ignoreParentCodeOwners = true, a global code owner and a per file - // code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .ignoreParentCodeOwners() - .addCodeOwnerEmail(user.email()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("foo.md") - .addCodeOwnerEmail(user2.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -1744,23 +2031,58 @@ // Expectation: we get the per file code owners from the importing and the global code owner // from the imported code owner config, but not the per file code owner from the imported code // owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); // Expectation: the ignoreParentCodeOwners flag from the imported code owner config is ignored - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().ignoreParentCodeOwners()) - .isFalse(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.ignoreParentCodeOwners()).isFalse(); + + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void importsOfPerFileImportedCodeOwnerConfigAreResolved() throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with global code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create imported config with global code owner and global import + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with per file code owner and per file import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -1770,56 +2092,75 @@ CodeOwnerSet.builder() .addPathExpression("foo.md") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .build()) .create(); - // create imported config with global code owner and global import - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/baz/OWNERS")) - .create(); - - // create config with global code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerEmail(user2.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); // Expectation: we get the global owners from the importing code owner config, the imported code // owner config and the code owner config that is imported by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void onlyGlobalCodeOwnersAreImportedForTransitivePerFileImports() throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with per file code owner that is imported by the imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("foo.md") + .addCodeOwnerEmail(user2.email()) + .build()) + .create(); + + // create imported config with per global owner and global import with mode ALL + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport( + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig2)) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with per file code owner and per file import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) @@ -1829,39 +2170,14 @@ CodeOwnerSet.builder() .addPathExpression("foo.md") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .build()) .create(); - // create imported config with per global owner and global import with mode ALL - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS")) - .create(); - - // create config with per file code owner that is imported by the imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("foo.md") - .addCodeOwnerEmail(user2.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); @@ -1869,104 +2185,156 @@ // Expectation: we get the global owners from the importing code owner config and the imported // code owner config, but not the per file code owner from the code owner config that is // imported by the imported code owner config - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfImportedCodeOwnerConfig2))); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test public void onlyMatchingTransitivePerFileImportsAreImported() throws Exception { TestAccount user2 = accountCreator.user2(); + // create config with global code owner that is imported by the imported config for *.md files + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/md/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create config with global code owner that is imported by the imported config for *.txt files + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig3 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/txt/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference3 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig3); + + // create imported config with 2 per file imports, one for *.md files and one for *.txt + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.md") + .addImport(codeOwnerConfigReference2) + .build()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression("*.txt") + .addImport(codeOwnerConfigReference3) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config with global import - CodeOwnerConfig.Key rootCodeOwnerConfigKey = + CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations .newCodeOwnerConfig() .project(project) .branch("master") .folderPath("/") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config with 2 per file imports, one for *.md files and one for *.txt - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.md") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/md/OWNERS")) - .build()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression("*.txt") - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/txt/OWNERS")) - .build()) - .create(); - - // create config with global code owner that is imported by the imported config for *.md files - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/md/") - .addCodeOwnerEmail(user.email()) - .create(); - - // create config with global code owner that is imported by the imported config for *.txt files - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/txt/") - .addCodeOwnerEmail(user2.email()) - .create(); - // Expectation for foo.xyz file: code owners is empty since foo.xyz neither matches *.md nor // *.txt Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.xyz")); assertThat(pathCodeOwners).isPresent(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()).isEmpty(); + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()).isEmpty(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); // Expectation for foo.md file: code owners contains only user since foo.md only matches *.md pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.md")); assertThat(pathCodeOwners).isPresent(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); // Expectation for foo.txt file: code owners contains only user2 since foo.txt only matches // *.txt pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), - rootCodeOwnerConfigKey, + importingCodeOwnerConfigKey, projectOperations.project(project).getHead("master"), Paths.get("/foo.txt")); assertThat(pathCodeOwners).isPresent(); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user2.email()); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig3, + codeOwnerConfigReference3)); } @Test @@ -2090,6 +2458,20 @@ @Test public void perFileRuleThatIgnoresGlobalCodeOwnersCanImportGlobalCodeOwnersFromOtherFile() throws Exception { + // create imported config with a global code owner + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + // create importing config that: // * has a global code owner // * has a per-file import for md files @@ -2105,21 +2487,10 @@ CodeOwnerSet.builder() .addPathExpression(testPathExpressions.matchFileType("md")) .setIgnoreGlobalAndParentCodeOwners() - .addImport( - CodeOwnerConfigReference.create( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .build()) .create(); - // create imported config with a global code owner - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2131,11 +2502,17 @@ // Expectation: we get the global code owner from the imported code owner config (since it is // imported by a matching per-file rule), the global code owner from the importing code owner // config is ignored (since the matching per-file rule ignores parent and global code owners) - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -2146,6 +2523,24 @@ TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3", /* displayName= */ null); + // create imported config that has a matching per-file rule + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user2.email()) + .addCodeOwnerSet( + CodeOwnerSet.builder() + .addPathExpression(testPathExpressions.matchFileType("md")) + .addCodeOwnerEmail(user3.email()) + .build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, keyOfImportedCodeOwnerConfig); + // create importing config that has a global import with mode ALL and a per-file rule for md // files that ignores global and parent code owners CodeOwnerConfig.Key importingCodeOwnerConfigKey = @@ -2155,8 +2550,7 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS")) + .addImport(codeOwnerConfigReference) .addCodeOwnerSet( CodeOwnerSet.builder() .addPathExpression(testPathExpressions.matchFileType("md")) @@ -2165,20 +2559,6 @@ .build()) .create(); - // create imported config that has a matching per-file rule - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(project) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user2.email()) - .addCodeOwnerSet( - CodeOwnerSet.builder() - .addPathExpression(testPathExpressions.matchFileType("md")) - .addCodeOwnerEmail(user3.email()) - .build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2191,11 +2571,17 @@ // owner config and the code owner from the matching per-file rule in the imported code owner // config, the global code owners are ignored since there is a matching per-file rule that // ignores parent and global code owners - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(user.email(), user3.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -2204,6 +2590,35 @@ Project.NameKey otherProject = projectOperations.newProject().create(); + // create transitively imported config in other project + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/baz/") + .fileName("OWNERS") + .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create imported config in other project + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -2212,32 +2627,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "/bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config in other project - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/baz/OWNERS")) - .create(); - - // create transitively imported config in other project - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/baz/") - .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2248,11 +2640,21 @@ // Expectation: we get the global owners from the importing code owner config and from the // directly and transitively imported code owner configs in the other project - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); + assertThat(pathCodeOwnersResult.getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } @Test @@ -2261,6 +2663,35 @@ Project.NameKey otherProject = projectOperations.newProject().create(); + // create transitively imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/baz/") + .fileName("OWNERS") + .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig2); + + // create imported config + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(otherProject) + .branch("master") + .folderPath("/bar/") + .fileName("OWNERS") + .addCodeOwnerEmail(user.email()) + .addImport(codeOwnerConfigReference2) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig1); + // create importing config CodeOwnerConfig.Key importingCodeOwnerConfigKey = codeOwnerConfigOperations @@ -2269,32 +2700,9 @@ .branch("master") .folderPath("/") .addCodeOwnerEmail(admin.email()) - .addImport( - CodeOwnerConfigReference.builder( - CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "bar/OWNERS") - .setProject(otherProject) - .build()) + .addImport(codeOwnerConfigReference1) .create(); - // create imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/") - .addCodeOwnerEmail(user.email()) - .addImport(CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "baz/OWNERS")) - .create(); - - // create transitively imported config - codeOwnerConfigOperations - .newCodeOwnerConfig() - .project(otherProject) - .branch("master") - .folderPath("/bar/baz/") - .addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user2.email()).build()) - .create(); - Optional<PathCodeOwners> pathCodeOwners = pathCodeOwnersFactory.create( transientCodeOwnerConfigCacheProvider.get(), @@ -2305,11 +2713,21 @@ // Expectation: we get the global owners from the importing code owner config and from the // directly and transitively imported code owner configs + PathCodeOwnersResult pathCodeOwnersResult = pathCodeOwners.get().resolveCodeOwnerConfig().get(); assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().getPathCodeOwners()) .comparingElementsUsing(hasEmail()) .containsExactly(admin.email(), user.email(), user2.email()); - assertThat(pathCodeOwners.get().resolveCodeOwnerConfig().get().hasUnresolvedImports()) - .isFalse(); + assertThat(pathCodeOwnersResult.resolvedImports()) + .containsExactly( + CodeOwnerConfigImport.createResolvedImport( + importingCodeOwnerConfigKey, + keyOfImportedCodeOwnerConfig1, + codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig1, + keyOfImportedCodeOwnerConfig2, + codeOwnerConfigReference2)); + assertThat(pathCodeOwnersResult.unresolvedImports()).isEmpty(); } private CodeOwnerConfig.Builder createCodeOwnerBuilder() {
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java deleted file mode 100644 index ce0494f..0000000 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/UnresolvedImportTest.java +++ /dev/null
@@ -1,31 +0,0 @@ -// Copyright (C) 2021 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.plugins.codeowners.backend; - -import org.junit.Test; - -/** Tests for {@link UnresolvedImport}. */ -public class UnresolvedImportTest extends AbstractAutoValueTest { - @Test - public void toStringIncludesAllData() throws Exception { - UnresolvedImport unresolvedImport = - UnresolvedImport.create( - CodeOwnerConfig.Key.create(project, "master", "/"), - CodeOwnerConfig.Key.create(project, "master", "/bar/"), - CodeOwnerConfigReference.create(CodeOwnerConfigImportMode.ALL, "/bar/OWNERS"), - "test message"); - assertThatToStringIncludesAllData(unresolvedImport, UnresolvedImport.class); - } -}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java index 86801cc..17663b2 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java +++ b/javatests/com/google/gerrit/plugins/codeowners/backend/config/GeneralConfigTest.java
@@ -18,8 +18,10 @@ import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.DEFAULT_MAX_PATHS_IN_CHANGE_MESSAGES; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_ADD_REVIEWER; +import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_CODE_OWNER_CONFIG_FILES_WITH_FILE_EXTENSIONS; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_IMPLICIT_APPROVALS; +import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_STICKY_APPROVALS; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_BRANCH_CREATION; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED; import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT; @@ -51,12 +53,19 @@ import com.google.gerrit.plugins.codeowners.common.MergeCommitStrategy; import com.google.gerrit.server.git.validators.CommitValidationMessage; import com.google.gerrit.server.git.validators.ValidationMessage; +import com.google.gerrit.testing.ConfigSuite; import org.eclipse.jgit.lib.Config; import org.junit.Before; import org.junit.Test; /** Tests for {@link com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig}. */ public class GeneralConfigTest extends AbstractCodeOwnersTest { + @ConfigSuite.Default + public static Config defaultConfig() { + // Override the settings from AbstractCodeOwnersTest.defaultConfig(). + return new Config(); + } + private GeneralConfig generalConfig; @Before @@ -1642,6 +1651,63 @@ } @Test + public void cannotGetEnableStickyApprovalsForNullProject() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> generalConfig.enableStickyApprovals(/* project= */ null, new Config())); + assertThat(npe).hasMessageThat().isEqualTo("project"); + } + + @Test + public void cannotGetEnableStickyApprovalsForNullPluginConfig() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> generalConfig.enableStickyApprovals(project, /* pluginConfig= */ null)); + assertThat(npe).hasMessageThat().isEqualTo("pluginConfig"); + } + + @Test + public void noEnableStickyApprovals() throws Exception { + assertThat(generalConfig.enableStickyApprovals(project, new Config())).isFalse(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void + enableStickyApprovalsConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel() + throws Exception { + assertThat(generalConfig.enableStickyApprovals(project, new Config())).isTrue(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void + enableStickyApprovalsConfigurationInPluginConfigOverridesEnableStickyApprovalsConfigurationInGerritConfig() + throws Exception { + Config cfg = new Config(); + cfg.setString( + SECTION_CODE_OWNERS, /* subsection= */ null, KEY_ENABLE_STICKY_APPROVALS, "false"); + assertThat(generalConfig.enableStickyApprovals(project, cfg)).isFalse(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "true") + public void invalidEnableStickyApprovalsConfigurationInPluginConfigIsIgnored() throws Exception { + Config cfg = new Config(); + cfg.setString( + SECTION_CODE_OWNERS, /* subsection= */ null, KEY_ENABLE_STICKY_APPROVALS, "INVALID"); + assertThat(generalConfig.enableStickyApprovals(project, cfg)).isTrue(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableStickyApprovals", value = "INVALID") + public void invalidEnableStickyApprovalsConfigurationInGerritConfigIsIgnored() throws Exception { + assertThat(generalConfig.enableStickyApprovals(project, new Config())).isFalse(); + } + + @Test public void cannotGetGlobalCodeOwnersForNullPluginConfig() throws Exception { NullPointerException npe = assertThrows( @@ -2046,4 +2112,76 @@ throws Exception { assertThat(generalConfig.enableAsyncMessageOnAddReviewer(project, new Config())).isTrue(); } + + @Test + public void cannotGetEnableAsyncMessageOnCodeOwnerApprovalForNullProject() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + generalConfig.enableAsyncMessageOnCodeOwnerApproval( + /* project= */ null, new Config())); + assertThat(npe).hasMessageThat().isEqualTo("project"); + } + + @Test + public void cannotGetEnableAsyncMessageOnCodeOwnerApprovalForNullPluginConfig() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + generalConfig.enableAsyncMessageOnCodeOwnerApproval( + project, /* pluginConfig= */ null)); + assertThat(npe).hasMessageThat().isEqualTo("pluginConfig"); + } + + @Test + public void noEnableAsyncMessageOnCodeOwnerApproval() throws Exception { + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, new Config())).isTrue(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "false") + public void + enableAsyncMessageOnCodeOwnerApprovalConfigurationIsRetrievedFromGerritConfigIfNotSpecifiedOnProjectLevel() + throws Exception { + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, new Config())) + .isFalse(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "false") + public void + enableAsyncMessageOnCodeOwnerApprovalConfigurationInPluginConfigOverridesEnableAsyncMessageOnCodeOwnerApprovalConfigurationInGerritConfig() + throws Exception { + Config cfg = new Config(); + cfg.setString( + SECTION_CODE_OWNERS, + /* subsection= */ null, + KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + "true"); + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, cfg)).isTrue(); + } + + @Test + @GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", value = "false") + public void invalidEnableAsyncMessageOnCodeOwnerApprovalConfigurationInPluginConfigIsIgnored() + throws Exception { + Config cfg = new Config(); + cfg.setString( + SECTION_CODE_OWNERS, + /* subsection= */ null, + KEY_ENABLE_ASYNC_MESSAGE_ON_CODE_OWNER_APPROVAL, + "INVALID"); + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, cfg)).isFalse(); + } + + @Test + @GerritConfig( + name = "plugin.code-owners.enableAsyncMessageOnCodeOwnerApproval", + value = "INVALID") + public void invalidEnableAsyncMessageOnCodeOwnerApprovalConfigurationInGerritConfigIsIgnored() + throws Exception { + assertThat(generalConfig.enableAsyncMessageOnCodeOwnerApproval(project, new Config())).isTrue(); + } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java new file mode 100644 index 0000000..ed0b301 --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/restapi/CodeOwnerConfigFileJsonIT.java
@@ -0,0 +1,388 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.restapi; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.gerrit.entities.Project; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; +import com.google.gerrit.plugins.codeowners.api.CodeOwnerConfigFileInfo; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImport; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; +import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference; +import org.junit.Before; +import org.junit.Test; + +/** Tests for {@link CodeOwnerConfigFileJson}. */ +public class CodeOwnerConfigFileJsonIT extends AbstractCodeOwnersIT { + private CodeOwnerConfigFileJson CodeOwnerConfigFileJson; + + @Before + public void setUpCodeOwnersPlugin() throws Exception { + CodeOwnerConfigFileJson = plugin.getSysInjector().getInstance(CodeOwnerConfigFileJson.class); + } + + @Test + public void cannotFormatWithNullCodeOwnerConfigKey() throws Exception { + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + CodeOwnerConfigFileJson.format( + /* codeOwnerConfigKey= */ null, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of())); + assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigKey"); + } + + @Test + public void cannotFormatWithNullResolvedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/"); + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ null, + /* unresolvedImports= */ ImmutableList.of())); + assertThat(npe).hasMessageThat().isEqualTo("resolvedImports"); + } + + @Test + public void cannotFormatWithNullUnresolvedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + CodeOwnerConfig.Key.create(Project.nameKey("project"), "master", "/"); + NullPointerException npe = + assertThrows( + NullPointerException.class, + () -> + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ null)); + assertThat(npe).hasMessageThat().isEqualTo("unresolvedImports"); + } + + @Test + public void formatWithoutImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/baz/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of()); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.imports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + } + + @Test + public void formatWithUnresolvedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of(), + /* unresolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + codeOwnerConfigKey, + keyOfImportedCodeOwnerConfig, + codeOwnerConfigReference, + "error message"))); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.imports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.unresolvedImports).hasSize(1); + CodeOwnerConfigFileInfo unresolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.unresolvedImports); + assertThat(unresolvedImportInfo.project) + .isEqualTo(keyOfImportedCodeOwnerConfig.project().get()); + assertThat(unresolvedImportInfo.branch) + .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch()); + assertThat(unresolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()); + assertThat(unresolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode()); + assertThat(unresolvedImportInfo.imports).isNull(); + assertThat(unresolvedImportInfo.unresolvedImports).isNull(); + assertThat(unresolvedImportInfo.unresolvedErrorMessage).isEqualTo("error message"); + } + + @Test + public void formatWithImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz/"); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference)), + /* unresolvedImports= */ ImmutableList.of()); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo resolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(resolvedImportInfo.project).isEqualTo(keyOfImportedCodeOwnerConfig.project().get()); + assertThat(resolvedImportInfo.branch) + .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch()); + assertThat(resolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()); + assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode()); + assertThat(resolvedImportInfo.imports).isNull(); + assertThat(resolvedImportInfo.unresolvedImports).isNull(); + assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull(); + } + + @Test + public void formatWithNestedImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + + CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/baz") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, keyOfImportedCodeOwnerConfig); + + CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig1 = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz1/"); + CodeOwnerConfigReference nestedCodeOwnerConfigReference1 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, + keyOfNestedImportedCodeOwnerConfig1); + + CodeOwnerConfig.Key keyOfNestedImportedCodeOwnerConfig2 = + CodeOwnerConfig.Key.create(project, "stable", "/foo/baz2/"); + CodeOwnerConfigReference nestedCodeOwnerConfigReference2 = + createCodeOwnerConfigReference( + CodeOwnerConfigImportMode.ALL, keyOfNestedImportedCodeOwnerConfig2); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey, + /* resolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey, keyOfImportedCodeOwnerConfig, codeOwnerConfigReference), + CodeOwnerConfigImport.createResolvedImport( + keyOfImportedCodeOwnerConfig, + keyOfNestedImportedCodeOwnerConfig1, + nestedCodeOwnerConfigReference1)), + /* unresolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createUnresolvedImport( + keyOfImportedCodeOwnerConfig, + keyOfNestedImportedCodeOwnerConfig2, + nestedCodeOwnerConfigReference2, + "error message"))); + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo resolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(resolvedImportInfo.project).isEqualTo(keyOfImportedCodeOwnerConfig.project().get()); + assertThat(resolvedImportInfo.branch) + .isEqualTo(keyOfImportedCodeOwnerConfig.branchNameKey().branch()); + assertThat(resolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()); + assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference.importMode()); + assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull(); + + assertThat(resolvedImportInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo nestedResolvedImportInfo = + Iterables.getOnlyElement(resolvedImportInfo.imports); + assertThat(nestedResolvedImportInfo.project) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig1.project().get()); + assertThat(nestedResolvedImportInfo.branch) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig1.branchNameKey().branch()); + assertThat(nestedResolvedImportInfo.path) + .isEqualTo( + codeOwnerConfigOperations + .codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig1) + .getFilePath()); + assertThat(nestedResolvedImportInfo.importMode) + .isEqualTo(nestedCodeOwnerConfigReference1.importMode()); + assertThat(nestedResolvedImportInfo.imports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedImports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull(); + + assertThat(resolvedImportInfo.unresolvedImports).hasSize(1); + CodeOwnerConfigFileInfo nestedUnresolvedImportInfo1 = + Iterables.getOnlyElement(resolvedImportInfo.unresolvedImports); + assertThat(nestedUnresolvedImportInfo1.project) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig2.project().get()); + assertThat(nestedUnresolvedImportInfo1.branch) + .isEqualTo(keyOfNestedImportedCodeOwnerConfig2.branchNameKey().branch()); + assertThat(nestedUnresolvedImportInfo1.path) + .isEqualTo( + codeOwnerConfigOperations + .codeOwnerConfig(keyOfNestedImportedCodeOwnerConfig2) + .getFilePath()); + assertThat(nestedUnresolvedImportInfo1.importMode) + .isEqualTo(nestedCodeOwnerConfigReference2.importMode()); + assertThat(nestedUnresolvedImportInfo1.imports).isNull(); + assertThat(nestedUnresolvedImportInfo1.unresolvedImports).isNull(); + assertThat(nestedUnresolvedImportInfo1.unresolvedErrorMessage).isEqualTo("error message"); + } + + @Test + public void formatWithCyclicImports() throws Exception { + CodeOwnerConfig.Key codeOwnerConfigKey1 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference1 = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey1); + + CodeOwnerConfig.Key codeOwnerConfigKey2 = + codeOwnerConfigOperations + .newCodeOwnerConfig() + .project(project) + .branch("master") + .folderPath("/foo/bar/baz") + .addCodeOwnerEmail(admin.email()) + .create(); + CodeOwnerConfigReference codeOwnerConfigReference2 = + createCodeOwnerConfigReference(CodeOwnerConfigImportMode.ALL, codeOwnerConfigKey2); + + CodeOwnerConfigFileInfo codeOwnerConfigFileInfo = + CodeOwnerConfigFileJson.format( + codeOwnerConfigKey1, + /* resolvedImports= */ ImmutableList.of( + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey1, codeOwnerConfigKey2, codeOwnerConfigReference1), + CodeOwnerConfigImport.createResolvedImport( + codeOwnerConfigKey2, codeOwnerConfigKey1, codeOwnerConfigReference2)), + /* unresolvedImports= */ ImmutableList.of()); + + assertThat(codeOwnerConfigFileInfo.project).isEqualTo(codeOwnerConfigKey1.project().get()); + assertThat(codeOwnerConfigFileInfo.branch) + .isEqualTo(codeOwnerConfigKey1.branchNameKey().branch()); + assertThat(codeOwnerConfigFileInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath()); + assertThat(codeOwnerConfigFileInfo.importMode).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedImports).isNull(); + assertThat(codeOwnerConfigFileInfo.unresolvedErrorMessage).isNull(); + + assertThat(codeOwnerConfigFileInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo resolvedImportInfo = + Iterables.getOnlyElement(codeOwnerConfigFileInfo.imports); + assertThat(resolvedImportInfo.project).isEqualTo(codeOwnerConfigKey2.project().get()); + assertThat(resolvedImportInfo.branch).isEqualTo(codeOwnerConfigKey2.branchNameKey().branch()); + assertThat(resolvedImportInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath()); + assertThat(resolvedImportInfo.importMode).isEqualTo(codeOwnerConfigReference1.importMode()); + assertThat(resolvedImportInfo.unresolvedErrorMessage).isNull(); + + assertThat(resolvedImportInfo.imports).hasSize(1); + CodeOwnerConfigFileInfo nestedResolvedImportInfo = + Iterables.getOnlyElement(resolvedImportInfo.imports); + assertThat(nestedResolvedImportInfo.project).isEqualTo(codeOwnerConfigKey1.project().get()); + assertThat(nestedResolvedImportInfo.branch) + .isEqualTo(codeOwnerConfigKey1.branchNameKey().branch()); + assertThat(nestedResolvedImportInfo.path) + .isEqualTo(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath()); + assertThat(nestedResolvedImportInfo.importMode) + .isEqualTo(codeOwnerConfigReference1.importMode()); + assertThat(nestedResolvedImportInfo.imports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedImports).isNull(); + assertThat(nestedResolvedImportInfo.unresolvedErrorMessage).isNull(); + } +}
diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index c06fa38..0000000 --- a/package-lock.json +++ /dev/null
@@ -1,15891 +0,0 @@ -{ - "name": "code-owners", - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@bazel/rollup": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@bazel/rollup/-/rollup-1.7.0.tgz", - "integrity": "sha512-Pp5aCJw3gwu77zn6/fQgZ39ArrWEI5O3dja5wKadBnfOQ66PImIEr+bf7JgROoWvACH1kGxaS423rq51fiuCsA==", - "dev": true - }, - "@polymer/iron-test-helpers": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@polymer/iron-test-helpers/-/iron-test-helpers-3.0.1.tgz", - "integrity": "sha512-2R7dnGcW2eg95i7LhYWWUO4AlAk6qXsPnKoyeN2R1t0km0ECMx0jjwqeLwCo8/7LwuVPZSiarI4DK7jyU7fJLQ==", - "dev": true, - "requires": { - "@polymer/polymer": "^3.0.0" - } - }, - "@polymer/polymer": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz", - "integrity": "sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==", - "dev": true, - "requires": { - "@webcomponents/shadycss": "^1.9.1" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "@webcomponents/shadycss": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.10.1.tgz", - "integrity": "sha512-XEVDA7oH6o4Au9apyRDucjcIzvP44Ur4sqTMGRKCcE6sCAeKiOkRE03TCYNJAFkzckMWWT8Xx3IxG3iwjAcsRQ==", - "dev": true - }, - "@webcomponents/webcomponentsjs": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz", - "integrity": "sha512-eLH04VBMpuZGzBIhOnUjECcQPEPcmfhWEijW9u1B5I+2PPYdWf3vWUExdDxu4Y3GljRSTCOlWnGtS9tpzmXMyQ==", - "dev": true - }, - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true - }, - "ajv": { - "version": "6.12.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", - "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", - "is-string": "^1.0.5" - } - }, - "array.prototype.flat": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", - "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bower": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.8.tgz", - "integrity": "sha512-1SrJnXnkP9soITHptSO+ahx3QKp3cVzn8poI6ujqc5SeOkg5iqM1pK9H+DSc2OQ8SnO0jC/NG4Ur/UIwy7574A==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "comment-parser": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.6.tgz", - "integrity": "sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.1.0.tgz", - "integrity": "sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz", - "integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==", - "dev": true - }, - "domhandler": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", - "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.3.0.tgz", - "integrity": "sha512-xWC75PM3QF6MjE5e58OzwTX0B/rPQnlqH0YyXB/c056RtVJA+eu60da2I/bdnEHzEYC00g8QaZUlAbqOZVbOsw==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-config-google": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.13.0.tgz", - "integrity": "sha512-ELgMdOIpn0CFdsQS+FuxO+Ttu4p+aLaXHv9wA9yVnzqlUGV7oN/eRRnJekk7TCur6Cu2FXX0fqfIXRBaM14lpQ==", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", - "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "resolve": "^1.13.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", - "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-html": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-6.1.0.tgz", - "integrity": "sha512-xcqithhnjUxoEDRL0hYci4RSS8EZ1NGr3/H8x3BxJvxgbu4R3YaEUea9i93j95NuAgoAbOUfNmybta8fqi4UbA==", - "dev": true, - "requires": { - "htmlparser2": "^4.1.0" - } - }, - "eslint-plugin-import": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", - "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", - "dev": true, - "requires": { - "array-includes": "^3.1.1", - "array.prototype.flat": "^1.2.3", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.3", - "eslint-module-utils": "^2.6.0", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.1", - "read-pkg-up": "^2.0.0", - "resolve": "^1.17.0", - "tsconfig-paths": "^3.9.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-plugin-jsdoc": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-19.2.0.tgz", - "integrity": "sha512-QdNifBFLXCDGdy+26RXxcrqzEZarFWNybCZQVqJQYEYPlxd6lm+LPkrs6mCOhaGc2wqC6zqpedBQFX8nQJuKSw==", - "dev": true, - "requires": { - "comment-parser": "^0.7.2", - "debug": "^4.1.1", - "jsdoctypeparser": "^6.1.0", - "lodash": "^4.17.15", - "object.entries-ponyfill": "^1.0.1", - "regextras": "^0.7.0", - "semver": "^6.3.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true - }, - "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true - }, - "htmlparser2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", - "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-callable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", - "integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==", - "dev": true - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-negative-zero": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", - "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", - "dev": true - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "dev": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsdoctypeparser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz", - "integrity": "sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", - "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.0", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", - "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } - } - }, - "object.entries-ponyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz", - "integrity": "sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY=", - "dev": true - }, - "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - }, - "polymer-bridges": { - "version": "file:../../polymer-bridges", - "dev": true - }, - "polymer-cli": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/polymer-cli/-/polymer-cli-1.9.11.tgz", - "integrity": "sha512-tiURjHDCOUUtDVPuVYvrfFI9PXe4OOUmBbn6Sg5GJNQ2POtP7r7hv+I5yI8P9qsxmalHTa19chVtf5/t9IBXDg==", - "dev": true, - "requires": { - "@octokit/rest": "^16.2.0", - "@types/chalk": "^2.2.0", - "@types/del": "^3.0.0", - "@types/findup-sync": "^0.3.29", - "@types/globby": "^6.1.0", - "@types/inquirer": "0.0.32", - "@types/merge-stream": "^1.0.28", - "@types/mz": "^0.0.31", - "@types/request": "2.0.3", - "@types/resolve": "0.0.4", - "@types/rimraf": "^0.0.28", - "@types/semver": "^5.3.30", - "@types/temp": "^0.8.28", - "@types/update-notifier": "^1.0.0", - "@types/vinyl": "^2.0.0", - "@types/vinyl-fs": "0.0.28", - "@types/yeoman-generator": "^2.0.3", - "bower": "^1.8.8", - "bower-json": "^0.8.1", - "bower-logger": "^0.2.2", - "chalk": "^2.4.2", - "chokidar": "^1.7.0", - "command-line-args": "^5.0.2", - "command-line-commands": "^2.0.1", - "command-line-usage": "^5.0.5", - "del": "^3.0.0", - "findup-sync": "^0.4.2", - "globby": "^8.0.1", - "gunzip-maybe": "^1.3.1", - "inquirer": "^1.0.2", - "merge-stream": "^1.0.1", - "mz": "^2.6.0", - "plylog": "^1.0.0", - "polymer-analyzer": "^3.2.2", - "polymer-build": "^3.1.4", - "polymer-bundler": "^4.0.9", - "polymer-linter": "^3.0.0", - "polymer-project-config": "^4.0.3", - "polyserve": "^0.27.15", - "request": "^2.72.0", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar-fs": "^1.12.0", - "temp": "^0.8.3", - "update-notifier": "^1.0.0", - "validate-element-name": "^2.1.1", - "vinyl": "^1.1.1", - "vinyl-fs": "^2.4.3", - "web-component-tester": "^6.9.0", - "yeoman-environment": "^1.5.2", - "yeoman-generator": "^3.1.1" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.4.tgz", - "integrity": "sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.5.0", - "@babel/helpers": "^7.5.4", - "@babel/parser": "^7.5.0", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.0", - "@babel/types": "^7.5.0", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.11", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.0.tgz", - "integrity": "sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==", - "dev": true, - "requires": { - "@babel/types": "^7.5.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-call-delegate": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", - "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-define-map": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", - "integrity": "sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/types": "^7.4.4", - "lodash": "^4.17.11" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", - "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", - "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-transforms": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz", - "integrity": "sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/template": "^7.4.4", - "@babel/types": "^7.4.4", - "lodash": "^4.17.11" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.4.4.tgz", - "integrity": "sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-wrap-function": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-replace-supers": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz", - "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.0.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-simple-access": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", - "dev": true, - "requires": { - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-wrap-function": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", - "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.2.0" - } - }, - "@babel/helpers": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz", - "integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==", - "dev": true, - "requires": { - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.0", - "@babel/types": "^7.5.0" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.0.tgz", - "integrity": "sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==", - "dev": true - }, - "@babel/plugin-external-helpers": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-external-helpers/-/plugin-external-helpers-7.2.0.tgz", - "integrity": "sha512-QFmtcCShFkyAsNtdCM3lJPmRe1iB+vPZymlB4LnDIKEBj2yKQLQKtoxXxJ8ePT5fwMl4QGg303p4mB0UsSI2/g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", - "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0", - "@babel/plugin-syntax-async-generators": "^7.2.0" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz", - "integrity": "sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", - "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", - "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz", - "integrity": "sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", - "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", - "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", - "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz", - "integrity": "sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "lodash": "^4.17.11" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz", - "integrity": "sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-define-map": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.4.4", - "@babel/helper-split-export-declaration": "^7.4.4", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", - "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz", - "integrity": "sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", - "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", - "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", - "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", - "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-instanceof": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-instanceof/-/plugin-transform-instanceof-7.2.0.tgz", - "integrity": "sha512-tw2fb96tpcd5XaJXns19tGKo/SeIUS0exAteHJ/7EP27Bke7RmV/gAftHCf1WKZgKeZOUfzOL7nrXH2HIH9auA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", - "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", - "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", - "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", - "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", - "dev": true, - "requires": { - "@babel/helper-call-delegate": "^7.4.4", - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", - "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.0" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", - "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", - "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", - "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", - "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", - "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", - "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" - } - }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/traverse": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz", - "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.5.0", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.0", - "@babel/types": "^7.5.0", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz", - "integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.0.0" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" - }, - "@octokit/endpoint": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.2.1.tgz", - "integrity": "sha512-GoUsRSRhtbCQugRY8eDWg5BnsczUZNq00qArrP7tKPHFmvz2KzJ8DoEq6IAQhLGwAOBHbZQ/Zml3DiaEKAWwkA==", - "dev": true, - "requires": { - "deepmerge": "4.0.0", - "is-plain-object": "^3.0.0", - "universal-user-agent": "^2.1.0", - "url-template": "^2.0.8" - } - }, - "@octokit/request": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-4.1.1.tgz", - "integrity": "sha512-LOyL0i3oxRo418EXRSJNk/3Q4I0/NKawTn6H/CQp+wnrG1UFLGu080gSsgnWobhPo5BpUNgSQ5BRk5FOOJhD1Q==", - "dev": true, - "requires": { - "@octokit/endpoint": "^5.1.0", - "@octokit/request-error": "^1.0.1", - "deprecation": "^2.0.0", - "is-plain-object": "^3.0.0", - "node-fetch": "^2.3.0", - "once": "^1.4.0", - "universal-user-agent": "^2.1.0" - } - }, - "@octokit/request-error": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.0.4.tgz", - "integrity": "sha512-L4JaJDXn8SGT+5G0uX79rZLv0MNJmfGa4vb4vy1NnpjSnWDLJRy6m90udGwvMmavwsStgbv2QNkPzzTCMmL+ig==", - "dev": true, - "requires": { - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/rest": { - "version": "16.28.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.28.2.tgz", - "integrity": "sha512-csuYiHvJ1P/GFDadVn0QhwO83R1+YREjcwCY7ZIezB6aJTRIEidJZj+R7gAkUhT687cqYb4cXTZsDVu9F+Fmug==", - "dev": true, - "requires": { - "@octokit/request": "^4.0.1", - "@octokit/request-error": "^1.0.2", - "atob-lite": "^2.0.0", - "before-after-hook": "^1.4.0", - "btoa-lite": "^1.0.0", - "deprecation": "^2.0.0", - "lodash.get": "^4.4.2", - "lodash.set": "^4.3.2", - "lodash.uniq": "^4.5.0", - "octokit-pagination-methods": "^1.1.0", - "once": "^1.4.0", - "universal-user-agent": "^2.0.0", - "url-template": "^2.0.8" - } - }, - "@polymer/esm-amd-loader": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@polymer/esm-amd-loader/-/esm-amd-loader-1.0.4.tgz", - "integrity": "sha512-h+hqYkL+tQV/y2ESD5gFXMl5z4cC+XY1jTlBeGSBaTcj3VbB5OBEScbvRXm63NcEbBneQQYbHfBAXAkF9i9wIA==", - "dev": true - }, - "@polymer/sinonjs": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@polymer/sinonjs/-/sinonjs-1.17.1.tgz", - "integrity": "sha512-/U8F/cOTrbF2iVVYgINYmvKbtbexs+89Q3v8AaHADRYabTg7aOZGOb0RyWpOI+sUJt04kj63U4FwMhzW5r4wZA==", - "dev": true - }, - "@polymer/test-fixture": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@polymer/test-fixture/-/test-fixture-0.0.3.tgz", - "integrity": "sha1-REN1JpfU2Sk7vEEuoLXk00HxSdk=", - "dev": true - }, - "@polymer/tools-common": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@polymer/tools-common/-/tools-common-2.0.1.tgz", - "integrity": "sha512-McFmpKINBpdDkIz7jf+hIddUDYLaeEV+SFqfoNJEYZwgXZta145fy/SlXSKj31RcAjroxfocRplN/p3HLYwloQ==", - "requires": { - "@types/node": "^9.6.4", - "depcheck": "^0.6.0", - "fs-extra": "^3.0.0", - "gulp-eslint": "^4.0.0", - "gulp-mocha": "^6.0.0", - "gulp-tslint": "^8.0.0", - "gulp-typescript": "^3.0.0", - "merge-stream": "^1.0.0", - "run-sequence": "^1.0.0" - }, - "dependencies": { - "@types/node": { - "version": "9.6.49", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.49.tgz", - "integrity": "sha512-YY0Okyn4QXC4ugJI+Kng5iWjK8A6eIHiQVaGIhJkyn0YL6Iqo0E0tBC8BuhvYcBK87vykBijM5FtMnCqaa5anA==" - }, - "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - } - } - }, - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/formatio": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", - "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", - "requires": { - "samsam": "1.3.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", - "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" - }, - "@types/babel-generator": { - "version": "6.25.3", - "resolved": "https://registry.npmjs.org/@types/babel-generator/-/babel-generator-6.25.3.tgz", - "integrity": "sha512-pGgnuxVddKcYIc+VJkRDop7gxLhqclNKBdlsm/5Vp8d+37pQkkDK7fef8d9YYImRzw9xcojEPc18pUYnbxmjqA==", - "dev": true, - "requires": { - "@types/babel-types": "*" - } - }, - "@types/babel-traverse": { - "version": "6.25.5", - "resolved": "https://registry.npmjs.org/@types/babel-traverse/-/babel-traverse-6.25.5.tgz", - "integrity": "sha512-WrMbwmu+MWf8FiUMbmVOGkc7bHPzndUafn1CivMaBHthBBoo0VNIcYk1KV71UovYguhsNOwf3UF5oRmkkGOU3w==", - "dev": true, - "requires": { - "@types/babel-types": "*" - } - }, - "@types/babel-types": { - "version": "6.25.2", - "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-6.25.2.tgz", - "integrity": "sha512-+3bMuktcY4a70a0KZc8aPJlEOArPuAKQYHU5ErjkOqGJdx8xuEEVK6nWogqigBOJ8nKPxRpyCUDTCPmZ3bUxGA==", - "dev": true - }, - "@types/babylon": { - "version": "6.16.5", - "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", - "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", - "dev": true, - "requires": { - "@types/babel-types": "*" - } - }, - "@types/bluebird": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.27.tgz", - "integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==", - "dev": true - }, - "@types/body-parser": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", - "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/chai": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", - "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", - "dev": true - }, - "@types/chai-subset": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.2.tgz", - "integrity": "sha512-VMA1aOXwPEJADlj5ykmYv77YKmbEuAxiLz/+lT6vFIWQ1EA06jF01TytVBAbVTNk0pjfW1Uhw5R5MaEq426N0A==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, - "@types/chalk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-2.2.0.tgz", - "integrity": "sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==", - "dev": true, - "requires": { - "chalk": "*" - } - }, - "@types/chokidar": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz", - "integrity": "sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ==", - "requires": { - "@types/events": "*", - "@types/node": "*" - } - }, - "@types/clean-css": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz", - "integrity": "sha512-A1HQhQ0hkvqqByJMgg+Wiv9p9XdoYEzuwm11SVo1mX2/4PSdhjcrUlilJQoqLscIheC51t1D5g+EFWCXZ2VTQQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/clone": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", - "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=", - "dev": true - }, - "@types/compression": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-0.0.33.tgz", - "integrity": "sha1-ldxzOiM5qoRjgdfxN3eS0lU9wn0=", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/connect": { - "version": "3.4.32", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", - "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/content-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.3.tgz", - "integrity": "sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==", - "dev": true - }, - "@types/cssbeautify": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@types/cssbeautify/-/cssbeautify-0.3.1.tgz", - "integrity": "sha1-jgvuj33suVIlDaDK6+BeMFkcF+8=", - "dev": true - }, - "@types/del": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/del/-/del-3.0.1.tgz", - "integrity": "sha512-y6qRq6raBuu965clKgx6FHuiPu3oHdtmzMPXi8Uahsjdq1L6DL5fS/aY5/s71YwM7k6K1QIWvem5vNwlnNGIkQ==", - "dev": true, - "requires": { - "@types/glob": "*" - } - }, - "@types/doctrine": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.1.tgz", - "integrity": "sha1-uZny2fe0PKvgoaLzm8IDvH3K2p0=", - "dev": true - }, - "@types/escape-html": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-0.0.20.tgz", - "integrity": "sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==", - "dev": true - }, - "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, - "@types/express": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", - "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.16.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz", - "integrity": "sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/range-parser": "*" - } - }, - "@types/fancy-log": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@types/fancy-log/-/fancy-log-1.3.0.tgz", - "integrity": "sha512-mQjDxyOM1Cpocd+vm1kZBP7smwKZ4TNokFeds9LV7OZibmPJFEzY3+xZMrKfUdNT71lv8GoCPD6upKwHxubClw==" - }, - "@types/fast-levenshtein": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.1.tgz", - "integrity": "sha1-OjYVzxc2Rcj8pY0FHk4ygk5L0oY=", - "dev": true - }, - "@types/findup-sync": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@types/findup-sync/-/findup-sync-0.3.30.tgz", - "integrity": "sha512-Dpt1x3rhz6t8BMTS4vziTVos8VLkF4RngIxMBCSE6w0STmnVEEaoe3w+BG5xHyZXshye9lyZE99lpBDoLGY8eA==", - "dev": true, - "requires": { - "@types/minimatch": "*" - } - }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/freeport": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@types/freeport/-/freeport-1.0.21.tgz", - "integrity": "sha1-c/ZUPtZ9PKP/+XuYVZFZi3CSBm8=", - "dev": true, - "optional": true - }, - "@types/fs-extra": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz", - "integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha512-RHv6ZQjcTncXo3thYZrsbAVwoy4vSKosSWhuhuQxLOTv74OJuFQxXkmUuZCr3q9uNBEVCvIzmZL/FeRNbHZGUg==", - "dev": true, - "requires": { - "@types/glob": "*", - "@types/node": "*" - } - }, - "@types/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-j3XSDNoK4LO5T+ZviQD6PqfEjm07QFEacOTbJR3hnLWuWX0ZMLJl9oRPgj1PyrfGbXhfHFkksC9QZ9HFltJyrw==", - "dev": true, - "requires": { - "@types/glob": "*" - } - }, - "@types/gulp-if": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/gulp-if/-/gulp-if-0.0.33.tgz", - "integrity": "sha512-J5lzff21X7r1x/4hSzn02GgIUEyjCqYIXZ9GgGBLhbsD3RiBdqwnkFWgF16/0jO5rcVZ52Zp+6MQMQdvIsWuKg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/vinyl": "*" - } - }, - "@types/html-minifier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-3.5.3.tgz", - "integrity": "sha512-j1P/4PcWVVCPEy5lofcHnQ6BtXz9tHGiFPWzqm7TtGuWZEfCHEP446HlkSNc9fQgNJaJZ6ewPtp2aaFla/Uerg==", - "dev": true, - "requires": { - "@types/clean-css": "*", - "@types/relateurl": "*", - "@types/uglify-js": "*" - } - }, - "@types/inquirer": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-0.0.32.tgz", - "integrity": "sha1-pKCOg3QcUAp8PI53dgFPf4plhw0=", - "requires": { - "@types/rx": "*", - "@types/through": "*" - } - }, - "@types/is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@types/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-byTuSHMdMRaOpRBhDW3RXl/Jxv8=", - "dev": true - }, - "@types/launchpad": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/launchpad/-/launchpad-0.6.0.tgz", - "integrity": "sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=", - "dev": true, - "optional": true - }, - "@types/merge-stream": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/merge-stream/-/merge-stream-1.1.2.tgz", - "integrity": "sha512-7faLmaE99g/yX0Y9pF1neh2IUqOf/fXMOWCVzsXjqI1EJ91lrgXmaBKf6bRWM164lLyiHxHt6t/ZO/cIzq61XA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/mime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", - "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mz": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/@types/mz/-/mz-0.0.31.tgz", - "integrity": "sha1-pNgMCC/v5x5Ap8DwfR5lVbu8e1I=", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.1.tgz", - "integrity": "sha512-rp7La3m845mSESCgsJePNL/JQyhkOJA6G4vcwvVgkDAwHhGdq5GCumxmPjEk1MZf+8p5ZQAUE7tqgQRQTXN7uQ==" - }, - "@types/opn": { - "version": "3.0.28", - "resolved": "https://registry.npmjs.org/@types/opn/-/opn-3.0.28.tgz", - "integrity": "sha1-CX0NHJtXSVc6XZbfEyOHu20CEYo=", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/parse5": { - "version": "2.2.34", - "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-2.2.34.tgz", - "integrity": "sha1-44cKEOgnNacg9i1x3NGDunjvOp0=", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/path-is-inside": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/path-is-inside/-/path-is-inside-1.0.0.tgz", - "integrity": "sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==", - "dev": true - }, - "@types/pem": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@types/pem/-/pem-1.9.5.tgz", - "integrity": "sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/puppeteer": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/@types/puppeteer/-/puppeteer-1.12.4.tgz", - "integrity": "sha512-aaGbJaJ9TuF9vZfTeoh876sBa+rYJWPwtsmHmYr28pGr42ewJnkDTq2aeSKEmS39SqUdkwLj73y/d7rBSp7mDQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", - "dev": true - }, - "@types/relateurl": { - "version": "0.2.28", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz", - "integrity": "sha1-a9p9uGU/piZD9e5p6facEaOS46Y=", - "dev": true - }, - "@types/request": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.0.3.tgz", - "integrity": "sha512-cIvnyFRARxwE4OHpCyYue7H+SxaKFPpeleRCHJicft8QhyTNbVYsMwjvEzEPqG06D2LGHZ+sN5lXc8+bTu6D8A==", - "dev": true, - "requires": { - "@types/form-data": "*", - "@types/node": "*" - } - }, - "@types/resolve": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.4.tgz", - "integrity": "sha1-m1htZalH3qiMS8JNoLkF/pUgoNU=", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/rimraf": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz", - "integrity": "sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY=", - "dev": true - }, - "@types/rx": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/rx/-/rx-4.1.1.tgz", - "integrity": "sha1-WY/JSla67ZdfGUV04PVy/Y5iekg=", - "requires": { - "@types/rx-core": "*", - "@types/rx-core-binding": "*", - "@types/rx-lite": "*", - "@types/rx-lite-aggregates": "*", - "@types/rx-lite-async": "*", - "@types/rx-lite-backpressure": "*", - "@types/rx-lite-coincidence": "*", - "@types/rx-lite-experimental": "*", - "@types/rx-lite-joinpatterns": "*", - "@types/rx-lite-testing": "*", - "@types/rx-lite-time": "*", - "@types/rx-lite-virtualtime": "*" - } - }, - "@types/rx-core": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/rx-core/-/rx-core-4.0.3.tgz", - "integrity": "sha1-CzNUsSOM7b4rdPYybxOdvHpZHWA=" - }, - "@types/rx-core-binding": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/rx-core-binding/-/rx-core-binding-4.0.4.tgz", - "integrity": "sha512-5pkfxnC4w810LqBPUwP5bg7SFR/USwhMSaAeZQQbEHeBp57pjKXRlXmqpMrLJB4y1oglR/c2502853uN0I+DAQ==", - "requires": { - "@types/rx-core": "*" - } - }, - "@types/rx-lite": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/rx-lite/-/rx-lite-4.0.6.tgz", - "integrity": "sha512-oYiDrFIcor9zDm0VDUca1UbROiMYBxMLMaM6qzz4ADAfOmA9r1dYEcAFH+2fsPI5BCCjPvV9pWC3X3flbrvs7w==", - "requires": { - "@types/rx-core": "*", - "@types/rx-core-binding": "*" - } - }, - "@types/rx-lite-aggregates": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/rx-lite-aggregates/-/rx-lite-aggregates-4.0.3.tgz", - "integrity": "sha512-MAGDAHy8cRatm94FDduhJF+iNS5//jrZ/PIfm+QYw9OCeDgbymFHChM8YVIvN2zArwsRftKgE33QfRWvQk4DPg==", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-async": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/rx-lite-async/-/rx-lite-async-4.0.2.tgz", - "integrity": "sha512-vTEv5o8l6702ZwfAM5aOeVDfUwBSDOs+ARoGmWAKQ6LOInQ8J4/zjM7ov12fuTpktUKdMQjkeCp07Vd73mPkxw==", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-backpressure": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/rx-lite-backpressure/-/rx-lite-backpressure-4.0.3.tgz", - "integrity": "sha512-Y6aIeQCtNban5XSAF4B8dffhIKu6aAy/TXFlScHzSxh6ivfQBQw6UjxyEJxIOt3IT49YkS+siuayM2H/Q0cmgA==", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-coincidence": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/rx-lite-coincidence/-/rx-lite-coincidence-4.0.3.tgz", - "integrity": "sha512-1VNJqzE9gALUyMGypDXZZXzR0Tt7LC9DdAZQ3Ou/Q0MubNU35agVUNXKGHKpNTba+fr8GdIdkC26bRDqtCQBeQ==", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-experimental": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/rx-lite-experimental/-/rx-lite-experimental-4.0.1.tgz", - "integrity": "sha1-xTL1y98/LBXaFt7Ykw0bKYQCPL0=", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-joinpatterns": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/rx-lite-joinpatterns/-/rx-lite-joinpatterns-4.0.1.tgz", - "integrity": "sha1-9w/jcFGKhDLykVjMkv+1a05K/D4=", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-testing": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/rx-lite-testing/-/rx-lite-testing-4.0.1.tgz", - "integrity": "sha1-IbGdEfTf1v/vWp0WSOnIh5v+Iek=", - "requires": { - "@types/rx-lite-virtualtime": "*" - } - }, - "@types/rx-lite-time": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/rx-lite-time/-/rx-lite-time-4.0.3.tgz", - "integrity": "sha512-ukO5sPKDRwCGWRZRqPlaAU0SKVxmWwSjiOrLhoQDoWxZWg6vyB9XLEZViKOzIO6LnTIQBlk4UylYV0rnhJLxQw==", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/rx-lite-virtualtime": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/rx-lite-virtualtime/-/rx-lite-virtualtime-4.0.3.tgz", - "integrity": "sha512-3uC6sGmjpOKatZSVHI2xB1+dedgml669ZRvqxy+WqmGJDVusOdyxcKfyzjW0P3/GrCiN4nmRkLVMhPwHCc5QLg==", - "requires": { - "@types/rx-lite": "*" - } - }, - "@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", - "dev": true - }, - "@types/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", - "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" - } - }, - "@types/sinon": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-5.0.7.tgz", - "integrity": "sha512-opwMHufhUwkn/UUDk35LDbKJpA2VBsZT8WLU8NjayvRLGPxQkN+8XmfC2Xl35MAscBE8469koLLBjaI3XLEIww==" - }, - "@types/spdy": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@types/spdy/-/spdy-3.4.4.tgz", - "integrity": "sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/temp": { - "version": "0.8.34", - "resolved": "https://registry.npmjs.org/@types/temp/-/temp-0.8.34.tgz", - "integrity": "sha512-oLa9c5LHXgS6UimpEVp08De7QvZ+Dfu5bMQuWyMhf92Z26Q10ubEMOWy9OEfUdzW7Y/sDWVHmUaLFtmnX/2j0w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/through": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", - "integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==", - "requires": { - "@types/node": "*" - } - }, - "@types/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=" - }, - "@types/ua-parser-js": { - "version": "0.7.33", - "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.33.tgz", - "integrity": "sha512-ngUKcHnytUodUCL7C6EZ+lVXUjTMQb+9p/e1JjV5tN9TVzS98lHozWEFRPY1QcCdwFeMsmVWfZ3DPPT/udCyIw==", - "dev": true - }, - "@types/uglify-js": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", - "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@types/update-notifier": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-1.0.3.tgz", - "integrity": "sha512-BLStNhP2DFF7funARwTcoD6tetRte8NK3Sc59mn7GNALCN975jOlKX3dGvsFxXr/HwQMxxCuRn9IWB3WQ7odHQ==", - "dev": true - }, - "@types/uuid": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz", - "integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/vinyl": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.3.tgz", - "integrity": "sha512-hrT6xg16CWSmndZqOTJ6BGIn2abKyTw0B58bI+7ioUoj3Sma6u8ftZ1DTI2yCaJamOVGLOnQWiPH3a74+EaqTA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/vinyl-fs": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-0.0.28.tgz", - "integrity": "sha1-RmMBe8gCxlcOrk80Cf1cq/l8v94=", - "dev": true, - "requires": { - "@types/glob-stream": "*", - "@types/node": "*", - "@types/vinyl": "*" - } - }, - "@types/whatwg-url": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-6.4.0.tgz", - "integrity": "sha512-tonhlcbQ2eho09am6RHnHOgvtDfDYINd5rgxD+2YSkKENooVCFsWizJz139MQW/PV8FfClyKrNe9ZbdHrSCxGg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@types/which/-/which-1.3.1.tgz", - "integrity": "sha512-ZrJDWpvg75LTGX4XwuneY9s6bF3OeZcGTpoGh3zDV9ytzcHMFsRrMIaLBRJZQMBoGyKs6unBQfVdrLZiYfb1zQ==", - "dev": true, - "optional": true - }, - "@types/yeoman-generator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/yeoman-generator/-/yeoman-generator-2.0.3.tgz", - "integrity": "sha512-vch2UFd6k7DdfWEv/alRwZIRXQoxZNUDpfLOK24+005dzE1HVnwSWfETF3WxJnWlsOcH87wU4uzldAE/7F/6Lw==", - "requires": { - "@types/events": "*", - "@types/inquirer": "*" - } - }, - "@types/yeoman-test": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@types/yeoman-test/-/yeoman-test-1.7.4.tgz", - "integrity": "sha512-nppkWxmwWX2yuvuHqWjYZOxMuhjeKJ0CK6IONDBzYiLEC7l5dQR9RMTyBqtIXKrzX7PDMQmZDsKHfWds7anI7Q==", - "requires": { - "@types/events": "*", - "@types/yeoman-generator": "*" - } - }, - "@webcomponents/webcomponentsjs": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz", - "integrity": "sha512-eLH04VBMpuZGzBIhOnUjECcQPEPcmfhWEijW9u1B5I+2PPYdWf3vWUExdDxu4Y3GljRSTCOlWnGtS9tpzmXMyQ==", - "dev": true - }, - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - } - }, - "accessibility-developer-tools": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz", - "integrity": "sha1-PaDM6dbsY3OWS4TzXbfPw996tRQ=", - "dev": true - }, - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" - }, - "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" - } - } - }, - "adm-zip": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", - "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==", - "dev": true, - "optional": true - }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - }, - "dependencies": { - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } - } - } - }, - "ajv": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", - "integrity": "sha512-w1YQaVGNC6t2UCPjEawK/vo/dG8OOrVtUmhBT1uJJYxbl5kU2Tj3v6LGqBcsysN1yhuCStJCCA3GqdvKY8sqXQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=" - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "optional": true - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "requires": { - "ansi-wrap": "^0.1.0" - } - }, - "ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", - "dev": true - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } - }, - "append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=", - "dev": true - }, - "archiver": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", - "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=", - "dev": true, - "requires": { - "archiver-utils": "^1.3.0", - "async": "^2.0.0", - "buffer-crc32": "^0.2.1", - "glob": "^7.0.0", - "lodash": "^4.8.0", - "readable-stream": "^2.0.0", - "tar-stream": "^1.5.0", - "zip-stream": "^1.2.0" - } - }, - "archiver-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", - "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "graceful-fs": "^4.1.0", - "lazystream": "^1.0.0", - "lodash": "^4.8.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" - }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "requires": { - "lodash": "^4.17.11" - } - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "atob-lite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz", - "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - } - } - }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - } - } - }, - "babel-helper-evaluate-path": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.5.0.tgz", - "integrity": "sha512-mUh0UhS607bGh5wUMAQfOpt2JX2ThXMtppHRdRU1kL7ZLRWIXxoV2UIV1r2cAeeNeU1M5SB5/RSUgUxrK8yOkA==", - "dev": true - }, - "babel-helper-flip-expressions": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.4.3.tgz", - "integrity": "sha1-NpZzahKKwYvCUlS19AoizrPB0/0=", - "dev": true - }, - "babel-helper-is-nodes-equiv": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz", - "integrity": "sha1-NOmzALFHnd2Y7HfqC76TQt/jloQ=", - "dev": true - }, - "babel-helper-is-void-0": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-is-void-0/-/babel-helper-is-void-0-0.4.3.tgz", - "integrity": "sha1-fZwBtFYee5Xb2g9u7kj1tg5nMT4=", - "dev": true - }, - "babel-helper-mark-eval-scopes": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.4.3.tgz", - "integrity": "sha1-0kSjvvmESHJgP/tG4izorN9VFWI=", - "dev": true - }, - "babel-helper-remove-or-void": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.4.3.tgz", - "integrity": "sha1-pPA7QAd6D/6I5F0HAQ3uJB/1rmA=", - "dev": true - }, - "babel-helper-to-multiple-sequence-expressions": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz", - "integrity": "sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA==", - "dev": true - }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", - "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-minify-builtins": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz", - "integrity": "sha512-wpqbN7Ov5hsNwGdzuzvFcjgRlzbIeVv1gMIlICbPj0xkexnfoIDe7q+AZHMkQmAE/F9R5jkrB6TLfTegImlXag==", - "dev": true - }, - "babel-plugin-minify-constant-folding": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.5.0.tgz", - "integrity": "sha512-Vj97CTn/lE9hR1D+jKUeHfNy+m1baNiJ1wJvoGyOBUx7F7kJqDZxr9nCHjO/Ad+irbR3HzR6jABpSSA29QsrXQ==", - "dev": true, - "requires": { - "babel-helper-evaluate-path": "^0.5.0" - } - }, - "babel-plugin-minify-dead-code-elimination": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.5.0.tgz", - "integrity": "sha512-XQteBGXlgEoAKc/BhO6oafUdT4LBa7ARi55mxoyhLHNuA+RlzRmeMAfc31pb/UqU01wBzRc36YqHQzopnkd/6Q==", - "dev": true, - "requires": { - "babel-helper-evaluate-path": "^0.5.0", - "babel-helper-mark-eval-scopes": "^0.4.3", - "babel-helper-remove-or-void": "^0.4.3", - "lodash.some": "^4.6.0" - } - }, - "babel-plugin-minify-flip-comparisons": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.4.3.tgz", - "integrity": "sha1-AMqHDLjxO0XAOLPB68DyJyk8llo=", - "dev": true, - "requires": { - "babel-helper-is-void-0": "^0.4.3" - } - }, - "babel-plugin-minify-guarded-expressions": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.4.3.tgz", - "integrity": "sha1-zHCbRFP9IbHzAod0RMifiEJ845c=", - "dev": true, - "requires": { - "babel-helper-flip-expressions": "^0.4.3" - } - }, - "babel-plugin-minify-infinity": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.4.3.tgz", - "integrity": "sha1-37h2obCKBldjhO8/kuZTumB7Oco=", - "dev": true - }, - "babel-plugin-minify-mangle-names": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.5.0.tgz", - "integrity": "sha512-3jdNv6hCAw6fsX1p2wBGPfWuK69sfOjfd3zjUXkbq8McbohWy23tpXfy5RnToYWggvqzuMOwlId1PhyHOfgnGw==", - "dev": true, - "requires": { - "babel-helper-mark-eval-scopes": "^0.4.3" - } - }, - "babel-plugin-minify-numeric-literals": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.4.3.tgz", - "integrity": "sha1-jk/VYcefeAEob/YOjF/Z3u6TwLw=", - "dev": true - }, - "babel-plugin-minify-replace": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.5.0.tgz", - "integrity": "sha512-aXZiaqWDNUbyNNNpWs/8NyST+oU7QTpK7J9zFEFSA0eOmtUNMU3fczlTTTlnCxHmq/jYNFEmkkSG3DDBtW3Y4Q==", - "dev": true - }, - "babel-plugin-minify-simplify": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.5.0.tgz", - "integrity": "sha512-TM01J/YcKZ8XIQd1Z3nF2AdWHoDsarjtZ5fWPDksYZNsoOjQ2UO2EWm824Ym6sp127m44gPlLFiO5KFxU8pA5Q==", - "dev": true, - "requires": { - "babel-helper-flip-expressions": "^0.4.3", - "babel-helper-is-nodes-equiv": "^0.0.1", - "babel-helper-to-multiple-sequence-expressions": "^0.5.0" - } - }, - "babel-plugin-minify-type-constructors": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.4.3.tgz", - "integrity": "sha1-G8bxW4f3qxCF1CszC3F2V6IVZQA=", - "dev": true, - "requires": { - "babel-helper-is-void-0": "^0.4.3" - } - }, - "babel-plugin-transform-inline-consecutive-adds": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz", - "integrity": "sha1-Mj1Ho+pjqDp6w8gRro5pQfrysNE=", - "dev": true - }, - "babel-plugin-transform-member-expression-literals": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.9.4.tgz", - "integrity": "sha1-NwOcmgwzE6OUlfqsL/OmtbnQOL8=", - "dev": true - }, - "babel-plugin-transform-merge-sibling-variables": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.9.4.tgz", - "integrity": "sha1-hbQi/DN3tEnJ0c3kQIcgNTJAHa4=", - "dev": true - }, - "babel-plugin-transform-minify-booleans": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.9.4.tgz", - "integrity": "sha1-rLs+VqNVXdI5KOS1gtKFFi3SsZg=", - "dev": true - }, - "babel-plugin-transform-property-literals": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.9.4.tgz", - "integrity": "sha1-mMHSHiVXNlc/k+zlRFn2ziSYXTk=", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "babel-plugin-transform-regexp-constructors": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz", - "integrity": "sha1-WLd3W2OvzzMyj66aX4j71PsLSWU=", - "dev": true - }, - "babel-plugin-transform-remove-console": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz", - "integrity": "sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A=", - "dev": true - }, - "babel-plugin-transform-remove-debugger": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.9.4.tgz", - "integrity": "sha1-QrcnYxyXl44estGZp67IShgznvI=", - "dev": true - }, - "babel-plugin-transform-remove-undefined": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.5.0.tgz", - "integrity": "sha512-+M7fJYFaEE/M9CXa0/IRkDbiV3wRELzA1kKQFCJ4ifhrzLKn/9VCCgj9OFmYWwBd8IB48YdgPkHYtbYq+4vtHQ==", - "dev": true, - "requires": { - "babel-helper-evaluate-path": "^0.5.0" - } - }, - "babel-plugin-transform-simplify-comparison-operators": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.9.4.tgz", - "integrity": "sha1-9ir+CWyrDh9ootdT/fKDiIRxzrk=", - "dev": true - }, - "babel-plugin-transform-undefined-to-void": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz", - "integrity": "sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA=", - "dev": true - }, - "babel-preset-minify": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.5.0.tgz", - "integrity": "sha512-xj1s9Mon+RFubH569vrGCayA9Fm2GMsCgDRm1Jb8SgctOB7KFcrVc2o8K3YHUyMz+SWP8aea75BoS8YfsXXuiA==", - "dev": true, - "requires": { - "babel-plugin-minify-builtins": "^0.5.0", - "babel-plugin-minify-constant-folding": "^0.5.0", - "babel-plugin-minify-dead-code-elimination": "^0.5.0", - "babel-plugin-minify-flip-comparisons": "^0.4.3", - "babel-plugin-minify-guarded-expressions": "^0.4.3", - "babel-plugin-minify-infinity": "^0.4.3", - "babel-plugin-minify-mangle-names": "^0.5.0", - "babel-plugin-minify-numeric-literals": "^0.4.3", - "babel-plugin-minify-replace": "^0.5.0", - "babel-plugin-minify-simplify": "^0.5.0", - "babel-plugin-minify-type-constructors": "^0.4.3", - "babel-plugin-transform-inline-consecutive-adds": "^0.4.3", - "babel-plugin-transform-member-expression-literals": "^6.9.4", - "babel-plugin-transform-merge-sibling-variables": "^6.9.4", - "babel-plugin-transform-minify-booleans": "^6.9.4", - "babel-plugin-transform-property-literals": "^6.9.4", - "babel-plugin-transform-regexp-constructors": "^0.4.3", - "babel-plugin-transform-remove-console": "^6.9.4", - "babel-plugin-transform-remove-debugger": "^6.9.4", - "babel-plugin-transform-remove-undefined": "^0.5.0", - "babel-plugin-transform-simplify-comparison-operators": "^6.9.4", - "babel-plugin-transform-undefined-to-void": "^6.9.4", - "lodash.isplainobject": "^4.0.6" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - }, - "dependencies": { - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" - } - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - }, - "dependencies": { - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" - } - } - }, - "babylon": { - "version": "7.0.0-beta.47", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.47.tgz", - "integrity": "sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==", - "dev": true - }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, - "base64-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", - "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", - "dev": true - }, - "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=" - }, - "before-after-hook": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-1.4.0.tgz", - "integrity": "sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==", - "dev": true - }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "binaryextensions": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.1.2.tgz", - "integrity": "sha512-xVNN69YGDghOqCCtA6FI7avYrr02mTJjOgB0/f1VPD3pJC8QEvjTKWc4epDx8AqxxA75NI0QpVM2gPJXUbE4Tg==" - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "dev": true, - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - } - } - }, - "bower": { - "version": "1.8.8", - "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.8.tgz", - "integrity": "sha512-1SrJnXnkP9soITHptSO+ahx3QKp3cVzn8poI6ujqc5SeOkg5iqM1pK9H+DSc2OQ8SnO0jC/NG4Ur/UIwy7574A==", - "dev": true - }, - "bower-config": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bower-config/-/bower-config-1.4.1.tgz", - "integrity": "sha1-hf2d82fCuNu9DKpMXyutQM2Ewsw=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.3", - "mout": "^1.0.0", - "optimist": "^0.6.1", - "osenv": "^0.1.3", - "untildify": "^2.1.0" - } - }, - "bower-json": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/bower-json/-/bower-json-0.8.1.tgz", - "integrity": "sha1-lsFHIyQa5kZqnFLhbKoyYjqIOEM=", - "dev": true, - "requires": { - "deep-extend": "^0.4.0", - "ext-name": "^3.0.0", - "graceful-fs": "^4.1.3", - "intersect": "^1.0.1" - } - }, - "bower-logger": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/bower-logger/-/bower-logger-0.2.2.tgz", - "integrity": "sha1-Ob4H6Xmy/I4DqUY0IF7ZQiNz04E=", - "dev": true - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "browser-capabilities": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/browser-capabilities/-/browser-capabilities-1.1.4.tgz", - "integrity": "sha512-BezMQhbQklxjRQpZZQ8tnbzEo6AldUwMh8/PeWt5/CTBSwByQRXZEAK2fbnEahQ4poeeaI0suAYRq25A1YGOmw==", - "dev": true, - "requires": { - "@types/ua-parser-js": "^0.7.31", - "ua-parser-js": "^0.7.15" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" - }, - "browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", - "dev": true, - "requires": { - "pako": "~0.2.0" - } - }, - "browserstack": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.2.tgz", - "integrity": "sha512-+6AFt9HzhKykcPF79W6yjEUJcdvZOV0lIXdkORXMJftGrDl0OKWqRF4GHqpDNkxiceDT/uB7Fb/aDwktvXX7dg==", - "dev": true, - "optional": true, - "requires": { - "https-proxy-agent": "^2.2.1" - } - }, - "btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", - "dev": true - }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", - "dev": true, - "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "requires": { - "callsites": "^0.2.0" - } - }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, - "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=" - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "cancel-token": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/cancel-token/-/cancel-token-0.1.1.tgz", - "integrity": "sha1-wYGXZ0uxyEwdaTPr8V2NWlznm08=", - "dev": true, - "requires": { - "@types/node": "^4.0.30" - }, - "dependencies": { - "@types/node": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-4.9.3.tgz", - "integrity": "sha512-Q9eESThBvAbfEzznF1qTAKUoPbJEbK3lTSO0S3mICvmG/vUSZ+HnCtidpuB58Po7CJt5A2goKsDiYScN8d1V4A==", - "dev": true - } - } - }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", - "dev": true - }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "dev": true, - "requires": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - } - }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", - "dev": true - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==" - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "clean-css": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", - "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "cleankill": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cleankill/-/cleankill-2.0.0.tgz", - "integrity": "sha1-WYMN/ItBHVPccq0J1Fp46jMWGpE=", - "dev": true - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "cli-cursor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", - "dev": true, - "requires": { - "restore-cursor": "^1.0.1" - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "requires": { - "colors": "1.0.3" - }, - "dependencies": { - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=" - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=" - }, - "cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "colornames": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", - "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=", - "dev": true - }, - "colors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", - "dev": true - }, - "colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "dev": true, - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "command-line-args": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz", - "integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==", - "dev": true, - "requires": { - "array-back": "^3.0.1", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - } - }, - "command-line-commands": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/command-line-commands/-/command-line-commands-2.0.1.tgz", - "integrity": "sha512-m8c2p1DrNd2ruIAggxd/y6DgygQayf6r8RHwchhXryaLF8I6koYjoYroVP+emeROE9DXN5b9sP1Gh+WtvTTdtQ==", - "dev": true, - "requires": { - "array-back": "^2.0.0" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", - "dev": true - } - } - }, - "command-line-usage": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-5.0.5.tgz", - "integrity": "sha512-d8NrGylA5oCXSbGoKz05FkehDAzSmIm4K03S5VDh4d5lZAtTWfc3D1RuETtuQCn8129nYfJfDdF7P/lwcz1BlA==", - "dev": true, - "requires": { - "array-back": "^2.0.0", - "chalk": "^2.4.1", - "table-layout": "^0.4.3", - "typical": "^2.6.1" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", - "dev": true - } - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" - }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, - "compress-commons": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", - "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", - "dev": true, - "requires": { - "buffer-crc32": "^0.2.1", - "crc32-stream": "^2.0.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", - "dev": true, - "requires": { - "mime-db": ">= 1.40.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", - "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", - "dev": true, - "requires": { - "crc": "^3.4.4", - "readable-stream": "^2.0.0" - } - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", - "dev": true - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "css-slam": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/css-slam/-/css-slam-2.1.2.tgz", - "integrity": "sha512-cObrY+mhFEmepWpua6EpMrgRNTQ0eeym+kvR0lukI6hDEzK7F8himEDS4cJ9+fPHCoArTzVrrR0d+oAUbTR1NA==", - "dev": true, - "requires": { - "command-line-args": "^5.0.2", - "command-line-usage": "^5.0.5", - "dom5": "^3.0.0", - "parse5": "^4.0.0", - "shady-css-parser": "^0.1.0" - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, - "cssbeautify": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cssbeautify/-/cssbeautify-0.3.1.tgz", - "integrity": "sha1-Et0fc0A1wub6ymfcvc73TkKBE5c=", - "dev": true - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "dargs": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-6.1.0.tgz", - "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.0.0.tgz", - "integrity": "sha512-YZ1rOP5+kHor4hMAH+HRQnBQHg+wvS1un1hAOuIcxcBy0hzcUf6Jg2a1w65kpoOUnurOfZbERwjI1TfZxNjcww==", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "dev": true, - "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" - }, - "dependencies": { - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "depcheck": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-0.6.11.tgz", - "integrity": "sha512-wTVJ8cNilB8NfkzoBblcYqsB8LRfbjqKEwAOLD3YXIRigktSM7/lS9xQfVkAVujhjstmiQMZR0hkdHSnQxzb9A==", - "requires": { - "babel-traverse": "^6.7.3", - "babylon": "^6.1.21", - "builtin-modules": "^1.1.1", - "deprecate": "^1.0.0", - "deps-regex": "^0.1.4", - "js-yaml": "^3.4.2", - "lodash": "^4.5.1", - "minimatch": "^3.0.2", - "require-package-name": "^2.0.1", - "walkdir": "0.0.11", - "yargs": "^8.0.2" - }, - "dependencies": { - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - } - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "deprecate": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/deprecate/-/deprecate-1.1.0.tgz", - "integrity": "sha512-b5dDNQYdy2vW9WXUD8+RQlfoxvqztLLhDE+T7Gd37I5E8My7nJkKu6FmhdDeRWJ8B+yjZKuwjCta8pgi8kgSqA==" - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true - }, - "deps-regex": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.1.4.tgz", - "integrity": "sha1-UYZnt2kUYKXn4KNBvnbrfOgJAYQ=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-conflict": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/detect-conflict/-/detect-conflict-1.0.1.tgz", - "integrity": "sha1-CIZXpmqWHAUBnbfEIwiDsca0F24=" - }, - "detect-file": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", - "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", - "dev": true, - "requires": { - "fs-exists-sync": "^0.1.0" - } - }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true - }, - "diagnostics": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", - "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", - "dev": true, - "requires": { - "colorspace": "1.1.x", - "enabled": "1.0.x", - "kuler": "1.0.x" - } - }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "dev": true, - "requires": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dom-urls/-/dom-urls-1.1.0.tgz", - "integrity": "sha1-AB3fgWKM0ecGElxxdvU8zsVdkY4=", - "dev": true, - "requires": { - "urijs": "^1.16.1" - } - }, - "dom5": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dom5/-/dom5-3.0.1.tgz", - "integrity": "sha512-JPFiouQIr16VQ4dX6i0+Hpbg3H2bMKPmZ+WZgBOSSvOPx9QHwwY8sPzeM2baUtViESYto6wC2nuZOMC/6gulcA==", - "dev": true, - "requires": { - "@types/parse5": "^2.2.34", - "clone": "^2.1.0", - "parse5": "^4.0.0" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "editions": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", - "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", - "requires": { - "errlop": "^1.1.1", - "semver": "^5.6.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "ejs": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.2.tgz", - "integrity": "sha512-PcW2a0tyTuPHz3tWyYqtK6r1fZ3gp+3Sop8Ph+ZYN81Ob5rwmbHEzaqs10N3BEsaGTkh/ooniXK+WwszGlc2+Q==" - }, - "emitter-component": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz", - "integrity": "sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=", - "dev": true - }, - "enabled": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", - "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", - "dev": true, - "requires": { - "env-variable": "0.0.x" - } - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "ends-with": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ends-with/-/ends-with-0.2.0.tgz", - "integrity": "sha1-L52pjVelDP2kVxzkM5AAUA9Oa4o=", - "dev": true - }, - "engine.io": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz", - "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~6.1.0" - }, - "dependencies": { - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-client": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz", - "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~6.1.0", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", - "dev": true, - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.5", - "has-binary2": "~1.0.2" - } - }, - "env-variable": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", - "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==", - "dev": true - }, - "errlop": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/errlop/-/errlop-1.1.1.tgz", - "integrity": "sha512-WX7QjiPHhsny7/PQvrhS5VMizXXKoKCS3udaBp8gjlARdbn+XmK300eKBAAN0hGyRaTCtRpOaxK+xFVPUJ3zkw==", - "requires": { - "editions": "^2.1.2" - } - }, - "error": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", - "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", - "requires": { - "string-template": "~0.2.1", - "xtend": "~4.0.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.0.1.tgz", - "integrity": "sha512-J3ZkwbEnnO+fGAKrjVpeUAnZshAdfZvbhQpqfIH9kSAspReRC4nJnu8ewm55b4y9ElyeuhCTzJD0XiH8Tsbhlw==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", - "requires": { - "ajv": "^5.3.0", - "babel-code-frame": "^6.22.0", - "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^3.7.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.4", - "esquery": "^1.0.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.0.1", - "ignore": "^3.3.3", - "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", - "progress": "^2.0.0", - "regexpp": "^1.0.1", - "require-uncached": "^1.0.3", - "semver": "^5.3.0", - "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "4.0.2", - "text-table": "~0.2.0" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - } - } - }, - "eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==" - }, - "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", - "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit-hook": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", - "dev": true - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "requires": { - "fill-range": "^2.1.0" - } - }, - "expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", - "dev": true, - "requires": { - "os-homedir": "^1.0.1" - } - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - } - } - } - }, - "ext-list": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", - "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", - "dev": true, - "requires": { - "mime-db": "^1.28.0" - } - }, - "ext-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-3.0.0.tgz", - "integrity": "sha1-B+RBhzfLH1E8MsbqSNi4yOBHGrs=", - "dev": true, - "requires": { - "ends-with": "^0.2.0", - "ext-list": "^2.0.0", - "meow": "^3.1.0", - "sort-keys-length": "^1.0.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "external-editor": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", - "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "spawn-sync": "^1.0.15", - "tmp": "^0.0.29" - }, - "dependencies": { - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.1" - } - } - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "requires": { - "is-extglob": "^1.0.0" - } - }, - "extract-zip": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", - "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", - "requires": { - "concat-stream": "1.6.2", - "debug": "2.6.9", - "mkdirp": "0.5.1", - "yauzl": "2.4.1" - }, - "dependencies": { - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", - "requires": { - "pend": "~1.2.0" - } - }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "requires": { - "fd-slicer": "~1.0.1" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fast-safe-stringify": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", - "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==", - "dev": true - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", - "dev": true, - "optional": true, - "requires": { - "pend": "~1.2.0" - } - }, - "fecha": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", - "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" - }, - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "filled-array": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filled-array/-/filled-array-1.1.0.tgz", - "integrity": "sha1-w8T2xmO5I0WamqKZEtLQMfFQf4Q=", - "dev": true - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - } - }, - "find-port": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-port/-/find-port-1.0.1.tgz", - "integrity": "sha1-2whKbL+ZVk2Zhprnn73s9m6KGFw=", - "dev": true, - "requires": { - "async": "~0.2.9" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true - } - } - }, - "find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "dev": true, - "requires": { - "array-back": "^3.0.1" - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "findup-sync": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", - "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", - "dev": true, - "requires": { - "detect-file": "^0.1.0", - "is-glob": "^2.0.1", - "micromatch": "^2.3.7", - "resolve-dir": "^0.1.0" - } - }, - "first-chunk-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=" - }, - "flat-cache": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", - "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", - "requires": { - "circular-json": "^0.3.1", - "graceful-fs": "^4.1.2", - "rimraf": "~2.6.2", - "write": "^0.2.1" - } - }, - "follow-redirects": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", - "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", - "dev": true, - "requires": { - "debug": "^3.2.6" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "fork-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", - "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.x" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "freeport": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/freeport/-/freeport-1.0.5.tgz", - "integrity": "sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=", - "dev": true, - "optional": true - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, - "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "dependencies": { - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "gh-got": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-6.0.0.tgz", - "integrity": "sha512-F/mS+fsWQMo1zfgG9MD8KWvTWPPzzhuVwY++fhQ5Ggd+0P+CAMHtzMZhNxG+TqGfHDChJKsbh6otfMGqO2AKBw==", - "requires": { - "got": "^7.0.0", - "is-plain-obj": "^1.1.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "got": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", - "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", - "requires": { - "decompress-response": "^3.2.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-plain-obj": "^1.1.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "p-cancelable": "^0.3.0", - "p-timeout": "^1.1.1", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "url-parse-lax": "^1.0.0", - "url-to-options": "^1.0.1" - } - } - } - }, - "github-username": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/github-username/-/github-username-4.1.0.tgz", - "integrity": "sha1-y+KABBiDIG2kISrp5LXxacML9Bc=", - "requires": { - "gh-got": "^6.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "requires": { - "is-glob": "^2.0.0" - } - }, - "glob-stream": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", - "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", - "requires": { - "extend": "^3.0.0", - "glob": "^5.0.3", - "glob-parent": "^3.0.0", - "micromatch": "^2.3.7", - "ordered-read-streams": "^0.3.0", - "through2": "^0.6.0", - "to-absolute-glob": "^0.1.1", - "unique-stream": "^2.0.2" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "^2.1.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true, - "requires": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - }, - "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - } - } - }, - "global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" - }, - "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - } - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "globby": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", - "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", - "requires": { - "array-union": "^1.0.1", - "dir-glob": "2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", - "requires": { - "sparkles": "^1.0.0" - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" - }, - "grouped-queue": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/grouped-queue/-/grouped-queue-0.3.3.tgz", - "integrity": "sha1-wWfSpTGcWg4JZO9qJbfC34mWyFw=", - "requires": { - "lodash": "^4.17.2" - } - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" - }, - "gulp-eslint": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp-eslint/-/gulp-eslint-4.0.2.tgz", - "integrity": "sha512-fcFUQzFsN6dJ6KZlG+qPOEkqfcevRUXgztkYCvhNvJeSvOicC8ucutN4qR/ID8LmNZx9YPIkBzazTNnVvbh8wg==", - "requires": { - "eslint": "^4.0.0", - "fancy-log": "^1.3.2", - "plugin-error": "^1.0.0" - } - }, - "gulp-if": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz", - "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=", - "dev": true, - "requires": { - "gulp-match": "^1.0.3", - "ternary-stream": "^2.0.1", - "through2": "^2.0.1" - } - }, - "gulp-match": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.0.3.tgz", - "integrity": "sha1-kcfA1/Kb7NZgbVfYCn+Hdqh6uo4=", - "dev": true, - "requires": { - "minimatch": "^3.0.3" - } - }, - "gulp-mocha": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gulp-mocha/-/gulp-mocha-6.0.0.tgz", - "integrity": "sha512-FfBldW5ttnDpKf4Sg6/BLOOKCCbr5mbixDGK1t02/8oSrTCwNhgN/mdszG3cuQuYNzuouUdw4EH/mlYtgUscPg==", - "requires": { - "dargs": "^5.1.0", - "execa": "^0.10.0", - "mocha": "^5.2.0", - "npm-run-path": "^2.0.2", - "plugin-error": "^1.0.1", - "supports-color": "^5.4.0", - "through2": "^2.0.3" - }, - "dependencies": { - "dargs": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-5.1.0.tgz", - "integrity": "sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk=" - }, - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.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" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - } - } - }, - "gulp-sourcemaps": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", - "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", - "requires": { - "convert-source-map": "^1.1.1", - "graceful-fs": "^4.1.2", - "strip-bom": "^2.0.0", - "through2": "^2.0.0", - "vinyl": "^1.0.0" - } - }, - "gulp-tslint": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/gulp-tslint/-/gulp-tslint-8.1.4.tgz", - "integrity": "sha512-wBoZIEMJRz9urHwolsvQpngA9l931p6g/Liwz1b/KrsVP6jEBFZv/o0NS1TFCQZi/l8mXxz8+v3twhf4HOXxPQ==", - "requires": { - "@types/fancy-log": "1.3.0", - "ansi-colors": "^1.0.1", - "fancy-log": "1.3.3", - "map-stream": "~0.0.7", - "plugin-error": "1.0.1", - "through": "~2.3.8" - } - }, - "gulp-typescript": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-3.2.4.tgz", - "integrity": "sha512-bZosNvbUGzFA4bjjWoUPyjU5vfgJSzlYKkU0Jutbsrj+td8yvtqxethhqfzB9MwyamaUODIuidj5gIytZ523Bw==", - "requires": { - "gulp-util": "~3.0.7", - "source-map": "~0.5.3", - "through2": "~2.0.1", - "vinyl-fs": "~2.4.3" - } - }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=" - }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "requires": { - "readable-stream": "~1.1.9" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" - } - }, - "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" - } - }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "requires": { - "duplexer2": "0.0.2" - } - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "requires": { - "glogg": "^1.0.0" - } - }, - "gunzip-maybe": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.1.tgz", - "integrity": "sha512-qtutIKMthNJJgeHQS7kZ9FqDq59/Wn0G2HYCRNjpup7yKfVI6/eqwpmroyZGFoCYaG+sW6psNVb4zoLADHpp2g==", - "dev": true, - "requires": { - "browserify-zlib": "^0.1.4", - "is-deflate": "^1.0.0", - "is-gzip": "^1.0.0", - "peek-stream": "^1.1.0", - "pumpify": "^1.3.3", - "through2": "^2.0.3" - } - }, - "handle-thing": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", - "dev": true - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "has-color": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "requires": { - "sparkles": "^1.0.0" - } - }, - "has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", - "requires": { - "has-symbol-support-x": "^1.4.1" - } - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, - "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "dev": true, - "requires": { - "eventemitter3": "^3.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz", - "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=", - "dev": true, - "requires": { - "http-proxy": "^1.16.2", - "is-glob": "^3.1.0", - "lodash": "^4.17.2", - "micromatch": "^2.3.11" - }, - "dependencies": { - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz", - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indent": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/indent/-/indent-0.0.2.tgz", - "integrity": "sha1-jHnwgBkFWbaHA0uEx676l9WpEdk=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", - "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", - "dev": true, - "requires": { - "ansi-escapes": "^1.1.0", - "chalk": "^1.0.0", - "cli-cursor": "^1.0.1", - "cli-width": "^2.0.0", - "external-editor": "^1.1.0", - "figures": "^1.3.5", - "lodash": "^4.3.0", - "mute-stream": "0.0.6", - "pinkie-promise": "^2.0.0", - "run-async": "^2.2.0", - "rx": "^4.1.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" - }, - "intersect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/intersect/-/intersect-1.0.1.tgz", - "integrity": "sha1-MyZQ4QhU2MCsWMGSvcJ6i/fnoww=", - "dev": true - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-deflate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha1-yGKQHDwWH7CdrHzcfnhPgOmPLxQ=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-gzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=", - "dev": true - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" - }, - "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "dev": true, - "requires": { - "isobject": "^4.0.0" - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" - }, - "is-potential-custom-element-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", - "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" - }, - "is-scoped": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-1.0.0.tgz", - "integrity": "sha1-RJypgpnnEwOCViieyytUDcQ3yzA=", - "requires": { - "scoped-regex": "^1.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-valid-glob": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", - "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "requires": { - "buffer-alloc": "^1.2.0" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - } - }, - "istextorbinary": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.5.1.tgz", - "integrity": "sha512-pv/JNPWnfpwGjPx7JrtWTwsWsxkrK3fNzcEVnt92YKEIErps4Fsk49+qzCe9iQF2hjqK8Naqf8P9kzoeCuQI1g==", - "requires": { - "binaryextensions": "^2.1.2", - "editions": "^2.1.3", - "textextensions": "^2.4.0" - } - }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonschema": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", - "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==" - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - }, - "kuler": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", - "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", - "dev": true, - "requires": { - "colornames": "^1.1.1" - } - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, - "launchpad": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/launchpad/-/launchpad-0.7.2.tgz", - "integrity": "sha512-nmfx8QWqCvAmukHPticyO3+frnUKp0tDKXz3Au570XHBE0G1lNyN+KSODHgTqIIEuCIAlAgpbWjI36Vjk1mDKA==", - "dev": true, - "optional": true, - "requires": { - "async": "^2.0.1", - "browserstack": "^1.2.0", - "debug": "^2.2.0", - "plist": "^2.0.1", - "q": "^1.4.1", - "underscore": "^1.8.3" - }, - "dependencies": { - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", - "dev": true, - "optional": true - } - } - }, - "lazy-req": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/lazy-req/-/lazy-req-1.1.0.tgz", - "integrity": "sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=", - "dev": true - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "^1.0.0" - } - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, - "lodash": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.12.tgz", - "integrity": "sha512-+CiwtLnsJhX03p20mwXuvhoebatoh5B3tt+VvYlrPgZC1g36y+RRbkufX95Xa+X4I59aWEacDFYwnJZiyBh9gA==" - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=" - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=" - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=" - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" - }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "requires": { - "lodash._root": "^3.0.0" - } - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=", - "dev": true - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", - "dev": true, - "requires": { - "chalk": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "logform": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", - "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", - "dev": true, - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "macos-release": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", - "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==", - "dev": true - }, - "magic-string": { - "version": "0.22.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", - "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", - "dev": true, - "requires": { - "vlq": "^0.2.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", - "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "matcher": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz", - "integrity": "sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.4" - } - }, - "math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" - }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", - "dev": true, - "requires": { - "charenc": "~0.0.1", - "crypt": "~0.0.1", - "is-buffer": "~1.1.1" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "mem-fs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-1.1.3.tgz", - "integrity": "sha1-uK6NLj/Lb10/kWXBLUVRoGXZicw=", - "requires": { - "through2": "^2.0.0", - "vinyl": "^1.1.0", - "vinyl-file": "^2.0.0" - } - }, - "mem-fs-editor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-5.1.0.tgz", - "integrity": "sha512-2Yt2GCYEbcotYbIJagmow4gEtHDqzpq5XN94+yAx/NT5+bGqIjkXnm3KCUQfE6kRfScGp9IZknScoGRKu8L78w==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "deep-extend": "^0.6.0", - "ejs": "^2.5.9", - "glob": "^7.0.3", - "globby": "^8.0.1", - "isbinaryfile": "^3.0.2", - "mkdirp": "^0.5.0", - "multimatch": "^2.0.0", - "rimraf": "^2.2.8", - "through2": "^2.0.0", - "vinyl": "^2.0.1" - }, - "dependencies": { - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "memory-streams": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/memory-streams/-/memory-streams-0.1.3.tgz", - "integrity": "sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==", - "requires": { - "readable-stream": "~1.0.2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "requires": { - "readable-stream": "^2.0.1" - } - }, - "merge2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.3.tgz", - "integrity": "sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimatch-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minimatch-all/-/minimatch-all-1.1.0.tgz", - "integrity": "sha1-QMSWonouEo0Zv3WOdrsBoMcUV4c=", - "dev": true, - "requires": { - "minimatch": "^3.0.2" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "mout": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz", - "integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.1.tgz", - "integrity": "sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw==", - "dev": true, - "requires": { - "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.1", - "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - } - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "multipipe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-1.0.2.tgz", - "integrity": "sha1-zBPv2DPJzamfIk+GhGG44aP9k50=", - "dev": true, - "requires": { - "duplexer2": "^0.1.2", - "object-assign": "^4.1.0" - } - }, - "mute-stream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", - "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=", - "dev": true - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "nise": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", - "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", - "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==" - } - } - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true - }, - "node-status-codes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", - "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=", - "dev": true - }, - "nomnom": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", - "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", - "dev": true, - "requires": { - "chalk": "~0.4.0", - "underscore": "~1.6.0" - }, - "dependencies": { - "ansi-styles": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", - "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", - "dev": true - }, - "chalk": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", - "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", - "dev": true, - "requires": { - "ansi-styles": "~1.0.0", - "has-color": "~0.1.0", - "strip-ansi": "~0.1.0" - } - }, - "strip-ansi": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", - "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", - "dev": true - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "octokit-pagination-methods": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz", - "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "one-time": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", - "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=", - "dev": true - }, - "onetime": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true - }, - "opn": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz", - "integrity": "sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=", - "dev": true, - "requires": { - "object-assign": "^4.0.1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - } - }, - "ordered-read-streams": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", - "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", - "requires": { - "is-stream": "^1.0.1", - "readable-stream": "^2.0.1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.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" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - } - } - }, - "os-name": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", - "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", - "dev": true, - "requires": { - "macos-release": "^2.2.0", - "windows-release": "^3.1.0" - } - }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-cancelable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", - "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-timeout": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", - "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", - "dev": true - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==" - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "peek-stream": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", - "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "duplexify": "^3.5.0", - "through2": "^2.0.3" - } - }, - "pem": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.2.tgz", - "integrity": "sha512-TOnPtq3ZFnCniOZ+rka4pk8UIze9xG1qI+wNE7EmkiR/cg+53uVvk5QbkWZ7M6RsuOxzz62FW1hlAobJr/lTOA==", - "dev": true, - "requires": { - "es6-promisify": "^6.0.0", - "md5": "^2.2.1", - "os-tmpdir": "^1.0.1", - "which": "^1.3.1" - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "plist": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz", - "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=", - "dev": true, - "optional": true, - "requires": { - "base64-js": "1.2.0", - "xmlbuilder": "8.2.2", - "xmldom": "0.1.x" - } - }, - "plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "requires": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - } - } - }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==" - }, - "plylog": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/plylog/-/plylog-1.1.0.tgz", - "integrity": "sha512-/QnY5aSVaP54va6hruzNtAj02HpsLlAt7V5EndMrtq6ZUTZJKUja43rgiUtGXqm95yrSJjbZoPW0yQQQwLpoJA==", - "dev": true, - "requires": { - "logform": "^1.9.1", - "winston": "^3.0.0", - "winston-transport": "^4.2.0" - } - }, - "polymer-analyzer": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/polymer-analyzer/-/polymer-analyzer-3.2.4.tgz", - "integrity": "sha512-JmxUhMajTuC18tLXbTtu2+aN2x9bTX+4MvCD4IZKJ0rtAL8jWi1iRLfogpHJB4Ig9Dc8EEEuEYipLuzPFl3vqA==", - "dev": true, - "requires": { - "@babel/generator": "^7.0.0-beta.42", - "@babel/traverse": "^7.0.0-beta.42", - "@babel/types": "^7.0.0-beta.42", - "@types/babel-generator": "^6.25.1", - "@types/babel-traverse": "^6.25.2", - "@types/babel-types": "^6.25.1", - "@types/babylon": "^6.16.2", - "@types/chai-subset": "^1.3.0", - "@types/chalk": "^0.4.30", - "@types/clone": "^0.1.30", - "@types/cssbeautify": "^0.3.1", - "@types/doctrine": "^0.0.1", - "@types/is-windows": "^0.2.0", - "@types/minimatch": "^3.0.1", - "@types/parse5": "^2.2.34", - "@types/path-is-inside": "^1.0.0", - "@types/resolve": "0.0.6", - "@types/whatwg-url": "^6.4.0", - "babylon": "^7.0.0-beta.42", - "cancel-token": "^0.1.1", - "chalk": "^1.1.3", - "clone": "^2.0.0", - "cssbeautify": "^0.3.1", - "doctrine": "^2.0.2", - "dom5": "^3.0.0", - "indent": "0.0.2", - "is-windows": "^1.0.2", - "jsonschema": "^1.1.0", - "minimatch": "^3.0.4", - "parse5": "^4.0.0", - "path-is-inside": "^1.0.2", - "resolve": "^1.5.0", - "shady-css-parser": "^0.1.0", - "stable": "^0.1.6", - "strip-indent": "^2.0.0", - "vscode-uri": "=1.0.6", - "whatwg-url": "^6.4.0" - }, - "dependencies": { - "@types/chalk": { - "version": "0.4.31", - "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz", - "integrity": "sha1-ox10JBprHtu5c8822XooloNKUfk=", - "dev": true - }, - "@types/resolve": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.6.tgz", - "integrity": "sha512-g+Rg8uMWY76oYTyaL+m7ZcblqF/oj7pE6uEUyACluJx4zcop1Lk14qQiocdEkEVMDFm6DmKpxJhsER+ZuTwG3g==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "polymer-build": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/polymer-build/-/polymer-build-3.1.4.tgz", - "integrity": "sha512-OhTOPG5Y/tK2HqGZ5XA/CVDh+TuOaDv7wTZWXDCg6hxeMgNKuljDMn2coyGU5NLM0pLbS+gwFAc2ZJ5cWHCHNg==", - "dev": true, - "requires": { - "@babel/core": "^7.0.0", - "@babel/plugin-external-helpers": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-async-generators": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-import-meta": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-async-to-generator": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-duplicate-keys": "^7.0.0", - "@babel/plugin-transform-exponentiation-operator": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-instanceof": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-modules-amd": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-regenerator": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-sticky-regex": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "@babel/plugin-transform-typeof-symbol": "^7.0.0", - "@babel/plugin-transform-unicode-regex": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@polymer/esm-amd-loader": "^1.0.0", - "@types/babel-types": "^6.25.1", - "@types/babylon": "^6.16.2", - "@types/gulp-if": "0.0.33", - "@types/html-minifier": "^3.5.1", - "@types/is-windows": "^0.2.0", - "@types/mz": "0.0.31", - "@types/parse5": "^2.2.34", - "@types/resolve": "0.0.7", - "@types/uuid": "^3.4.3", - "@types/vinyl": "^2.0.0", - "@types/vinyl-fs": "^2.4.8", - "babel-plugin-minify-guarded-expressions": "^0.4.3", - "babel-preset-minify": "^0.5.0", - "babylon": "^7.0.0-beta.42", - "css-slam": "^2.1.2", - "dom5": "^3.0.0", - "gulp-if": "^2.0.2", - "html-minifier": "^3.5.10", - "matcher": "^1.1.0", - "multipipe": "^1.0.2", - "mz": "^2.6.0", - "parse5": "^4.0.0", - "plylog": "^1.0.0", - "polymer-analyzer": "^3.1.3", - "polymer-bundler": "^4.0.9", - "polymer-project-config": "^4.0.3", - "regenerator-runtime": "^0.11.1", - "stream": "0.0.2", - "sw-precache": "^5.1.1", - "uuid": "^3.2.1", - "vinyl": "^1.2.0", - "vinyl-fs": "^2.4.4" - }, - "dependencies": { - "@types/resolve": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.7.tgz", - "integrity": "sha512-GPewdjkb0Q76o459qgp6pBLzJj/bD3oveS2kfLhIkZ9U3t3AFKtl5DlFB6lGTw0iZmcmxoGC8lpLW3NNJKrN9A==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/vinyl-fs": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz", - "integrity": "sha512-2OzQSfIr9CqqWMGqmcERE6Hnd2KY3eBVtFaulVo3sJghplUcaeMdL9ZjEiljcQQeHjheWY9RlNmumjIAvsBNaA==", - "dev": true, - "requires": { - "@types/glob-stream": "*", - "@types/node": "*", - "@types/vinyl": "*" - } - } - } - }, - "polymer-bundler": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/polymer-bundler/-/polymer-bundler-4.0.10.tgz", - "integrity": "sha512-nwlN3LQlQDqbZ2sUH3394C/dHZUDHq8tpdS5HARvPDb0Q9IXWD+znOR1cr7wSjF0EZN4LiUH5hWyUoV4QSjhpQ==", - "dev": true, - "requires": { - "@types/babel-generator": "^6.25.1", - "@types/babel-traverse": "^6.25.3", - "babel-generator": "^6.26.1", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "clone": "^2.1.0", - "command-line-args": "^5.0.2", - "command-line-usage": "^5.0.5", - "dom5": "^3.0.0", - "espree": "^3.5.2", - "magic-string": "^0.22.4", - "mkdirp": "^0.5.1", - "parse5": "^4.0.0", - "polymer-analyzer": "^3.2.2", - "rollup": "^1.3.0", - "source-map": "^0.5.6", - "vscode-uri": "=1.0.6" - } - }, - "polymer-linter": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/polymer-linter/-/polymer-linter-3.0.1.tgz", - "integrity": "sha512-eDh2CeswZz4Rwf8gfYXpMN66pieq4qJvP9bH3m39LLGm81hRePo4N5OHoQzR5unen1PUdmtjDv0Iicz3dTYEZQ==", - "dev": true, - "requires": { - "@types/fast-levenshtein": "0.0.1", - "@types/parse5": "^2.2.34", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "cancel-token": "^0.1.1", - "css-what": "^2.1.0", - "dom5": "^3.0.0", - "fast-levenshtein": "^2.0.6", - "parse5": "^4.0.0", - "polymer-analyzer": "^3.0.0", - "shady-css-parser": "^0.1.0", - "stable": "^0.1.6", - "strip-indent": "^2.0.0", - "validate-element-name": "^2.1.1" - }, - "dependencies": { - "strip-indent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", - "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", - "dev": true - } - } - }, - "polymer-project-config": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/polymer-project-config/-/polymer-project-config-4.0.3.tgz", - "integrity": "sha512-Drr+Imq+znhBC8XSt9pMlmPixoGnIOmleV5SD6mto1zOGC5oCDbSNsQL2v89DWOk+9aSUO79vnWwOmEPDSvYfw==", - "dev": true, - "requires": { - "@types/parse5": "^2.2.34", - "browser-capabilities": "^1.0.0", - "jsonschema": "^1.1.1", - "minimatch-all": "^1.1.0", - "plylog": "^1.0.0", - "winston": "^3.0.0" - } - }, - "polyserve": { - "version": "0.27.15", - "resolved": "https://registry.npmjs.org/polyserve/-/polyserve-0.27.15.tgz", - "integrity": "sha512-AaFgANt+tUUVgHLw+BnaVYcn649JiwL1ru0TOZUKj1gGGn/Bq2S16gxql+1muGpRaAsgFu13Zu7k5XkwatwwSg==", - "dev": true, - "requires": { - "@types/compression": "^0.0.33", - "@types/content-type": "^1.1.0", - "@types/escape-html": "0.0.20", - "@types/express": "^4.0.36", - "@types/mime": "^2.0.0", - "@types/mz": "0.0.29", - "@types/opn": "^3.0.28", - "@types/parse5": "^2.2.34", - "@types/pem": "^1.8.1", - "@types/resolve": "0.0.6", - "@types/serve-static": "^1.7.31", - "@types/spdy": "^3.4.1", - "bower-config": "^1.4.1", - "browser-capabilities": "^1.0.0", - "command-line-args": "^5.0.2", - "command-line-usage": "^5.0.5", - "compression": "^1.6.2", - "content-type": "^1.0.2", - "cors": "^2.8.4", - "escape-html": "^1.0.3", - "express": "^4.8.5", - "find-port": "^1.0.1", - "http-proxy-middleware": "^0.17.2", - "lru-cache": "^4.0.2", - "mime": "^2.3.1", - "mz": "^2.4.0", - "opn": "^3.0.2", - "pem": "^1.8.3", - "polymer-build": "^3.1.0", - "polymer-project-config": "^4.0.0", - "requirejs": "^2.3.4", - "resolve": "^1.5.0", - "send": "^0.16.2", - "spdy": "^3.3.3" - }, - "dependencies": { - "@types/mz": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/mz/-/mz-0.0.29.tgz", - "integrity": "sha1-vCRyjGSZc/HHhR6QM/nOUlZowns=", - "dev": true, - "requires": { - "@types/bluebird": "*", - "@types/node": "*" - } - }, - "@types/resolve": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.6.tgz", - "integrity": "sha512-g+Rg8uMWY76oYTyaL+m7ZcblqF/oj7pE6uEUyACluJx4zcop1Lk14qQiocdEkEVMDFm6DmKpxJhsER+ZuTwG3g==", - "dev": true, - "requires": { - "@types/node": "*" - } - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" - }, - "pretty-bytes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", - "integrity": "sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=" - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "puppeteer": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.18.1.tgz", - "integrity": "sha512-luUy0HPSuWPsPZ1wAp6NinE0zgetWtudf5zwZ6dHjMWfYpTQcmKveFRox7VBNhQ98OjNA9PQ9PzQyX8k/KrxTg==", - "requires": { - "debug": "^4.1.0", - "extract-zip": "^1.6.6", - "https-proxy-agent": "^2.2.1", - "mime": "^2.0.3", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^2.6.1", - "ws": "^6.1.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true, - "optional": true - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "requires": { - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - } - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - } - } - }, - "read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "read-chunk": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz", - "integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "with-open-file": "^0.1.6" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "requires": { - "resolve": "^1.1.6" - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "reduce-flatten": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=", - "dev": true - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", - "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "regenerator-transform": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", - "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==", - "dev": true, - "requires": { - "private": "^0.1.6" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "requires": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==" - }, - "regexpu-core": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", - "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.0.2", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", - "dev": true - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "require-package-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", - "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=" - }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, - "requirejs": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", - "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", - "dev": true, - "requires": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" - } - }, - "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "restore-cursor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", - "dev": true, - "requires": { - "exit-hook": "^1.0.0", - "onetime": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - }, - "rollup": { - "version": "1.16.7", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.16.7.tgz", - "integrity": "sha512-P3GVcbVSLLjHWFLKGerYRe3Q/yggRXmTZFx/4WZf4wzGwO6hAg5jyMAFMQKc0dts8rFID4BQngfoz6yQbI7iMQ==", - "dev": true, - "requires": { - "@types/estree": "0.0.39", - "@types/node": "^12.0.10", - "acorn": "^6.1.1" - }, - "dependencies": { - "acorn": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz", - "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw==", - "dev": true - } - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-sequence": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-1.2.2.tgz", - "integrity": "sha1-UJWgvr6YczsBQL0I3YDsAw3azes=", - "requires": { - "chalk": "*", - "gulp-util": "*" - } - }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", - "dev": true - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=" - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "requires": { - "rx-lite": "*" - } - }, - "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==" - }, - "sauce-connect-launcher": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/sauce-connect-launcher/-/sauce-connect-launcher-1.2.7.tgz", - "integrity": "sha512-v07+QhFrxgz3seMFuRSonu3gW1s6DbcLQlFhjsRrmKUauzPbbudHdnn91WYgEwhoZVdPNzeZpAEJwcQyd9xnTA==", - "dev": true, - "optional": true, - "requires": { - "adm-zip": "~0.4.3", - "async": "^2.1.2", - "https-proxy-agent": "^2.2.1", - "lodash": "^4.16.6", - "rimraf": "^2.5.4" - } - }, - "scoped-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-1.0.0.tgz", - "integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=" - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selenium-standalone": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/selenium-standalone/-/selenium-standalone-6.16.0.tgz", - "integrity": "sha512-tl7HFH2FOxJD1is7Pzzsl0pY4vuePSdSWiJdPn+6ETBkpeJDiuzou8hBjvWYWpD+eIVcOrmy3L0R3GzkdHLzDw==", - "dev": true, - "optional": true, - "requires": { - "async": "^2.6.2", - "commander": "^2.19.0", - "cross-spawn": "^6.0.5", - "debug": "^4.1.1", - "lodash": "^4.17.11", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "progress": "2.0.3", - "request": "2.88.0", - "tar-stream": "2.0.0", - "urijs": "^1.19.1", - "which": "^1.3.1", - "yauzl": "^2.10.0" - }, - "dependencies": { - "bl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", - "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", - "dev": true, - "optional": true, - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "optional": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "tar-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.0.0.tgz", - "integrity": "sha512-n2vtsWshZOVr/SY4KtslPoUlyNh06I2SGgAOCZmquCEjlbV/LjY2CY80rDtdQRHFOYXNlgBDo6Fr3ww2CWPOtA==", - "dev": true, - "optional": true, - "requires": { - "bl": "^2.2.0", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - } - } - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "dependencies": { - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - } - } - } - }, - "server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=", - "dev": true - }, - "serviceworker-cache-polyfill": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serviceworker-cache-polyfill/-/serviceworker-cache-polyfill-4.0.0.tgz", - "integrity": "sha1-3hnuc77yGrPAdAo3sz22JGS6ves=", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, - "shady-css-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/shady-css-parser/-/shady-css-parser-0.1.0.tgz", - "integrity": "sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } - } - }, - "sinon": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-5.1.1.tgz", - "integrity": "sha512-h/3uHscbt5pQNxkf7Y/Lb9/OM44YNCicHakcq73ncbrIS8lXg+ZGOZbtuU+/km4YnyiCYfQQEwANaReJz7KDfw==", - "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.5.0", - "lodash.get": "^4.4.2", - "lolex": "^2.4.2", - "nise": "^1.3.3", - "supports-color": "^5.4.0", - "type-detect": "^4.0.8" - }, - "dependencies": { - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==" - } - } - }, - "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "requires": { - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - } - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - } - }, - "socket.io": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz", - "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==", - "dev": true, - "requires": { - "debug": "~4.1.0", - "engine.io": "~3.3.1", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.2.0", - "socket.io-parser": "~3.3.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "socket.io-adapter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", - "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=", - "dev": true - }, - "socket.io-client": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz", - "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==", - "dev": true, - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.3.1", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "socket.io-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", - "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", - "dev": true, - "requires": { - "sort-keys": "^1.0.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==" - }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", - "dev": true, - "requires": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", - "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" - }, - "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", - "dev": true, - "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", - "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", - "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" - } - }, - "spdy-transport": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.1.tgz", - "integrity": "sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==", - "dev": true, - "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", - "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true - }, - "stacky": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/stacky/-/stacky-1.3.1.tgz", - "integrity": "sha1-PxF+UYe5pz0j+HbWnwXIWxGAShI=", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "lodash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, - "stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", - "integrity": "sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=", - "dev": true, - "requires": { - "emitter-component": "^1.1.1" - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", - "dev": true - }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-bom-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", - "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", - "requires": { - "first-chunk-stream": "^1.0.0", - "strip-bom": "^2.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "sw-precache": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/sw-precache/-/sw-precache-5.2.1.tgz", - "integrity": "sha512-8FAy+BP/FXE+ILfiVTt+GQJ6UEf4CVHD9OfhzH0JX+3zoy2uFk7Vn9EfXASOtVmmIVbL3jE/W8Z66VgPSZcMhw==", - "dev": true, - "requires": { - "dom-urls": "^1.1.0", - "es6-promise": "^4.0.5", - "glob": "^7.1.1", - "lodash.defaults": "^4.2.0", - "lodash.template": "^4.4.0", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "pretty-bytes": "^4.0.2", - "sw-toolbox": "^3.4.0", - "update-notifier": "^2.3.0" - }, - "dependencies": { - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - } - } - }, - "sw-toolbox": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/sw-toolbox/-/sw-toolbox-3.6.0.tgz", - "integrity": "sha1-Jt8dHHA0hljk3qKIQxkUm3sxg7U=", - "dev": true, - "requires": { - "path-to-regexp": "^1.0.1", - "serviceworker-cache-polyfill": "^4.0.0" - } - }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" - }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "requires": { - "ajv": "^5.2.3", - "ajv-keywords": "^2.1.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", - "slice-ansi": "1.0.0", - "string-width": "^2.1.1" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "table-layout": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.5.tgz", - "integrity": "sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==", - "dev": true, - "requires": { - "array-back": "^2.0.0", - "deep-extend": "~0.6.0", - "lodash.padend": "^4.6.1", - "typical": "^2.6.1", - "wordwrapjs": "^3.0.0" - }, - "dependencies": { - "array-back": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", - "integrity": "sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==", - "dev": true, - "requires": { - "typical": "^2.6.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", - "dev": true - } - } - }, - "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", - "dev": true, - "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" - }, - "dependencies": { - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "dev": true, - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - } - }, - "temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", - "dev": true, - "requires": { - "os-tmpdir": "^1.0.0", - "rimraf": "~2.2.6" - }, - "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true - } - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.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" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - } - } - }, - "ternary-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-2.0.1.tgz", - "integrity": "sha1-Bk5Im0tb9gumpre8fy9cJ07Pgmk=", - "dev": true, - "requires": { - "duplexify": "^3.5.0", - "fork-stream": "^0.0.4", - "merge-stream": "^1.0.0", - "through2": "^2.0.1" - } - }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "textextensions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.4.0.tgz", - "integrity": "sha512-qftQXnX1DzpSV8EddtHIT0eDDEiBF8ywhFYR2lI9xrGtxqKN+CvLXhACeCIGbCpQfxxERbrkZEFb8cZcDKbVZA==" - }, - "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", - "dev": true, - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "dev": true, - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" - }, - "tmp": { - "version": "0.0.31", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", - "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", - "requires": { - "os-tmpdir": "~1.0.1" - } - }, - "to-absolute-glob": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", - "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", - "requires": { - "extend-shallow": "^2.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - } - } - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==", - "dev": true - }, - "tsc-then": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tsc-then/-/tsc-then-1.1.0.tgz", - "integrity": "sha512-830G8SK8tewOxfKVBC5YWqZ2C7wS1D4dh9267ebLxR7Gvh3UuI6aKU5Hxo+L3Kkcy6CuxZp4PMGl066DZ3Hlmw==", - "requires": { - "@types/node": "^8.0.53", - "semver": "^5.4.1" - }, - "dependencies": { - "@types/node": { - "version": "8.10.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.50.tgz", - "integrity": "sha512-+ZbcUwJdaBgOZpwXeT0v+gHC/jQbEfzoc9s4d0rN0JIKeQbuTrT+A2n1aQY6LpZjrLXJT7avVUqiCecCJeeZxA==" - } - } - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", - "dev": true - }, - "ua-parser-js": { - "version": "0.7.20", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", - "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==", - "dev": true - }, - "uglify-js": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", - "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", - "requires": { - "commander": "~2.19.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", - "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", - "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", - "requires": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" - }, - "dependencies": { - "through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - } - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "universal-user-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.1.0.tgz", - "integrity": "sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q==", - "dev": true, - "requires": { - "os-name": "^3.0.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "untildify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-2.1.0.tgz", - "integrity": "sha1-F+soB5h/dpUunASF/DEdBqgmouA=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "update-notifier": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-1.0.3.tgz", - "integrity": "sha1-j5LFFUgr1oMbfJMBPnD4dVLHz1o=", - "dev": true, - "requires": { - "boxen": "^0.6.0", - "chalk": "^1.0.0", - "configstore": "^2.0.0", - "is-npm": "^1.0.0", - "latest-version": "^2.0.0", - "lazy-req": "^1.1.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^2.0.0" - }, - "dependencies": { - "ansi-align": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-1.1.0.tgz", - "integrity": "sha1-LwwWWIKXOa3V67FeawxuNCPwFro=", - "dev": true, - "requires": { - "string-width": "^1.0.1" - } - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "boxen": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-0.6.0.tgz", - "integrity": "sha1-g2TUJIrDT/DvGy8r9JpsYM4NgbY=", - "dev": true, - "requires": { - "ansi-align": "^1.1.0", - "camelcase": "^2.1.0", - "chalk": "^1.1.1", - "cli-boxes": "^1.0.0", - "filled-array": "^1.0.0", - "object-assign": "^4.0.1", - "repeating": "^2.0.0", - "string-width": "^1.0.1", - "widest-line": "^1.0.0" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "configstore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", - "integrity": "sha1-c3o6cDbpiGECqmCZ5HuzOrGroaE=", - "dev": true, - "requires": { - "dot-prop": "^3.0.0", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.1", - "os-tmpdir": "^1.0.0", - "osenv": "^0.1.0", - "uuid": "^2.0.1", - "write-file-atomic": "^1.1.2", - "xdg-basedir": "^2.0.0" - } - }, - "dot-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", - "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "got": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", - "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", - "dev": true, - "requires": { - "create-error-class": "^3.0.1", - "duplexer2": "^0.1.4", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "node-status-codes": "^1.0.0", - "object-assign": "^4.0.1", - "parse-json": "^2.1.0", - "pinkie-promise": "^2.0.0", - "read-all-stream": "^3.0.0", - "readable-stream": "^2.0.5", - "timed-out": "^3.0.0", - "unzip-response": "^1.0.2", - "url-parse-lax": "^1.0.0" - } - }, - "latest-version": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-2.0.0.tgz", - "integrity": "sha1-VvjWE5YghHuAF/jx9NeOIRMkFos=", - "dev": true, - "requires": { - "package-json": "^2.0.0" - } - }, - "package-json": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-2.4.0.tgz", - "integrity": "sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=", - "dev": true, - "requires": { - "got": "^5.0.0", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "timed-out": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz", - "integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=", - "dev": true - }, - "unzip-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", - "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=", - "dev": true - }, - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", - "dev": true - }, - "widest-line": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", - "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", - "dev": true, - "requires": { - "string-width": "^1.0.1" - } - }, - "write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - } - }, - "xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0" - } - } - } - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urijs": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", - "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==", - "dev": true - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "requires": { - "prepend-http": "^1.0.1" - } - }, - "url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=", - "dev": true - }, - "url-to-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=" - }, - "validate-element-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/validate-element-name/-/validate-element-name-2.1.1.tgz", - "integrity": "sha1-j/dffaafc+fFEFiDYhMFCLesZE4=", - "dev": true, - "requires": { - "is-potential-custom-element-name": "^1.0.0", - "log-symbols": "^1.0.0", - "meow": "^3.7.0" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vargs": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz", - "integrity": "sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - } - } - }, - "vinyl-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", - "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.3.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0", - "strip-bom-stream": "^2.0.0", - "vinyl": "^1.1.0" - }, - "dependencies": { - "first-chunk-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", - "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "strip-bom-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", - "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", - "requires": { - "first-chunk-stream": "^2.0.0", - "strip-bom": "^2.0.0" - } - } - } - }, - "vinyl-fs": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", - "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", - "requires": { - "duplexify": "^3.2.0", - "glob-stream": "^5.3.2", - "graceful-fs": "^4.0.0", - "gulp-sourcemaps": "1.6.0", - "is-valid-glob": "^0.3.0", - "lazystream": "^1.0.0", - "lodash.isequal": "^4.0.0", - "merge-stream": "^1.0.0", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.0", - "readable-stream": "^2.0.4", - "strip-bom": "^2.0.0", - "strip-bom-stream": "^1.0.0", - "through2": "^2.0.0", - "through2-filter": "^2.0.0", - "vali-date": "^1.0.0", - "vinyl": "^1.0.0" - } - }, - "vinyl-fs-fake": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vinyl-fs-fake/-/vinyl-fs-fake-1.1.1.tgz", - "integrity": "sha512-FY1KYQwp4yRt2695GqppK9viJTysuH6n6ns3t94l3wYFJlyf7YHtqCdNZGrMrpC1yLZZlueTioiOsL/6lFHykw==", - "requires": { - "through2": "^0.6.3", - "vinyl": "^0.4.6", - "vinyl-fs": "^2.4.4" - }, - "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "requires": { - "clone": "^0.2.0", - "clone-stats": "^0.0.1" - } - } - } - }, - "vlq": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", - "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==", - "dev": true - }, - "vscode-uri": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.6.tgz", - "integrity": "sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==", - "dev": true - }, - "walkdir": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", - "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=" - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "wct-local": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/wct-local/-/wct-local-2.1.5.tgz", - "integrity": "sha512-eqoZhjGy4Xq2tY0uB46Grkw/ztq+/rC0ImbYKl62unFHXtOgal+kkvnxR3SLRFNM8ty9+ItgycPeH0IpTqVL+w==", - "dev": true, - "optional": true, - "requires": { - "@types/express": "^4.0.30", - "@types/freeport": "^1.0.19", - "@types/launchpad": "^0.6.0", - "@types/which": "^1.3.1", - "chalk": "^2.3.0", - "cleankill": "^2.0.0", - "freeport": "^1.0.4", - "launchpad": "^0.7.0", - "selenium-standalone": "^6.7.0", - "which": "^1.0.8" - } - }, - "wct-sauce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wct-sauce/-/wct-sauce-2.1.0.tgz", - "integrity": "sha512-c3R4PJcbpS7Gxv2vZ4HDAqpXV6cT9peslAWMU7hHH9PMhKDPbn8RNa6E4DVL0tOmZznB+3cRmtZ6+vJ/aDwu1A==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^2.4.1", - "cleankill": "^2.0.0", - "lodash": "^4.17.10", - "request": "^2.85.0", - "sauce-connect-launcher": "^1.0.0", - "temp": "^0.8.1", - "uuid": "^3.2.1" - } - }, - "wd": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/wd/-/wd-1.11.2.tgz", - "integrity": "sha512-zXJY9ARjQQYN2LatLTRcW39EYzIVqKNhGpp4XWJmRgHBioG4FoenIOsoVbaO8lnFGgv31V99kAy5hB4eWGIwzA==", - "dev": true, - "requires": { - "archiver": "2.1.1", - "async": "2.0.1", - "lodash": "4.17.11", - "mkdirp": "^0.5.1", - "q": "1.4.1", - "request": "2.88.0", - "vargs": "0.1.0" - }, - "dependencies": { - "async": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz", - "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", - "dev": true, - "requires": { - "lodash": "^4.8.0" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "q": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", - "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", - "dev": true - } - } - }, - "web-component-tester": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/web-component-tester/-/web-component-tester-6.9.2.tgz", - "integrity": "sha512-s2kB/+IE8XWcnxY1fqSpqTiiHEGHWgUWariAbiRlxmAvWSuvaCVNALHYebsZrLCNCLHKcJR8/sGv/bw0MWMvjw==", - "dev": true, - "requires": { - "@polymer/sinonjs": "^1.14.1", - "@polymer/test-fixture": "^0.0.3", - "@webcomponents/webcomponentsjs": "^1.0.7", - "accessibility-developer-tools": "^2.12.0", - "async": "^2.4.1", - "body-parser": "^1.17.2", - "bower-config": "^1.4.0", - "chalk": "^1.1.3", - "cleankill": "^2.0.0", - "express": "^4.15.3", - "findup-sync": "^2.0.0", - "glob": "^7.1.2", - "lodash": "^3.10.1", - "multer": "^1.3.0", - "nomnom": "^1.8.1", - "polyserve": "^0.27.13", - "resolve": "^1.5.0", - "semver": "^5.3.0", - "send": "^0.16.1", - "server-destroy": "^1.0.1", - "sinon": "^2.3.5", - "sinon-chai": "^2.10.0", - "socket.io": "^2.0.3", - "stacky": "^1.3.1", - "update-notifier": "^2.2.0", - "wct-local": "^2.1.1", - "wct-sauce": "^2.0.2", - "wd": "^1.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "sinon": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", - "integrity": "sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==", - "dev": true, - "requires": { - "diff": "^3.1.0", - "formatio": "1.2.0", - "lolex": "^1.6.0", - "native-promise-only": "^0.8.1", - "path-to-regexp": "^1.7.0", - "samsam": "^1.1.3", - "text-encoding": "0.6.4", - "type-detect": "^4.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "optional": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - } - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "dev": true, - "requires": { - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "windows-release": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz", - "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==", - "dev": true, - "requires": { - "execa": "^1.0.0" - } - }, - "winston": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", - "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", - "dev": true, - "requires": { - "async": "^2.6.1", - "diagnostics": "^1.1.1", - "is-stream": "^1.1.0", - "logform": "^2.1.1", - "one-time": "0.0.4", - "readable-stream": "^3.1.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.3.0" - }, - "dependencies": { - "logform": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", - "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", - "dev": true, - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "winston-transport": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", - "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", - "dev": true, - "requires": { - "readable-stream": "^2.3.6", - "triple-beam": "^1.2.0" - } - }, - "with-open-file": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.6.tgz", - "integrity": "sha512-SQS05JekbtwQSgCYlBsZn/+m2gpn4zWsqpCYIrCHva0+ojXcnmUEPsBN6Ipoz3vmY/81k5PvYEWSxER2g4BTqA==", - "dev": true, - "requires": { - "p-finally": "^1.0.0", - "p-try": "^2.1.0", - "pify": "^4.0.1" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "wordwrapjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz", - "integrity": "sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==", - "dev": true, - "requires": { - "reduce-flatten": "^1.0.1", - "typical": "^2.6.1" - }, - "dependencies": { - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=", - "dev": true - } - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", - "dev": true, - "optional": true - }, - "xmldom": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", - "dev": true, - "optional": true - }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", - "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", - "requires": { - "camelcase": "^4.1.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "read-pkg-up": "^2.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", - "yargs-parser": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "requires": { - "pify": "^2.0.0" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - } - } - }, - "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", - "requires": { - "camelcase": "^4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - } - } - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", - "dev": true, - "optional": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", - "dev": true - }, - "yeoman-assert": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/yeoman-assert/-/yeoman-assert-2.2.3.tgz", - "integrity": "sha1-pWgqg2MsUKwO6EFzpaEP1vMgZHQ=" - }, - "yeoman-environment": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-1.6.6.tgz", - "integrity": "sha1-zYX6Z9FWBg5EDXgH1+988NLR1nE=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "debug": "^2.0.0", - "diff": "^2.1.2", - "escape-string-regexp": "^1.0.2", - "globby": "^4.0.0", - "grouped-queue": "^0.3.0", - "inquirer": "^1.0.2", - "lodash": "^4.11.1", - "log-symbols": "^1.0.1", - "mem-fs": "^1.1.0", - "text-table": "^0.2.0", - "untildify": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "diff": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", - "integrity": "sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=", - "dev": true - }, - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globby": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz", - "integrity": "sha1-CA9UVJ7BuCpsYOYx/ILhIR2+lfg=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^6.0.1", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "yeoman-generator": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-3.2.0.tgz", - "integrity": "sha512-iR/qb2je3GdXtSfxgvOXxUW0Cp8+C6LaZaNlK2BAICzFNzwHtM10t/QBwz5Ea9nk6xVDQNj4Q889TjCXGuIv8w==", - "dev": true, - "requires": { - "async": "^2.6.0", - "chalk": "^2.3.0", - "cli-table": "^0.3.1", - "cross-spawn": "^6.0.5", - "dargs": "^6.0.0", - "dateformat": "^3.0.3", - "debug": "^4.1.0", - "detect-conflict": "^1.0.0", - "error": "^7.0.2", - "find-up": "^3.0.0", - "github-username": "^4.0.0", - "istextorbinary": "^2.2.1", - "lodash": "^4.17.10", - "make-dir": "^1.1.0", - "mem-fs-editor": "^5.0.0", - "minimist": "^1.2.0", - "pretty-bytes": "^5.1.0", - "read-chunk": "^3.0.0", - "read-pkg-up": "^4.0.0", - "rimraf": "^2.6.2", - "run-async": "^2.0.0", - "shelljs": "^0.8.0", - "text-table": "^0.2.0", - "through2": "^3.0.0", - "yeoman-environment": "^2.0.5" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "inquirer": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz", - "integrity": "sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.11", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pretty-bytes": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.2.0.tgz", - "integrity": "sha512-ujANBhiUsl9AhREUDUEY1GPOharMGm8x8juS7qOHybcLi7XsKfrYQ88hSly1l2i0klXHTDYrlL8ihMCG55Dc3w==", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "dev": true, - "requires": { - "readable-stream": "2 || 3" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "untildify": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", - "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==", - "dev": true - }, - "yeoman-environment": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-2.4.0.tgz", - "integrity": "sha512-SsvoL0RNAFIX69eFxkUhwKUN2hG1UwUjxrcP+T2ytwdhqC/kHdnFOH2SXdtSN1Ju4aO4xuimmzfRoheYY88RuA==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "debug": "^3.1.0", - "diff": "^3.5.0", - "escape-string-regexp": "^1.0.2", - "globby": "^8.0.1", - "grouped-queue": "^0.3.3", - "inquirer": "^6.0.0", - "is-scoped": "^1.0.0", - "lodash": "^4.17.10", - "log-symbols": "^2.2.0", - "mem-fs": "^1.1.0", - "strip-ansi": "^4.0.0", - "text-table": "^0.2.0", - "untildify": "^3.0.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - } - } - }, - "yeoman-test": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/yeoman-test/-/yeoman-test-1.9.1.tgz", - "integrity": "sha512-aWB8CglmjBfXd+U5g5Cm1b8KVW0uotjo521IgkepvhNXiAX/YswHYGVnbEFb0m9ZdXztELuNJn2UtuwgFZIw6Q==", - "requires": { - "inquirer": "^5.2.0", - "lodash": "^4.17.10", - "mkdirp": "^0.5.1", - "pinkie-promise": "^2.0.1", - "rimraf": "^2.4.4", - "sinon": "^5.0.7", - "yeoman-environment": "^2.3.0", - "yeoman-generator": "^2.0.5" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=" - }, - "dargs": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-5.1.0.tgz", - "integrity": "sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk=" - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "requires": { - "chalk": "^2.0.1" - } - }, - "mem-fs-editor": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-4.0.3.tgz", - "integrity": "sha512-tgWmwI/+6vwu6POan82dTjxEpwAoaj0NAFnghtVo/FcLK2/7IhPUtFUUYlwou4MOY6OtjTUJtwpfH1h+eSUziw==", - "requires": { - "commondir": "^1.0.1", - "deep-extend": "^0.6.0", - "ejs": "^2.5.9", - "glob": "^7.0.3", - "globby": "^7.1.1", - "isbinaryfile": "^3.0.2", - "mkdirp": "^0.5.0", - "multimatch": "^2.0.0", - "rimraf": "^2.2.8", - "through2": "^2.0.0", - "vinyl": "^2.0.1" - }, - "dependencies": { - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - } - } - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "read-chunk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz", - "integrity": "sha1-agTAkoAF7Z1C4aasVgDhnLx/9lU=", - "requires": { - "pify": "^3.0.0", - "safe-buffer": "^5.1.1" - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "rxjs": { - "version": "5.5.12", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", - "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", - "requires": { - "symbol-observable": "1.0.1" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "untildify": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", - "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==" - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - }, - "yeoman-environment": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-2.4.0.tgz", - "integrity": "sha512-SsvoL0RNAFIX69eFxkUhwKUN2hG1UwUjxrcP+T2ytwdhqC/kHdnFOH2SXdtSN1Ju4aO4xuimmzfRoheYY88RuA==", - "requires": { - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "debug": "^3.1.0", - "diff": "^3.5.0", - "escape-string-regexp": "^1.0.2", - "globby": "^8.0.1", - "grouped-queue": "^0.3.3", - "inquirer": "^6.0.0", - "is-scoped": "^1.0.0", - "lodash": "^4.17.10", - "log-symbols": "^2.2.0", - "mem-fs": "^1.1.0", - "strip-ansi": "^4.0.0", - "text-table": "^0.2.0", - "untildify": "^3.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "inquirer": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz", - "integrity": "sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw==", - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.11", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", - "requires": { - "tslib": "^1.9.0" - } - } - } - }, - "yeoman-generator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-2.0.5.tgz", - "integrity": "sha512-rV6tJ8oYzm4mmdF2T3wjY+Q42jKF2YiiD0VKfJ8/0ZYwmhCKC9Xs2346HVLPj/xE13i68psnFJv7iS6gWRkeAg==", - "requires": { - "async": "^2.6.0", - "chalk": "^2.3.0", - "cli-table": "^0.3.1", - "cross-spawn": "^6.0.5", - "dargs": "^5.1.0", - "dateformat": "^3.0.3", - "debug": "^3.1.0", - "detect-conflict": "^1.0.0", - "error": "^7.0.2", - "find-up": "^2.1.0", - "github-username": "^4.0.0", - "istextorbinary": "^2.2.1", - "lodash": "^4.17.10", - "make-dir": "^1.1.0", - "mem-fs-editor": "^4.0.0", - "minimist": "^1.2.0", - "pretty-bytes": "^4.0.2", - "read-chunk": "^2.1.0", - "read-pkg-up": "^3.0.0", - "rimraf": "^2.6.2", - "run-async": "^2.0.0", - "shelljs": "^0.8.0", - "text-table": "^0.2.0", - "through2": "^2.0.0", - "yeoman-environment": "^2.0.5" - } - } - } - }, - "zip-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", - "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", - "dev": true, - "requires": { - "archiver-utils": "^1.3.0", - "compress-commons": "^1.2.0", - "lodash": "^4.8.0", - "readable-stream": "^2.0.0" - } - } - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "regextras": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", - "integrity": "sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==", - "dev": true - }, - "resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } - } - }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tsconfig-paths": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", - "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - } - }, - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "v8-compile-cache": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", - "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", - "dev": true - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - } - } -}
diff --git a/package.json b/package.json deleted file mode 100644 index df375b4..0000000 --- a/package.json +++ /dev/null
@@ -1,28 +0,0 @@ -{ - "name": "code-owners", - "description": "code owners plugin", - "browser": true, - "scripts": { - "test": "polymer serve", - "eslint": "../../node_modules/eslint/bin/eslint.js --ext .html,.js ./ui/", - "eslintfix": "npm run eslint -- --fix", - "wct-test": "npm i && npx bower i && npx polymer test -l chrome" - }, - "devDependencies": { - "@bazel/rollup": "^1.1.0", - "@polymer/iron-test-helpers": "^3.0.1", - "@webcomponents/shadycss": "^1.9.2", - "@webcomponents/webcomponentsjs": "^1.3.3", - "bower": "^1.8.8", - "es6-promise": "^3.3.1", - "eslint": "^6.6.0", - "eslint-config-google": "^0.13.0", - "eslint-plugin-html": "^6.0.0", - "eslint-plugin-import": "^2.20.1", - "eslint-plugin-jsdoc": "^19.2.0", - "polymer-bridges": "file:../../polymer-bridges/", - "polymer-cli": "^1.9.11" - }, - "license": "Apache-2.0", - "private": true -}
diff --git a/resources/Documentation/backend-find-owners.md b/resources/Documentation/backend-find-owners.md index 9d2fab5..1ea48ab 100644 --- a/resources/Documentation/backend-find-owners.md +++ b/resources/Documentation/backend-find-owners.md
@@ -20,6 +20,21 @@ details about code owner config files are described [here](backends.html#codeOwnerConfigFiles)). +**NOTE:** Tools should never parse `OWNERS` files on their own, but instead +always use the [REST API](rest-api.html) to access code owner information. + +Parsing `OWNERS` files is highly discouraged because: + +* it's hard for tools to implement the parsing of `OWNERS` files in a way that + is consistent with how Gerrit parses them +* reimplementing the parsing logic outside of Gerrit is a lot of effort that can + be saved by using the REST API and letting Gerrit do the parsing +* parsing `OWNERS` files outside of Gerrit is likely less efficient since the + parser likely needs to reach out to Gerrit to fetch additional input (e.g. if + `OWNERS` files from other branches or repositories are imported) +* the format of `OWNERS` files may change (e.g. by the addition of new features) + which could break parsers outside of Gerrit + ### <a id="defaultCodeOwnerConfiguration">Default code owners Default code owners that apply to all branches can be defined in an `OWNERS` file in the root directory of the `refs/meta/config` branch. This `OWNERS` file @@ -266,6 +281,13 @@ per-file docs.config,*.md=richard.roe@example.com ``` +**NOTE:** It's important to not include additional spaces in `per-file` lines. +E.g. "per-file docs.config, test.config=richard.roe@example.com" will make +Richard Roe a code owner of the files "docs.config" and " test.config" (pay +attention to the leading space), but not for the file "test.config". The correct +line to make Richard Roe a code owner of the files "docs.config" and +"test.config" would be "per-file docs.config,test.config=richard.roe@example.com". + ##### <a id="doNotUsePathExpressionsForSubdirectories"> **NOTE:** It is discouraged to use path expressions that explicitly name subdirectories such as `my-subdir/*` as they will break when the subdirectory @@ -349,10 +371,12 @@ annotation, this annotation applies to all these users. E.g. if an annotation is set for the all users wildcard (aka `*`) it applies to all users. -**NOTE:** Only [email lines](#userEmails) and [per-file lines](#perFile) support -annotations, for other lines (e.g. [file lines](#fileKeyword) and [include -lines](#includeKeyword)) annotations are interpreted as [comments](#comments) -and are silently ignored. +**NOTE:** Only [email lines](#userEmails) and [per-file lines](#perFile) that +assign code ownership directly to users support annotations, for other lines +(e.g. [file lines](#fileKeyword), [include lines](#includeKeyword) and +[per-file lines](#perFile) that reference other `OWNERS` files via the +[file](#fileKeyword) keyword) annotations are interpreted as +[comments](#comments) and are silently ignored. ### <a id="comments">Comments
diff --git a/resources/Documentation/config-faqs.md b/resources/Documentation/config-faqs.md index 8766ffd..be0bfb0 100644 --- a/resources/Documentation/config-faqs.md +++ b/resources/Documentation/config-faqs.md
@@ -124,13 +124,15 @@ * Use the `--debug` option of the [List Code Owners](rest-api.html#list-code-owners-for-path-in-branch) REST endpoints to - get debug logs included into the response. + get debug logs included into the response (requires the caller + to be host administrator or have the [Check Code Owner + capability](rest-api.html#checkCodeOwner)). * Check the code owner config files for issues by calling the [Check Code Owner - Config File REST endpoint](rest-api.html#check-code-owner-config-files) + Config File REST endpoint](rest-api.html#check-code-owner-config-files). * Check the code ownership of a user for a certain path by calling the [Check Code Owner REST endpoint](rest-api.html#check-code-owner) (requires the caller to be host administrator or have the [Check Code Owner - capability](rest-api.html#checkCodeOwner)) + capability](rest-api.html#checkCodeOwner)). Bugs with the @PLUGIN@ plugin should be filed as issues for the Gerrit team, but only after other causes have been excluded.
diff --git a/resources/Documentation/config-guide.md b/resources/Documentation/config-guide.md index 0975cff..3eaefca 100644 --- a/resources/Documentation/config-guide.md +++ b/resources/Documentation/config-guide.md
@@ -30,6 +30,9 @@ approval](config.html#pluginCodeOwnersRequiredApproval) and [override approval](config.html#pluginCodeOwnersOverrideApproval). +Alternatively code owner approvals can be made sticky per file by [enabling +sticky approvals in the plugin configuration](config.html#pluginCodeOwnersEnableStickyApprovals). + ### <a id="implicitApprovals">Implicit code owner approvals It's possible to [enable implicit approvals](config.html#pluginCodeOwnersEnableImplicitApprovals) @@ -257,11 +260,11 @@ control who can approve changes: * Configure the `Code-Review` label with a voting range from `-2` to `+2` (see - [label configuration](../../../Documentation/config-labels.html#label_Code-Review). + [label configuration](../../../Documentation/config-labels.html#label_Code-Review)). * For the code owners assign permissions to allow voting from `Code-Review-2` to `Code-Review+2` and allow other users only to vote from `Code-Review-1` to `Code-Review+1` (see [label - permissions](../../../Documentation/access-control.html#category_review_labels) + permissions](../../../Documentation/access-control.html#category_review_labels)) * Setup a submit requirement that requires a `Code-Review+2` approval for making the change submittable, optionally with disallowing self approvals (see [submit @@ -271,6 +274,7 @@ for changes to become submittable. Advantages of this approach over using the `@PLUGIN@` plugin: + * it requires less configuration as you do not need to maintain code owner config files (aka `OWNERS` files) * groups are supported (permissions can be assigned to groups, but code @@ -280,6 +284,7 @@ detect if a change is submittable Disadvantages: + * there are no code owner suggestions and no code owner specific UI controls ---
diff --git a/resources/Documentation/config.md b/resources/Documentation/config.md index b4ea424..63feaf7 100644 --- a/resources/Documentation/config.md +++ b/resources/Documentation/config.md
@@ -261,6 +261,66 @@ in `@PLUGIN@.config`.\ By default `FALSE`. +<a id="pluginCodeOwnersEnableStickyApprovals">plugin.@PLUGIN@.enableStickyApprovals</a> +: Whether code owner approvals should be sticky on the files they approve, + even if these files are changed in follow-up patch sets.\ + \ + With this setting previous code owner approvals on files only get + invalidated if the approval is revoked, changed (e.g. from + `Code-Review+1` to `Code-Review-1`) or re-applied, but not by the upload + of a new patch set regardless of whether the new patch sets changes + these files.\ + \ + Code owner approvals are sticky per file but not on change level. This + means they do not show up as (copied) approvals on the change screen + like regular sticky approvals, but only count for the computation of the + code owner file statuses. In contrast to this setting, making code owner + approvals sticky on change level (by setting a [copy + condition](../../../Documentation/config-labels.html#label_copyCondition) + on the label that is used for code owner approvals) would make the + sticky approvals count for all files in the current patch set that are + owned by the approvers, regardless of whether these files existed in the + patch sets that were originally approved.\ + \ + Since code owner approvals that are sticky on file level are not shown + in the UI, users need to inspect the [per-file code owner + statuses](how-to-use.html#perFileCodeOwnerStatuses) to know which files + are code owner approved.\ + \ + Example:\ + If patch set 1 contains the files A, B and C and a user that owns the + files A and B approves the patch set by voting with `Code-Review+1`, A + and B are code owner approved for this patch set and all future patch + sets unless the approval is revoked, changed or re-applied. This means + if a second patch set is uploaded that touches files A and B, A and B + are still code owner approved. If a third patch set is uploaded that + adds file D that is also owned by the same code owner, file D is not + code owner approved since it is not present in patch set 1 on which the + user applied the code owner approval. If the user changes their vote on + patch set 3 from `Code-Review+1` to `Code-Review-1`, the new vote + invalidates the approval on patch set 1, so that in this example the + files A and B would no longer be code owner approved by this user. If + the user re-applies the `Code-Review+1` approval now all owned files + that are present in the current patch set, files A, B and D, are code + owner approved.\ + \ + Enabling sticky code owner approvals on file level can improve the + overall developer productivity significantly, since it makes + re-approvals on new patch sets unecessary in many cases. With sticky + code owner approvals on files new patch sets only need to be re-approved + if they touch additional files, and then only by the users that own + these files. In contrast to this, if code owner approvals are not + sticky (neither on file level via this setting, nor on change level via + a copy condition) a new patch set must always be re-approved by all code + owners that approved any of the contained files, no matter if these + files are touched in the new patch set.\ + \ + Can be overridden per project by setting + [codeOwners.enableStickyApprovals](#codeOwnersEnableStickyApprovals) + in `@PLUGIN@.config`.\ + \ + By default `false`. + <a id="pluginCodeOwnersGlobalCodeOwner">plugin.@PLUGIN@.globalCodeOwner</a> : The email of a user that should be a code owner globally across all branches.\ @@ -635,6 +695,22 @@ in `@PLUGIN@.config`.\ By default `true`. +<a id="pluginCodeOwnersEnableAsyncMessageOnCodeOwnerApproval">plugin.@PLUGIN@.enableAsyncMessageOnCodeOwnerApproval</a> +: When a code owner approval is applied on a change the @PLUGIN@ plugin + posts a change message that lists the paths that are owned by the code + owner to inform that these paths are code owner approved now. This + setting controls whether this change message should be posted + asynchronously instead of synchronously extending the change message + that is posted for the approval by Gerrit core.\ + Posting these change messages asynchronously improves the latency for + post review, since it does not need to wait until the owned paths are + computed for the code owner (computing the owned path for a user is + rather expensive).\ + Can be overridden per project by setting + [codeOwners.enableAsyncMessageOnCodeOwnerApproval](#codeOwnersEnableAsyncMessageOnCodeOwnerApproval) + in `@PLUGIN@.config`.\ + By default `true`. + <a id="pluginCodeOwnersMaxCodeOwnerConfigCacheSize">plugin.@PLUGIN@.maxCodeOwnerConfigCacheSize</a> : When computing code owner file statuses for a change (e.g. to compute the results for the code owners submit rule) parsed code owner config @@ -854,11 +930,24 @@ If implicit code owner approvals are disabled, code owners can still self-approve their own changes by voting on the change.\ Overrides the global setting - [plugin.@PLUGIN@.enableImplicitApprovals](#pluginCodeOwnersenableImplicitApprovals) + [plugin.@PLUGIN@.enableImplicitApprovals](#pluginCodeOwnersEnableImplicitApprovals) in `gerrit.config` and the `codeOwners.enableImplicitApprovals` setting from parent projects.\ If not set, the global setting - [plugin.@PLUGIN@.enableImplicitApprovals](#pluginCodeOwnersenableImplicitApprovals) + [plugin.@PLUGIN@.enableImplicitApprovals](#pluginCodeOwnersEnableImplicitApprovals) + in `gerrit.config` is used. + +<a id="codeOwnersEnableStickyApprovals">codeOwners.enableStickyApprovals</a> +: Whether code owner approvals should be sticky on the files they approve, + even if these files are changed in follow-up patch sets.\ + For details see + [plugin.@PLUGIN@.enableStickyApprovals](#pluginCodeOwnersEnableStickyApprovals).\ + Overrides the global setting + [plugin.@PLUGIN@.enableStickyApprovals](#pluginCodeOwnersEnableStickyApprovals) + in `gerrit.config` and the `codeOwners.enableStickyApprovals` setting + from parent projects.\ + If not set, the global setting + [plugin.@PLUGIN@.enableStickyApprovals](#pluginCodeOwnersEnableStickyApprovals) in `gerrit.config` is used. <a id="codeOwnersGlobalCodeOwner">codeOwners.globalCodeOwner</a> @@ -1204,6 +1293,25 @@ [plugin.@PLUGIN@.enableAsyncMessageOnAddReviewer](#pluginCodeOwnersEnableAsyncMessageOnAddReviewer) in `gerrit.config` is used. +<a id="codeOwnersEnableAsyncMessageOnCodeOwnerApproval">codeOwners.enableAsyncMessageOnCodeOwnerApproval</a> +: When a code owner approval is applied on a change the @PLUGIN@ plugin + posts a change message that lists the paths that are owned by the code + owner to inform that these paths are code owner approved now. This + setting controls whether this change message should be posted + asynchronously instead of synchronously extending the change message + that is posted for the approval by Gerrit core.\ + Posting these change messages asynchronously improves the latency for + post review, since it does not need to wait until the owned paths are + computed for the code owner (computing the owned path for a user is + rather expensive).\ + Overrides the global setting + [plugin.@PLUGIN@.enableAsyncMessageOnCodeOwnerApproval](#pluginCodeOwnersEnableAsyncMessageOnOwnerApproval) + in `gerrit.config` and the `codeOwners.enableAsyncMessageOnCodeOwnerApproval` + setting from parent projects.\ + If not set, the global setting + [plugin.@PLUGIN@.enableAsyncMessageOnCodeOwnerApproval](#pluginCodeOwnersEnableAsyncMessageOnCodeOwnerApproval) + in `gerrit.config` is used. + --- Back to [@PLUGIN@ documentation index](index.html)
diff --git a/resources/Documentation/how-to-use.md b/resources/Documentation/how-to-use.md index 552f675..d998286 100644 --- a/resources/Documentation/how-to-use.md +++ b/resources/Documentation/how-to-use.md
@@ -191,7 +191,7 @@ \  -### <a id="perFilCodeOwnerStatuses">Per file code owner statuses +### <a id="perFileCodeOwnerStatuses">Per file code owner statuses The `@PLUGIN@` plugin also shows the code owner statuses per file in the file list. @@ -225,6 +225,17 @@ \  +### <a id="reviewerOwners">Which reviewers own which file. + +Aside from the status of owners, the UI also provides information whether the +current viewer is an owner (by presenting a shield icon), or showing up to 3 +avatars of reviewers that own the given file. + + +If no reviewer is an owner of the given file, a red question mark is shown. + + + ### <a id="noStatus">No label and no status When you own all the files in your change and [implicit code owner
diff --git a/resources/Documentation/metrics.md b/resources/Documentation/metrics.md index 7c15669..5a54abb 100644 --- a/resources/Documentation/metrics.md +++ b/resources/Documentation/metrics.md
@@ -12,10 +12,15 @@ added as a reviewer. ** `post_type': Whether the change message was posted synchronously or asynchronously. +* `add_change_message_on_code_owner_approval`: + Latency for asynchronously adding a change message with the owned path when + a code owner approval is applied. * `compute_file_status`: Latency for computing the file status for one file. * `compute_file_statuses`: Latency for computing file statuses for all files in a change. + * `sticky_approvals`: + Whether sticky approvals on file level are enabled. * `compute_owned_paths`: Latency for computing file statuses. * `compute_owned_paths`: @@ -23,8 +28,8 @@ * `compute_patch_set_approvals`: Latency for computing the approvals of the current patch set. * `extend_change_message_on_post_review`: - Latency for extending the change message with the owned path when a code owner - approval is applied. + Latency for synchronously extending the change message with the owned path + when a code owner approval is applied. * `get_changed_files`: Latency for getting changed files from diff cache. * `prepare_file_status_computation`:
diff --git a/resources/Documentation/missing-owner.png b/resources/Documentation/missing-owner.png new file mode 100644 index 0000000..bd6b96c --- /dev/null +++ b/resources/Documentation/missing-owner.png Binary files differ
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md index 0151f7a..c97ba2c 100644 --- a/resources/Documentation/rest-api.md +++ b/resources/Documentation/rest-api.md
@@ -475,6 +475,71 @@ (visible) users is returned, as many as are needed to fill up the requested limit. +#### Request + +``` + GET /projects/foo%2Fbar/branches/master/code_owners/docs%2Findex.md HTTP/1.0 +``` + +#### Response + +``` + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "code_owners": [ + { + "account": { + "_account_id": 1000096 + } + }, + { + "account": { + "_account_id": 1001439 + } + }, + { + "account": { + "_account_id": 1007265 + } + }, + { + "account": { + "_account_id": 1009877 + } + }, + { + "account": { + "_account_id": 1002930 + } + } + ], + "code_owner_configs": [ + { + "project": "foo/bar", + "branch": "master", + "path": "/docs/OWNERS" + }, + { + "project": "foo/bar", + "branch": "master", + "path": "/OWNERS", + "imports": [ + { + "project": "foo", + "branch": "master", + "path": "/OWNERS", + "import_mode": "ALL", + } + ] + } + ] + } +``` + #### <a id="scoringFactors">Scoring Factors The following factors are taken into account for computing the scores of the @@ -550,36 +615,6 @@ The same applies for [default code owners](config-guide.html#codeOwners). -#### Request - -``` - GET /projects/foo%2Fbar/branches/master/code_owners/docs%2Findex.md HTTP/1.0 -``` - -#### Response - -``` - HTTP/1.1 200 OK - Content-Disposition: attachment - Content-Type: application/json; charset=UTF-8 - - )]}' - { - "code_owners": [ - { - "account": { - "_account_id": 1000096 - } - }, - { - "account": { - "_account_id": 1001439 - }, - } - ] - } -``` - #### <a id="batch-list-code-owners"> Batch Request There is no REST endpoint that allows to retrieve code owners for multiple @@ -726,11 +761,12 @@ The following request parameters can be specified: -| Field Name | | Description | -| ------------ | --------- | ----------- | -| `start`\|`S` | optional | Number of owned paths to skip. Allows to page over the owned files. By default 0. -| `limit`\|`n` | optional | Limit defining how many [OwnedChangedFileInfo](#owned-changed-file-info) entities should be returned at most. By default 50. -| `user` | mandatory | user for which the owned paths should be returned +| Field Name | | Description | +| ----------------------- | --------- | ----------- | +| `start`\|`S` | optional | Number of owned paths to skip. Allows to page over the owned files. By default 0. +| `limit`\|`n` | optional | Limit defining how many [OwnedChangedFileInfo](#owned-changed-file-info) entities should be returned at most. By default 50. +| `check_reviewers`\|`c'` | optional | Whether it should be checked which reviewers are owners. When set, [OwnedPathInfo](#owned-path-info) will contain the `owners` field +| `user` | mandatory | user for which the owned paths should be returned #### Request @@ -781,13 +817,6 @@ "owned": true } } - ], - "owned_paths": [ - "/abc/old-name.md", - "/foo/bar/baz.md", - "/foo/baz/bar.md", - "/foo/new-name.md", - "/foo/old-name.md" ] } ``` @@ -920,7 +949,23 @@ code owner. Note that if the email is configured as global code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner. | `is_owned_by_all_users` | Whether the the specified path in the branch is owned by all users (aka `*`). | `annotation` | Annotations that were set for the user. Contains only supported annotations (unsupported annotations are reported in the `debugs_logs`). Sorted alphabetically. -| `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner. +| `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner. This information is purely for debugging and the output may be changed at any time. This means bot callers must not parse the debug logs. + +--- + +### <a id="code-owner-config-file-info"> CodeOwnerConfigFileInfo +The `CodeOwnerConfigFileInfo` entity contains information about a code owner +config file and its imports. + +| Field Name | | Description | +| ---------- | -------- | ----------- | +| `project` || The name of the project from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded. +| `branch` || The name of the branch from which the code owner config was loaded, or for unresolved imports, from which the code owner config was supposed to be loaded. +| `path` || The absolute path of the code owner config file. +| `imports` | optional | Imported code owner config files as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities. +| `unresolved_imports` | optional | Imported code owner config files that couldn't be resolved as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities. +| `unresolved_error_message` | optional | Message explaining why this code owner config couldn't be resolved. Only set if the `CodeOwnerConfigFileInfo` represents an import code owner config file that couldn't be resolved. +| `import_mode` | optional | The import mode (`ALL` or `GLOBAL_CODE_OWNER_SETS_ONLY`). Only set if the `CodeOwnerConfigFileInfo` represents an imported code owner config file. --- @@ -1048,7 +1093,8 @@ | ------------- | -------- | ----------- | | `code_owners` | | List of code owners as [CodeOwnerInfo](#code-owner-info) entities. The code owners are sorted by a score that is computed from mutliple [scoring factors](#scoringFactors). | `owned_by_all_users` | optional | Whether the path is owned by all users. Not set if `false`. -| `debug_logs` | optional | Debug logs that may help to understand why a user is or isn't suggested as a code owner. Only set if requested via `--debug`. +| `code_owner_configs` || The code owner config files that have been inspected to gather the code owners as [CodeOwnerConfigFileInfo](#code-owner-config-file-info) entities. +| `debug_logs` | optional | Debug logs that may help to understand why a user is or isn't suggested as a code owner. Only set if requested via `--debug`. This information is purely for debugging and the output may be changed at any time. This means bot callers must not parse the debug logs. ### <a id="file-code-owner-status-info"> FileCodeOwnerStatusInfo The `FileCodeOwnerStatusInfo` entity describes the code owner statuses for a @@ -1090,6 +1136,7 @@ | ---------- | -------- | ----------- | | `path` | | The absolute file path. | `owned` | optional | `true` is the path is owned by the user. Otherwise unset. +| `owners` | optional | List of owners for this file that are either the specified user or the reviewers if `check_reviewers` is passed to the request. ### <a id="owned-paths-info"> OwnedPathsInfo The `OwnedPathsInfo` entity contains paths that are owned by a user.
diff --git a/resources/Documentation/reviewer-owner.png b/resources/Documentation/reviewer-owner.png new file mode 100644 index 0000000..8b4ecfc --- /dev/null +++ b/resources/Documentation/reviewer-owner.png Binary files differ
diff --git a/resources/Documentation/user-guide.md b/resources/Documentation/user-guide.md index 37049fc..a176398 100644 --- a/resources/Documentation/user-guide.md +++ b/resources/Documentation/user-guide.md
@@ -24,6 +24,12 @@ Who is a code owner of a path is controlled via [code owner config files](#codeOwnerConfigFiles) (e.g. `OWNERS` files). +**NOTE:** Being a code owner doesn't grant the user permissions to approve +changes, but permissions to vote on the label that is required as a +[code owner approval](#codeOwnerApproval) must be configured separately. This +means it's possible that a user is a code owner, but misses permissions to apply +code owner approvals. + ## <a id="whyCodeOwners">Why should code owners be used? Code owners are gatekeepers before a change is submitted, they enforce standards
diff --git a/resources/Documentation/validation.md b/resources/Documentation/validation.md index 60ee4d2..3f2d639 100644 --- a/resources/Documentation/validation.md +++ b/resources/Documentation/validation.md
@@ -182,6 +182,13 @@ * the project from which the file should be imported doesn't permit reads (e.g. has the state `HIDDEN`) +**NOTE:** Non-resolvable code owner emails are rejected with a generic message, +rather than informing about the exact reason why a code owner email is not +resolvable. This is intentional and prevents that uploaders can probe the +existence of emails/accounts (if specific reasons would be returned an uploader +could create a code owner config file with arbitrary emails and then deduce from +the messages which emails/accounts exist, which would a privacy issue). + **NOTE:** Whether commits that newly add non-resolvable code owners and non-resolvable imports are rejected on commit received and on submit is controlled by the
diff --git a/test/common-test-setup.js b/test/common-test-setup.js deleted file mode 100644 index d2b9953..0000000 --- a/test/common-test-setup.js +++ /dev/null
@@ -1,58 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import 'polymer-bridges/polymer/lib/utils/boot_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/resolve-url_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/settings_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/mixin_bridge.js'; -import 'polymer-bridges/polymer/lib/elements/dom-module_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/style-gather_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/path_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/case-map_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/async_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/wrap_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/properties-changed_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/property-accessors_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/template-stamp_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/property-effects_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/telemetry_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/properties-mixin_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/debounce_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/gestures_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/gesture-event-listeners_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/dir-mixin_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/render-status_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/unresolved_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/array-splice_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/flattened-nodes-observer_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/flush_bridge.js'; -import 'polymer-bridges/polymer/lib/legacy/polymer.dom_bridge.js'; -import 'polymer-bridges/polymer/lib/legacy/legacy-element-mixin_bridge.js'; -import 'polymer-bridges/polymer/lib/legacy/class_bridge.js'; -import 'polymer-bridges/polymer/lib/legacy/polymer-fn_bridge.js'; -import 'polymer-bridges/polymer/lib/mixins/mutable-data_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/templatize_bridge.js'; -import 'polymer-bridges/polymer/lib/legacy/templatizer-behavior_bridge.js'; -import 'polymer-bridges/polymer/lib/elements/dom-bind_bridge.js'; -import 'polymer-bridges/polymer/lib/utils/html-tag_bridge.js'; -import 'polymer-bridges/polymer/polymer-element_bridge.js'; -import 'polymer-bridges/polymer/lib/elements/dom-repeat_bridge.js'; -import 'polymer-bridges/polymer/lib/elements/dom-if_bridge.js'; -import 'polymer-bridges/polymer/lib/elements/array-selector_bridge.js'; -import 'polymer-bridges/polymer/lib/elements/custom-style_bridge.js'; -import 'polymer-bridges/polymer/lib/legacy/mutable-data-behavior_bridge.js'; -import 'polymer-bridges/polymer/polymer-legacy_bridge.js'; -import '@polymer/iron-test-helpers/iron-test-helpers.js'; \ No newline at end of file
diff --git a/test/index.html b/test/index.html deleted file mode 100644 index 2ee74df..0000000 --- a/test/index.html +++ /dev/null
@@ -1,28 +0,0 @@ -<!DOCTYPE html> -<!-- -@license -Copyright (C) 2020 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> - -<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> -<title>Test Runner</title> -<meta charset="utf-8"> -<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script> -<script src="../bower_components/web-component-tester/browser.js"></script> -<script> - WCT.loadSuites([ - '../ui/code-owners-service_test.html' - ]); -</script>
diff --git a/ui/BUILD b/ui/BUILD deleted file mode 100644 index 274676e..0000000 --- a/ui/BUILD +++ /dev/null
@@ -1,14 +0,0 @@ -load("//tools/bzl:js.bzl", "gerrit_js_bundle") - -package_group( - name = "visibility", - packages = ["//plugins/code-owners/..."], -) - -package(default_visibility = [":visibility"]) - -gerrit_js_bundle( - name = "code-owners", - srcs = glob(["*.js"]), - entry_point = "plugin.js", -)
diff --git a/ui/code-owners-api.js b/ui/code-owners-api.js deleted file mode 100644 index 80b3a2b..0000000 --- a/ui/code-owners-api.js +++ /dev/null
@@ -1,205 +0,0 @@ -/** - * @license - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO: Try to remove it. The ResponseError and getErrorMessage duplicates -// code from the gr-plugin-rest-api.ts. This code is required because -// we want custom error processing in some functions. For details see -// the original gr-plugin-rest-api.ts file/ -class ResponseError extends Error { - constructor(response) { - super(); - this.response = response; - } -} - -export class ServerConfigurationError extends Error { - constructor(msg) { - super(msg); - } -} - -async function getErrorMessage(response) { - const text = await response.text(); - return text ? - `${response.status}: ${text}` : - `${response.status}`; -} - -/** - * Responsible for communicating with the rest-api - * - * @see resources/Documentation/rest-api.md - */ -export class CodeOwnersApi { - constructor(restApi) { - this.restApi = restApi; - } - - /** - * Send a get request and provides custom response-code handling - */ - async _get(url) { - const errFn = (response, error) => { - if (error) throw error; - if (response) throw new ResponseError(response); - throw new Error('Generic REST API error'); - }; - try { - return await this.restApi.send( - 'GET', - url, - undefined, - errFn - ); - } catch (err) { - if (err instanceof ResponseError && err.response.status === 409) { - return getErrorMessage(err.response).then(msg => { - throw new ServerConfigurationError(msg); - }); - } - throw err; - } - } - - /** - * Returns a promise fetching the owner statuses for all files within the change. - * - * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#change-endpoints - * @param {string} changeId - */ - listOwnerStatus(changeId) { - return this._get(`/changes/${changeId}/code_owners.status?limit=100000`); - } - - /** - * Returns a promise fetching the owners for a given path. - * - * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#list-code-owners-for-path-in-branch - * @param {string} changeId - * @param {string} path - */ - listOwnersForPath(changeId, path, limit) { - return this._get( - `/changes/${changeId}/revisions/current/code_owners` + - `/${encodeURIComponent(path)}?limit=${limit}&o=DETAILS` - ); - } - - /** - * Returns a promise fetching the owners config for a given path. - * - * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#branch-endpoints - * @param {string} project - * @param {string} branch - * @param {string} path - */ - getConfigForPath(project, branch, path) { - return this._get( - `/projects/${encodeURIComponent(project)}/` + - `branches/${encodeURIComponent(branch)}/` + - `code_owners.config/${encodeURIComponent(path)}` - ); - } - - /** - * Returns a promise fetching the owners config for a given branch. - * - * @doc https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#branch-endpoints - * @param {string} project - * @param {string} branch - */ - async getBranchConfig(project, branch) { - try { - const config = await this._get( - `/projects/${encodeURIComponent(project)}/` + - `branches/${encodeURIComponent(branch)}/` + - `code_owners.branch_config` - ); - if (config.override_approval && - !(config.override_approval instanceof Array)) { - // In the upcoming backend changes, the override_approval will be changed - // to array with (possible) multiple items. - // While this transition is in progress, the frontend supports both API - - // the old one and the new one. - return {...config, override_approval: [config.override_approval]}; - } - return config; - } catch (err) { - if (err instanceof ResponseError) { - if (err.response.status === 404) { - // The 404 error means that the branch doesn't exist and - // the plugin should be disabled. - return {disabled: true}; - } - return getErrorMessage(err.response).then(msg => { - throw new Error(msg); - }); - } - throw err; - } - } -} - -/** - * Wrapper around codeOwnerApi, sends each requests only once and then cache - * the response. A new CodeOwnersCacheApi instance is created every time when a - * new change object is assigned. - * Gerrit never updates existing change object, but instead always assigns a new - * change object. Particularly, a new change object is assigned when a change - * is updated and user clicks reload toasts to see the updated change. - * As a result, the lifetime of a cache is the same as a lifetime of an assigned - * change object. - * Periodical cache invalidation can lead to inconsistency in UI, i.e. - * user can see the old reviewers list (reflects a state when a change was - * loaded) and code-owners status for the current reviewer list. To avoid - * this inconsistency, the cache doesn't invalidate. - */ -export class CodeOwnersCacheApi { - constructor(codeOwnerApi, change) { - this.codeOwnerApi = codeOwnerApi; - this.change = change; - this.promises = {}; - } - - _fetchOnce(cacheKey, asyncFn) { - if (!this.promises[cacheKey]) { - this.promises[cacheKey] = asyncFn(); - } - return this.promises[cacheKey]; - } - - getAccount() { - return this._fetchOnce('getAccount', () => this._getAccount()); - } - - async _getAccount() { - const loggedIn = await this.codeOwnerApi.restApi.getLoggedIn(); - if (!loggedIn) return undefined; - return await this.codeOwnerApi.restApi.getAccount(); - } - - listOwnerStatus() { - return this._fetchOnce('listOwnerStatus', - () => this.codeOwnerApi.listOwnerStatus(this.change._number)); - } - - getBranchConfig() { - return this._fetchOnce('getBranchConfig', - () => this.codeOwnerApi.getBranchConfig(this.change.project, - this.change.branch)); - } -}
diff --git a/ui/code-owners-banner.js b/ui/code-owners-banner.js deleted file mode 100644 index 9bbf64f..0000000 --- a/ui/code-owners-banner.js +++ /dev/null
@@ -1,223 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {CodeOwnersModelMixin} from './code-owners-model-mixin.js'; -import {PluginState, isPluginErrorState} from './code-owners-model.js'; - -// There are 2 elements in this file: -// CodeOwnersBanner - visual elements. This element is shown at the top -// of the page when pluginStatus are changed to failed. -// The CodeOwnersBanner is added to the 'banner' endpoint. The endpoint -// is placed at the top level of the gerrit and doesn't provide a current -// change, so it is impossible to get the pluginStatus directly in the -// CodeOwnersBanner. -// To solve this problem, this files provides non-visual -// CodeOwnersPluginStatusNotifier element. This element is added to the -// change-view-integration endpoint, so it can track the plugin state. -// When a plugin state is changed, the CodeOwnersPluginStatusNotifier updates -// the pluginStatus property of the banner. - -// CodeOwnersBanner and CodeOwnersPluginStatusNotifier can be attached and -// detached in any order. The following piece of code ensures that these -// banner and notifier are always connected correctly. The code assumes, -// that max one banner and one notifier can be connected at any time. -let activeBanner = undefined; -let activeStatusNotifier = undefined; - -function setActiveBanner(banner) { - // banner is null when CodeOwnersBanner has been disconnected - activeBanner = banner; - if (activeStatusNotifier) { - activeStatusNotifier.banner = banner; - } -} - -function setActiveStatusNotifier(notifier) { - // notifier is null when CodeOwnersBanner has been disconnected - if (activeStatusNotifier) { - if (activeStatusNotifier.banner) { - activeStatusNotifier.banner.pluginStatus = undefined; - } - activeStatusNotifier.banner = undefined; - } - activeStatusNotifier = notifier; - if (activeStatusNotifier) { - activeStatusNotifier.banner = activeBanner; - } -} - -export class CodeOwnersBanner extends Polymer.Element { - static get is() { return 'gr-code-owners-banner'; } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host { - display: block; - overflow: hidden; - background: red; - } - .text { - color: white; - font-family: var(--header-font-family); - font-size: var(--font-size-h3); - font-weight: var(--font-weight-h3); - line-height: var(--line-height-h3); - margin-left: var(--spacing-l); - } - </style> - <span class="text">[[_getErrorText(pluginStatus)]]</span> - <gr-button link on-click="_showFailDetails"> - Details - </gr-button> - `; - } - - static get properties() { - return { - // @internal attributes - hidden: { - type: Boolean, - value: true, - reflectToAttribute: true, - computed: '_computeHidden(pluginStatus)', - }, - pluginStatus: { - type: Object, - }, - }; - } - - connectedCallback() { - super.connectedCallback(); - setActiveBanner(this); - } - - disconnectedCallback() { - super.disconnectedCallback(); - setActiveBanner(undefined); - } - - _computeHidden(pluginStatus) { - return !pluginStatus || !isPluginErrorState(pluginStatus.state); - } - - _getErrorText(pluginStatus) { - return !pluginStatus || pluginStatus.state === PluginState.Failed ? - 'Error: Code-owners plugin has failed' : - 'The code-owners plugin has configuration issue. ' + - 'Please contact the project owner or the host admin.'; - } - - _showFailDetails() { - showPluginFailedMessage(this, this.pluginStatus); - } -} - -customElements.define(CodeOwnersBanner.is, CodeOwnersBanner); - -export class CodeOwnersPluginStatusNotifier extends - CodeOwnersModelMixin(Polymer.Element) { - static get is() { - return 'owners-plugin-status-notifier'; - } - - static get properties() { - return { - banner: { - type: Object, - }, - /** - * This is a temporary property for interop with find-owner plugin. - * It stores information about the change and code-owners status for this - * change. The possible values for branchState are: - * LOADING - branch config are requesting - * FAILED - plugin are in failed state (server returned error, etc...) - * ENABLED - code-owners is enabled for the change - * DISABLED - code-owners is disable for the change - * - * ENABLED/DISABLED values are different from the pluginStatus. - * For merged and abandoned changes the model.pluginStatus value is - * always DISABLED, even if code-owners is enabled for a project. - */ - _stateForFindOwnersPlugin: { - type: Object, - notify: true, - computed: - '_getStateForFindOwners(model.pluginStatus, model.branchConfig,' + - ' change)', - }, - }; - } - - static get observers() { - return [ - '_pluginStatusOrBannerChanged(model.pluginStatus, banner)', - ]; - } - - connectedCallback() { - super.connectedCallback(); - setActiveStatusNotifier(this); - } - - disconnectedCallback() { - super.disconnectedCallback(); - setActiveStatusNotifier(undefined); - } - - _pluginStatusOrBannerChanged(status, banner) { - if (!banner) return; - banner.pluginStatus = status; - } - - loadPropertiesAfterModelChanged() { - this.modelLoader.loadPluginStatus(); - this.modelLoader.loadBranchConfig(); - } - - _getStateForFindOwners(pluginStatus, branchConfig, change) { - if (pluginStatus === undefined || branchConfig === undefined || - change == undefined) { - return { - branchState: 'LOADING', - }; - } - if (isPluginErrorState(pluginStatus.state)) { - return { - change, - branchState: 'FAILED', - }; - } - return { - change, - branchState: branchConfig.disabled ? 'DISABLED' : 'ENABLED', - }; - } -} - -customElements.define(CodeOwnersPluginStatusNotifier.is, - CodeOwnersPluginStatusNotifier); - -export function showPluginFailedMessage(sourceEl, pluginStatus) { - sourceEl.dispatchEvent( - new CustomEvent('show-error', { - detail: {message: pluginStatus.failedMessage}, - composed: true, - bubbles: true, - }) - ); -}
diff --git a/ui/code-owners-fetcher.js b/ui/code-owners-fetcher.js deleted file mode 100644 index 6fd5b35..0000000 --- a/ui/code-owners-fetcher.js +++ /dev/null
@@ -1,205 +0,0 @@ -/** - * @license - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {CodeOwnersApi} from './code-owners-api.js'; - -/** - * All statuses returned for owner status. - * - * @enum - */ -export const OwnerStatus = { - INSUFFICIENT_REVIEWERS: 'INSUFFICIENT_REVIEWERS', - PENDING: 'PENDING', - APPROVED: 'APPROVED', -}; - -/** - * @enum - */ -export const FetchStatus = { - /** Fetch hasn't been started */ - NOT_STARTED: 0, - /** - * Fetch has been started, but not all files has been finished. - * Pausing during fetching doesn't change state. - */ - FETCHING: 1, - /** - * All owners has been loaded. resume/pause call doesn't change state. - */ - FINISHED: 2, -}; - -/** - * Fetch owners for files. The class fetches owners in parallel and allows to - * pause/resume fetch. - */ -class OwnersFetcher { - /** - * Creates a fetcher in paused state. Actual fetching starts after resume() - * is called. - * - * @param {Array<string>} filesToFetch - Files paths for loading owners. - * @param {number} ownersLimit - number of requested owners per file. - * @param {number} maxConcurrentRequest - max number of concurrent requests to server. - */ - constructor(codeOwnerApi, changeId, filesToFetch, ownersLimit, - maxConcurrentRequest) { - this._fetchedOwners = new Map(); - this._ownersLimit = ownersLimit; - this._paused = true; - this._pausedFilesFetcher = []; - this._filesToFetch = filesToFetch; - this._fetchFilesPromises = []; - this._codeOwnerApi = codeOwnerApi; - this._changeId = changeId; - - for (let i = 0; i < maxConcurrentRequest; i++) { - this._fetchFilesPromises.push(this._fetchFiles()); - } - } - - async _fetchFiles() { - for (;;) { - const filePath = await this._getNextFilePath(); - if (!filePath) return; - try { - this._fetchedOwners.set(filePath, { - owners: await this._codeOwnerApi.listOwnersForPath(this._changeId, - filePath, this._ownersLimit), - }); - } catch (error) { - this._fetchedOwners.set(filePath, {error}); - } - } - } - - async _getNextFilePath() { - if (this._paused) { - await new Promise(resolve => this._pausedFilesFetcher.push(resolve)); - } - if (this._filesToFetch.length === 0) return null; - return this._filesToFetch.splice(0, 1)[0]; - } - - async waitFetchComplete() { - await Promise.allSettled(this._fetchFilesPromises); - } - - resume() { - if (!this._paused) return; - this._paused = false; - for (const fetcher of this._pausedFilesFetcher.splice(0, - this._pausedFilesFetcher.length)) { - fetcher(); - } - } - - pause() { - this._paused = true; - } - - getFetchedOwners() { - return this._fetchedOwners; - } - - getFiles() { - const result = []; - for (const [path, info] of this._fetchedOwners.entries()) { - result.push({path, info}); - } - return result; - } -} - -export class OwnersProvider { - constructor(restApi, change, options) { - this.change = change; - this.options = options; - this._totalFetchCount = 0; - this._status = FetchStatus.NOT_STARTED; - this._codeOwnerApi = new CodeOwnersApi(restApi); - } - - getStatus() { - return this._status; - } - - getProgressString() { - return !this._ownersFetcher || this._totalFetchCount === 0 ? - `Loading suggested owners ...` : - `${this._ownersFetcher.getFetchedOwners().size} out of ` + - `${this._totalFetchCount} files have returned suggested owners.`; - } - - getFiles() { - if (!this._ownersFetcher) return []; - return this._ownersFetcher.getFiles(); - } - - async fetchSuggestedOwners(codeOwnerStatusMap) { - if (this._status !== FetchStatus.NOT_STARTED) { - await this._ownersFetcher.waitFetchComplete(); - return; - } - const filesToFetch = this._getFilesToFetch(codeOwnerStatusMap); - this._totalFetchCount = filesToFetch.length; - this._ownersFetcher = new OwnersFetcher(this._codeOwnerApi, this.change.id, - filesToFetch, - this.options.ownersLimit, this.options.maxConcurrentRequests); - this._status = FetchStatus.FETCHING; - this._ownersFetcher.resume(); - await this._ownersFetcher.waitFetchComplete(filesToFetch); - this._status = FetchStatus.FINISHED; - } - - _getFilesToFetch(codeOwnerStatusMap) { - // only fetch those not approved yet - const filesGroupByStatus = [...codeOwnerStatusMap.entries()].reduce( - (list, [file, fileInfo]) => { - if (list[fileInfo.status]) list[fileInfo.status].push(file); - return list; - } - , { - [OwnerStatus.PENDING]: [], - [OwnerStatus.INSUFFICIENT_REVIEWERS]: [], - [OwnerStatus.APPROVED]: [], - } - ); - // always fetch INSUFFICIENT_REVIEWERS first, then pending and then approved - return filesGroupByStatus[OwnerStatus.INSUFFICIENT_REVIEWERS] - .concat(filesGroupByStatus[OwnerStatus.PENDING]) - .concat(filesGroupByStatus[OwnerStatus.APPROVED]); - } - - pause() { - if (!this._ownersFetcher) return; - this._ownersFetcher.pause(); - } - - resume() { - if (!this._ownersFetcher) return; - this._ownersFetcher.resume(); - } - - reset() { - this._totalFetchCount = 0; - this.ownersFetcher = null; - this._status = FetchStatus.NOT_STARTED; - } -}
diff --git a/ui/code-owners-model-loader.js b/ui/code-owners-model-loader.js deleted file mode 100644 index cdc04dd..0000000 --- a/ui/code-owners-model-loader.js +++ /dev/null
@@ -1,151 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {SuggestionsState} from './code-owners-model.js'; -import {ServerConfigurationError} from './code-owners-api.js'; - -/** - * ModelLoader provides a method for loading data into the model. - * It is a bridge between an ownersModel and an ownersService. - * When UI elements depends on a model property XXX, the element should - * observe (or bind to) the property and call modelLoader.loadXXX method - * to load the value. - * It is recommended to use CodeOwnersModelMixin and call all load... methods - * in the loadPropertiesAfterModelChanged method - */ -export class ModelLoader { - constructor(ownersService, ownersModel) { - this.ownersService = ownersService; - this.ownersModel = ownersModel; - } - - async _loadProperty(propertyName, propertyLoader, propertySetter) { - // Load property only if it was not already loaded - if (this.ownersModel[propertyName] !== undefined) return; - let newValue; - try { - newValue = await propertyLoader(); - } catch (e) { - if (e instanceof ServerConfigurationError) { - this.ownersModel.setServerConfigurationError(e.message); - return; - } - console.error(e); - this.ownersModel.setPluginFailed(e.message); - return; - } - // It is possible, that several requests is made in parallel. - // Store only the first result and discard all other results. - // (also, due to the CodeOwnersCacheApi all result must be identical) - if (this.ownersModel[propertyName] !== undefined) return; - propertySetter(newValue); - } - - async loadBranchConfig() { - await this._loadProperty('branchConfig', - () => this.ownersService.getBranchConfig(), - value => this.ownersModel.setBranchConfig(value) - ); - } - - async loadStatus() { - await this._loadProperty('status', - () => this.ownersService.getStatus(), - value => this.ownersModel.setStatus(value) - ); - } - - async loadUserRole() { - await this._loadProperty('userRole', - () => this.ownersService.getLoggedInUserInitialRole(), - value => this.ownersModel.setUserRole(value) - ); - } - - async loadPluginStatus() { - await this._loadProperty('pluginStatus', - () => this.ownersService.isCodeOwnerEnabled(), - value => this.ownersModel.setPluginEnabled(value) - ); - } - - async loadAreAllFilesApproved() { - await this._loadProperty('areAllFilesApproved', - () => this.ownersService.areAllFilesApproved(), - value => this.ownersModel.setAreAllFilesApproved(value) - ); - } - - async loadSuggestions(suggestionsType) { - this.pauseActiveSuggestedOwnersLoading(); - this.activeLoadSuggestionType = suggestionsType; - if (this.ownersModel.suggestionsByTypes[suggestionsType].state === - SuggestionsState.Loading) { - this.ownersService.resumeSuggestedOwnersLoading(suggestionsType); - return; - } - - // If a loading has been started already, do nothing - if (this.ownersModel.suggestionsByTypes[suggestionsType].state !== - SuggestionsState.NotLoaded) return; - - this.ownersModel.setSuggestionsState(suggestionsType, - SuggestionsState.Loading); - let suggestedOwners; - try { - suggestedOwners = - await this.ownersService.getSuggestedOwners(suggestionsType); - } catch (e) { - console.error(e); - this.ownersModel.setSuggestionsState(suggestionsType, - SuggestionsState.LoadFailed); - // The selectedSuggestionsType can be changed while getSuggestedOwners - // is executed. The plugin should fail only if the selectedSuggestionsType - // is the same. - if (this.ownersModel.selectedSuggestionsType === suggestionsType) { - this.ownersModel.setPluginFailed(e.message); - } - return; - } - this.ownersModel.setSuggestionsFiles(suggestionsType, - suggestedOwners.files); - this.ownersModel.setSuggestionsState(suggestionsType, - SuggestionsState.Loaded); - } - - pauseActiveSuggestedOwnersLoading() { - if (!this.activeLoadSuggestionType) return; - this.ownersService.pauseSuggestedOwnersLoading( - this.activeLoadSuggestionType); - } - - async updateLoadSelectedSuggestionsProgress() { - const suggestionsType = this.ownersModel.selectedSuggestionsType; - let suggestedOwners; - try { - suggestedOwners = - await this.ownersService.getSuggestedOwnersProgress(suggestionsType); - } catch { - // Ignore any error, keep progress unchanged. - return; - } - this.ownersModel.setSuggestionsLoadProgress(suggestionsType, - suggestedOwners.progress); - this.ownersModel.setSuggestionsFiles(suggestionsType, - suggestedOwners.files); - } -}
diff --git a/ui/code-owners-model-mixin.js b/ui/code-owners-model-mixin.js deleted file mode 100644 index 80c8ffb..0000000 --- a/ui/code-owners-model-mixin.js +++ /dev/null
@@ -1,101 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {CodeOwnerService} from './code-owners-service.js'; -import {ModelLoader} from './code-owners-model-loader.js'; -import {CodeOwnersModel} from './code-owners-model.js'; - -/** - * The CodeOwnersMixin adds several properties to a class and translates - * 'model-property-changed' events from the model to the notifyPath calls. - * This allows to use properties of the model in observers, calculated - * properties and bindings. - * It is guaranteed, that model and modelLoader are set only if change, - * reporting and restApi properties are set. - */ -export const CodeOwnersModelMixin = Polymer.dedupingMixin(base => { - return class extends base { - constructor(...args) { - super(...args); - /** - * The modelLoader allows an element to request a property - * Typically should be used in loadPropertiesAfterModelChanged - * to ensure that all required model properties are loaded - */ - this.modelLoader = undefined; - } - - static get properties() { - return { - /* The following 3 properties (change, reporting, restApi) have to be - * set from the outside for the mixin to work. - */ - change: Object, - reporting: Object, - restApi: Object, - model: { - type: Object, - observer: '_modelChanged', - }, - }; - } - - static get observers() { - return ['onInputChanged(restApi, change, reporting)']; - } - - onInputChanged(restApi, change, reporting) { - if ([restApi, change, reporting].includes(undefined)) { - this.model = undefined; - this.modelLoader = undefined; - return; - } - const ownerService = CodeOwnerService.getOwnerService( - this.restApi, - this.change - ); - const model = CodeOwnersModel.getModel(change); - this.modelLoader = new ModelLoader(ownerService, model); - // Assign model after modelLoader, so modelLoader can be used in - // the _requirePropertiesAfterModelChanged method - this.model = model; - } - - _modelChanged(newModel) { - if (this.modelPropertyChangedUnsubscriber) { - this.modelPropertyChangedUnsubscriber(); - this.modelPropertyChangedUnsubscriber = undefined; - } - if (!newModel) return; - const propertyChangedListener = e => { - this.notifyPath(`model.${e.detail.propertyName}`); - }; - newModel.addEventListener('model-property-changed', - propertyChangedListener); - this.modelPropertyChangedUnsubscriber = () => { - newModel.removeEventListener('model-property-changed', - propertyChangedListener); - }; - this.loadPropertiesAfterModelChanged(); - } - - loadPropertiesAfterModelChanged() { - // The class should override this method and calls appropriate methods - // from this.modelLoader to ensure that all required properties are - // set in model. - } - }; -});
diff --git a/ui/code-owners-model.js b/ui/code-owners-model.js deleted file mode 100644 index b6fcbbf..0000000 --- a/ui/code-owners-model.js +++ /dev/null
@@ -1,217 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const SuggestionsState = { - NotLoaded: 'NotLoaded', - Loaded: 'Loaded', - Loading: 'Loading', - LoadFailed: 'LoadFailed', -}; - -export const PluginState = { - Enabled: 'Enabled', - Disabled: 'Disabled', - ServerConfigurationError: 'ServerConfigurationError', - Failed: 'Failed', -}; - -export function isPluginErrorState(state) { - return state === PluginState.ServerConfigurationError || - state === PluginState.Failed; -} - -export const SuggestionsType = { - BEST_SUGGESTIONS: 'BEST_SUGGESTIONS', - ALL_SUGGESTIONS: 'ALL_SUGGESTIONS', -}; - -/** - * @enum - */ -export const UserRole = { - ANONYMOUS: 'ANONYMOUS', - AUTHOR: 'AUTHOR', - CHANGE_OWNER: 'CHANGE_OWNER', - REVIEWER: 'REVIEWER', - CC: 'CC', - REMOVED_REVIEWER: 'REMOVED_REVIEWER', - OTHER: 'OTHER', -}; - -export const BestSuggestionsLimit = 5; -export const AllSuggestionsLimit = 1000; - -/** - * Maintain the state of code-owners. - * Raises 'model-property-changed' event when a property is changed. - * The plugin shares the same model between all UI elements (if it is not, - * the plugin can't maintain showSuggestions state across different UI elements). - * UI elements use values from this model to display information - * and listens for the model-property-changed event. To do so, UI elements - * add CodeOwnersModelMixin, which is doing the listening and the translation - * from model-property-changed event to Polymer property-changed-event. The - * translation allows to use model properties in observables, bindings, - * computed properties, etc... - * The CodeOwnersModelLoader updates the model. - * - * It would be good to use RxJs Observable for implementing model properties. - * However, RxJs library is imported by Gerrit and there is no - * good way to reuse the same library in the plugin. - */ -export class CodeOwnersModel extends EventTarget { - constructor(change) { - super(); - this.change = change; - this.branchConfig = undefined; - this.status = undefined; - this.userRole = undefined; - this.isCodeOwnerEnabled = undefined; - this.areAllFilesApproved = undefined; - this.suggestionsByTypes = {}; - for (const suggestionType of Object.values(SuggestionsType)) { - this.suggestionsByTypes[suggestionType] = { - files: undefined, - state: SuggestionsState.NotLoaded, - loadProgress: undefined, - }; - } - this.selectedSuggestionsType = SuggestionsType.BEST_SUGGESTIONS; - this.showSuggestions = false; - this.pluginStatus = undefined; - } - - get selectedSuggestions() { - return this.suggestionsByTypes[this.selectedSuggestionsType]; - } - - setBranchConfig(config) { - if (this.branchConfig === config) return; - this.branchConfig = config; - this._firePropertyChanged('branchConfig'); - } - - setStatus(status) { - if (this.status === status) return; - this.status = status; - this._firePropertyChanged('status'); - } - - setUserRole(userRole) { - if (this.userRole === userRole) return; - this.userRole = userRole; - this._firePropertyChanged('userRole'); - } - - setIsCodeOwnerEnabled(enabled) { - if (this.isCodeOwnerEnabled === enabled) return; - this.isCodeOwnerEnabled = enabled; - this._firePropertyChanged('isCodeOwnerEnabled'); - } - - setAreAllFilesApproved(approved) { - if (this.areAllFilesApproved === approved) return; - this.areAllFilesApproved = approved; - this._firePropertyChanged('areAllFilesApproved'); - } - - setSuggestionsFiles(suggestionsType, files) { - const suggestions = this.suggestionsByTypes[suggestionsType]; - if (suggestions.files === files) return; - suggestions.files = files; - this._fireSuggestionsChanged(suggestionsType, 'files'); - } - - setSuggestionsState(suggestionsType, state) { - const suggestions = this.suggestionsByTypes[suggestionsType]; - if (suggestions.state === state) return; - suggestions.state = state; - this._fireSuggestionsChanged(suggestionsType, 'state'); - } - - setSuggestionsLoadProgress(suggestionsType, progress) { - const suggestions = this.suggestionsByTypes[suggestionsType]; - if (suggestions.loadProgress === progress) return; - suggestions.loadProgress = progress; - this._fireSuggestionsChanged(suggestionsType, 'loadProgress'); - } - - setSelectedSuggestionType(suggestionsType) { - if (this.selectedSuggestionsType === suggestionsType) return; - this.selectedSuggestionsType = suggestionsType; - this._firePropertyChanged('selectedSuggestionsType'); - this._firePropertyChanged('selectedSuggestions'); - } - - setShowSuggestions(show) { - if (this.showSuggestions === show) return; - this.showSuggestions = show; - this._firePropertyChanged('showSuggestions'); - } - - setPluginEnabled(enabled) { - this._setPluginStatus({state: enabled ? - PluginState.Enabled : PluginState.Disabled}); - } - - setServerConfigurationError(failedMessage) { - this._setPluginStatus({state: PluginState.ServerConfigurationError, - failedMessage}); - } - - setPluginFailed(failedMessage) { - this._setPluginStatus({state: PluginState.Failed, failedMessage}); - } - - _setPluginStatus(status) { - if (this._arePluginStatusesEqual(this.pluginStatus, status)) return; - this.pluginStatus = status; - this._firePropertyChanged('pluginStatus'); - } - - _arePluginStatusesEqual(status1, status2) { - if (status1 === undefined || status2 === undefined) { - return status1 === status2; - } - if (status1.state !== status2.state) return false; - return isPluginErrorState(status1.state)? - status1.failedMessage === status2.failedMessage : - true; - } - - _firePropertyChanged(propertyName) { - this.dispatchEvent(new CustomEvent('model-property-changed', { - detail: { - propertyName, - }, - })); - } - - _fireSuggestionsChanged(suggestionsType, propertyName) { - this._firePropertyChanged( - `suggestionsByTypes.${suggestionsType}.${propertyName}`); - if (suggestionsType === this.selectedSuggestionsType) { - this._firePropertyChanged(`selectedSuggestions.${propertyName}`); - } - } - - static getModel(change) { - if (!this.model || this.model.change !== change) { - this.model = new CodeOwnersModel(change); - } - return this.model; - } -}
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js deleted file mode 100644 index 3a5cbdc..0000000 --- a/ui/code-owners-service.js +++ /dev/null
@@ -1,312 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {SuggestionsType, BestSuggestionsLimit, AllSuggestionsLimit, UserRole} from './code-owners-model.js'; -import {OwnersProvider, OwnerStatus, FetchStatus} from './code-owners-fetcher.js'; -import {CodeOwnersApi, CodeOwnersCacheApi} from './code-owners-api.js'; - -/** - * Specifies status for a change. The same as ChangeStatus enum in gerrit - * - * @enum - */ -const ChangeStatus = { - ABANDONED: 'ABANDONED', - MERGED: 'MERGED', - NEW: 'NEW', -}; - -/** - * Service for the data layer used in the plugin UI. - */ -export class CodeOwnerService { - constructor(restApi, change, options = {}) { - this.restApi = restApi; - this.change = change; - const codeOwnersApi = new CodeOwnersApi(restApi); - this.codeOwnersCacheApi = new CodeOwnersCacheApi(codeOwnersApi, change); - - const providerOptions = { - maxConcurrentRequests: options.maxConcurrentRequests || 10, - }; - this.ownersProviders = { - [SuggestionsType.BEST_SUGGESTIONS]: new OwnersProvider(restApi, change, { - ...providerOptions, - ownersLimit: BestSuggestionsLimit, - }), - [SuggestionsType.ALL_SUGGESTIONS]: new OwnersProvider(restApi, change, { - ...providerOptions, - ownersLimit: AllSuggestionsLimit, - }), - }; - } - - /** - * Prefetch data - */ - async prefetch() { - try { - await Promise.all([ - this.codeOwnersCacheApi.getAccount(), - this.getStatus(), - ]); - } catch { - // Ignore any errors during prefetch. - // The same call from a different place throws the same exception - // again. The CodeOwnerService is not responsible for error processing. - } - } - - /** - * Returns the role of the current user. The returned value reflects the - * role of the user at the time when the change is loaded. - * For example, if a user removes themselves as a reviewer, the returned - * role 'REVIEWER' remains unchanged until the change view is reloaded. - */ - async getLoggedInUserInitialRole() { - const account = await this.codeOwnersCacheApi.getAccount(); - if (!account) { - return UserRole.ANONYMOUS; - } - const change = this.change; - if ( - change.revisions && - change.current_revision && - change.revisions[change.current_revision] - ) { - const commit = change.revisions[change.current_revision].commit; - if ( - commit && - commit.author && - account.email && - commit.author.email === account.email - ) { - return UserRole.AUTHOR; - } - } - if (change.owner._account_id === account._account_id) { - return UserRole.CHANGE_OWNER; - } - if (change.reviewers) { - if (this._accountInReviewers(change.reviewers.REVIEWER, account)) { - return UserRole.REVIEWER; - } else if (this._accountInReviewers(change.reviewers.CC, account)) { - return UserRole.CC; - } else if (this._accountInReviewers(change.reviewers.REMOVED, account)) { - return UserRole.REMOVED_REVIEWER; - } - } - return UserRole.OTHER; - } - - _accountInReviewers(reviewers, account) { - if (!reviewers) { - return false; - } - return reviewers.some(reviewer => - reviewer._account_id === account._account_id); - } - - async getStatus() { - const status = await this._getStatus(); - if (status.enabled && this._isOnOlderPatchset(status.patchsetNumber)) { - // status is returned for an older patchset. Abort, re-init and refetch - // new status - it is expected, that after several retry a status - // for the newest patchset is returned - this.reset(); - this.prefetch(); - return await this.getStatus(); - } - return status; - } - - async _getStatus() { - const enabled = await this.isCodeOwnerEnabled(); - if (!enabled) { - return { - patchsetNumber: 0, - enabled: false, - codeOwnerStatusMap: new Map(), - rawStatuses: [], - newerPatchsetUploaded: false, - }; - } - - const ownerStatus = await this.codeOwnersCacheApi.listOwnerStatus(); - - return { - enabled: true, - patchsetNumber: ownerStatus.patch_set_number, - codeOwnerStatusMap: this._formatStatuses( - ownerStatus.file_code_owner_statuses - ), - rawStatuses: ownerStatus.file_code_owner_statuses, - newerPatchsetUploaded: - this._isOnNewerPatchset(ownerStatus.patch_set_number), - }; - } - - async areAllFilesApproved() { - const {rawStatuses} = await this.getStatus(); - return !rawStatuses.some(status => { - const oldPathStatus = status.old_path_status; - const newPathStatus = status.new_path_status; - // For deleted files, no new_path_status exists - return (newPathStatus && newPathStatus.status !== OwnerStatus.APPROVED) || - (oldPathStatus && oldPathStatus.status !== OwnerStatus.APPROVED); - }); - } - - /** - * Gets owner suggestions. - * - * @returns {{ - * finished?: boolean, - * progress?: string, - * suggestions: Array<{ - * groupName: { - * name: string, - * prefix: string - * }, - * error?: Error, - * owners?: Array, - * files: Array, - * }> - * }} - */ - async getSuggestedOwners(suggestionsType) { - const {codeOwnerStatusMap} = await this.getStatus(); - const ownersProvider = this.ownersProviders[suggestionsType]; - - await ownersProvider.fetchSuggestedOwners(codeOwnerStatusMap); - - return { - finished: ownersProvider.getStatus() === FetchStatus.FINISHED, - status: ownersProvider.getStatus(), - progress: ownersProvider.getProgressString(), - files: this._getFilesWithStatuses(codeOwnerStatusMap, - ownersProvider.getFiles()), - }; - } - - async getSuggestedOwnersProgress(suggestionsType) { - const {codeOwnerStatusMap} = await this.getStatus(); - const ownersProvider = this.ownersProviders[suggestionsType]; - return { - finished: ownersProvider.getStatus() === FetchStatus.FINISHED, - status: ownersProvider.getStatus(), - progress: ownersProvider.getProgressString(), - files: this._getFilesWithStatuses(codeOwnerStatusMap, - ownersProvider.getFiles()), - }; - } - - pauseSuggestedOwnersLoading(suggestionsType) { - this.ownersProviders[suggestionsType].pause(); - } - - resumeSuggestedOwnersLoading(suggestionsType) { - this.ownersProviders[suggestionsType].resume(); - } - - _formatStatuses(statuses) { - // convert the array of statuses to map between file path -> status - return statuses.reduce((prev, cur) => { - const newPathStatus = cur.new_path_status; - const oldPathStatus = cur.old_path_status; - if (oldPathStatus) { - prev.set(oldPathStatus.path, { - changeType: cur.change_type, - status: oldPathStatus.status, - newPath: newPathStatus ? newPathStatus.path : null, - }); - } - if (newPathStatus) { - prev.set(newPathStatus.path, { - changeType: cur.change_type, - status: newPathStatus.status, - oldPath: oldPathStatus ? oldPathStatus.path : null, - }); - } - return prev; - }, new Map()); - } - - _computeFileStatus(fileStatusMap, path) { - // empty for modified files and old-name files - // Show `Renamed` for renamed file - const status = fileStatusMap.get(path); - if (status.oldPath) { - return 'Renamed'; - } - return; - } - - _getFilesWithStatuses(codeOwnerStatusMap, files) { - return files.map(file => { - return { - path: file.path, - info: file.info, - status: this._computeFileStatus(codeOwnerStatusMap, file.path), - }; - }); - } - - _isOnNewerPatchset(patchsetId) { - const latestRevision = this.change.revisions[this.change.current_revision]; - return patchsetId > latestRevision._number; - } - - _isOnOlderPatchset(patchsetId) { - const latestRevision = this.change.revisions[this.change.current_revision]; - return patchsetId < latestRevision._number; - } - - reset() { - for (const provider of Object.values(this.ownersProviders)) { - provider.reset(); - } - const codeOwnersApi = new CodeOwnersApi(this.restApi); - this.codeOwnersCacheApi = - new CodeOwnersCacheApi(codeOwnersApi, this.change); - } - - async getBranchConfig() { - return this.codeOwnersCacheApi.getBranchConfig(); - } - - async isCodeOwnerEnabled() { - if (this.change.status === ChangeStatus.ABANDONED || - this.change.status === ChangeStatus.MERGED) { - return false; - } - const config = await this.codeOwnersCacheApi.getBranchConfig(); - return config && !config.disabled; - } - - static getOwnerService(restApi, change) { - if (!this.ownerService || this.ownerService.change !== change) { - this.ownerService = new CodeOwnerService(restApi, change, { - // Chrome has a limit of 6 connections per host name, and a max of 10 connections. - maxConcurrentRequests: 6, - }); - this.ownerService.prefetch(); - } - return this.ownerService; - } -} -
diff --git a/ui/code-owners-service_test.html b/ui/code-owners-service_test.html deleted file mode 100644 index 884b97f..0000000 --- a/ui/code-owners-service_test.html +++ /dev/null
@@ -1,245 +0,0 @@ -<!DOCTYPE html> -<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> - -<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script> -<script src="../bower_components/web-component-tester/browser.js"></script> - -<title>code owners service test</title> - -<script type="module"> - import '../test/common-test-setup.js'; - import {CodeOwnerService} from './code-owners-service.js'; - - suite('code owners service tests', () => { - let sandbox; - let codeOwnersService; - const fakeRestApi = { - get() { - return Promise.resolve({}); - }, - getLoggedIn() { - return Promise.resolve(undefined); - }, - }; - const getApiStub = sinon.stub(fakeRestApi, 'get'); - getApiStub.returns(Promise.resolve({})); - const fakeStatus = { - // fake data with fake files - patch_set_number: 1, - file_code_owner_statuses: [ - { - new_path_status: { - path: 'a.js', - status: 'INSUFFICIENT_REVIEWERS', - }, - }, - { - new_path_status: { - path: 'b.js', - status: 'INSUFFICIENT_REVIEWERS', - }, - }, - { - new_path_status: { - path: 'c.ts', - status: 'INSUFFICIENT_REVIEWERS', - }, - }, - { - change_type: 'RENAMED', - old_path_status: { - path: 'd.ts', - status: 'INSUFFICIENT_REVIEWERS', - }, - new_path_status: { - path: 'd_edit.ts', - status: 'INSUFFICIENT_REVIEWERS', - }, - }, - { - new_path_status: { - path: 'e.ts', - status: 'PENDING', - }, - }, - { - new_path_status: { - path: 'f.ts', - status: 'PENDING', - }, - }, - { - new_path_status: { - path: 'g.ts', - status: 'PENDING', - }, - }, - ], - }; - const fakeChange = { - project: 'test', - branch: 'main', - _number: '123', - revisions: { - a: { - _number: 1, - }, - }, - current_revision: 'a', - }; - - setup(() => { - sandbox = sinon.sandbox.create(); - getApiStub.reset(); - }); - - teardown(() => { sandbox.restore(); }); - - suite('basic api request tests', () => { - setup(done => { - getApiStub.withArgs(`/changes/${fakeChange._number}/code_owners.status`) - .returns(Promise.resolve(fakeStatus)); - codeOwnersService = CodeOwnerService.getOwnerService(fakeRestApi, - {...fakeChange}); - flush(done); - }); - - test('getOwnerService - same change returns the same instance', () => { - assert.equal( - CodeOwnerService.getOwnerService(fakeRestApi, fakeChange), - CodeOwnerService.getOwnerService(fakeRestApi, fakeChange) - ); - }); - - test('return a new instance when change changed', () => { - assert.notEqual( - CodeOwnerService.getOwnerService(fakeRestApi, {...fakeChange}), - CodeOwnerService.getOwnerService(fakeRestApi, {...fakeChange}) - ); - }); - - test('should fetch status after init', () => { - assert.isTrue(getApiStub.calledOnce); - assert.equal(getApiStub.lastCall.args[0], - `/changes/${fakeChange._number}/code_owners.status`); - }); - - test('getSuggestion should kickoff the fetch', done => { - codeOwnersService.getSuggestedOwners().then(r => { - // 6 requests for getting file owners + 1 status call - assert.equal(getApiStub.callCount, 7); - assert.equal(r.finished, false); - flush(() => { - assert.equal(getApiStub.callCount, 9); - done(); - }); - }); - }); - - test('approved status calculation', done => { - codeOwnersService.areAllFilesApproved().then(approved => { - assert.equal(approved, false); - done(); - }); - }); - }); - - suite('all approved case', () => { - setup(done => { - getApiStub.withArgs(`/changes/${fakeChange._number}/code_owners.status`) - .returns(Promise.resolve({ - // fake data with fake files - patch_set_number: 1, - file_code_owner_statuses: [ - { - new_path_status: { - path: 'a.js', - status: 'APPROVED', - }, - }, - { - new_path_status: { - path: 'b.js', - status: 'APPROVED', - }, - }, - { - old_path_status: { - path: 'd.js', - status: 'APPROVED', - }, - change_type: 'DELETED', - }, - ], - })); - - codeOwnersService = CodeOwnerService.getOwnerService(fakeRestApi, - {...fakeChange}); - flush(done); - }); - - test('getSuggestion and finished in first batch', done => { - codeOwnersService.getSuggestedOwners().then(r => { - // 1 status call - assert.equal(getApiStub.callCount, 1); - // initially false as getSuggestedOwners is synchrously - assert.equal(r.finished, false); - codeOwnersService.getSuggestedOwners().then(nR => { - assert.equal(nR.finished, true); - done(); - }); - }); - }); - - test('approved status calculation', done => { - codeOwnersService.areAllFilesApproved().then(approved => { - assert.equal(approved, true); - done(); - }); - }); - }); - - suite('abort', () => { - setup(done => { - getApiStub.withArgs(`/changes/${fakeChange._number}/code_owners.status`) - .returns(Promise.resolve(fakeStatus)); - - codeOwnersService = CodeOwnerService.getOwnerService(fakeRestApi, - {...fakeChange}); - flush(done); - }); - - test('abort should abort pending requests', done => { - codeOwnersService.getSuggestedOwners().then(r => { - // 6 requests for getting file owners + 1 status call - assert.equal(getApiStub.callCount, 7); - assert.equal(r.finished, false); - codeOwnersService.abort(); - flush(() => { - // should still at 7 since aborted - assert.equal(getApiStub.callCount, 7); - done(); - }); - }); - }); - - test('getSuggestedOwners should restart if aborted', done => { - codeOwnersService.abort(); - codeOwnersService.getSuggestedOwners().then(r => { - // 6 requests for getting file owners + 1 status call - assert.equal(getApiStub.callCount, 7); - assert.equal(r.finished, false); - codeOwnersService.abort(); - flush(() => { - codeOwnersService.getSuggestedOwners().then(r => { - // should at 13 since restarted - assert.equal(getApiStub.callCount, 13); - done(); - }); - }); - }); - }); - }); - }); -</script>
diff --git a/ui/owner-requirement.js b/ui/owner-requirement.js deleted file mode 100644 index c15fcf3..0000000 --- a/ui/owner-requirement.js +++ /dev/null
@@ -1,275 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {OwnerStatus} from './code-owners-fetcher.js'; -import {CodeOwnersModelMixin} from './code-owners-model-mixin.js'; -import {showPluginFailedMessage} from './code-owners-banner.js'; -import {isPluginErrorState, UserRole} from './code-owners-model.js'; - -/** - * Owner requirement control for `submit-requirement-item-code-owners` endpoint. - * - * This will show the status and suggest owners button next to - * the code-owners submit requirement. - */ -export class OwnerRequirementValue extends - CodeOwnersModelMixin(Polymer.Element) { - static get is() { - return 'owner-requirement-value'; - } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host { - --gr-button: { - padding: 0px; - } - } - p.loading { - display: flex; - align-content: center; - align-items: center; - justify-content: center; - } - .loadingSpin { - display: inline-block; - margin-right: var(--spacing-m); - width: 18px; - height: 18px; - } - gr-button { - padding-left: var(--spacing-m); - } - a { - text-decoration: none; - } - </style> - <p class="loading" hidden="[[!_isLoading]]"> - <span class="loadingSpin"></span> - Loading status ... - </p> - <template is="dom-if" if="[[!_isLoading]]"> - <template is="dom-if" if="[[!_pluginFailed(model.pluginStatus)]]"> - <template is="dom-if" if="[[!model.branchConfig.no_code_owners_defined]]"> - <template is="dom-if" if="[[!_newerPatchsetUploaded]]"> - <span>[[_computeStatusText(_statusCount, _isOverriden)]]</span> - <template is="dom-if" if="[[_overrideInfoUrl]]"> - <a on-click="_reportDocClick" href="[[_overrideInfoUrl]]" - target="_blank"> - <iron-icon icon="gr-icons:help-outline" - title="Documentation for overriding code owners"></iron-icon> - </a> - </template> - <template is="dom-if" if="[[_isSignedInUser]]"> - <gr-button link on-click="_openReplyDialog"> - [[_getSuggestOwnersText(_statusCount)]] - </gr-button> - </template> - </template> - <template is="dom-if" if="[[_newerPatchsetUploaded]]"> - <span>A newer patch set has been uploaded.</span> - </template> - </template> - <template is="dom-if" if="[[model.branchConfig.no_code_owners_defined]]"> - <span>No code-owners file</span> - <a href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/user-guide.md#how-to-submit-changes-with-files-that-have-no-code-owners" target="_blank"> - <iron-icon icon="gr-icons:help-outline" - title="Documentation about submitting changes with files that have no code owners?"></iron-icon> - </a> - </template> - </template> - <template is="dom-if" if="[[_pluginFailed(model.pluginStatus)]]"> - <span>Code-owners plugin has failed</span> - <gr-button link on-click="_showFailDetails"> - Details - </gr-button> - </template> - </template> - `; - } - - static get properties() { - return { - _statusCount: Object, - _newerPatchsetUploaded: Boolean, - _isLoading: { - type: Boolean, - computed: '_computeIsLoading(model.branchConfig, model.status, ' + - 'model.userRole, model.pluginStatus)', - }, - _isOverriden: { - type: Boolean, - computed: '_computeIsOverriden(change, model.branchConfig)', - }, - _overrideInfoUrl: { - type: String, - computed: '_computeOverrideInfoUrl(model.branchConfig)', - }, - _isSignedInUser: { - type: Boolean, - computed: '_computeIsSignedInUser(model.userRole)' - } - }; - } - - static get observers() { - return [ - '_onStatusChanged(model.status, model.userRole)', - ]; - } - - loadPropertiesAfterModelChanged() { - super.loadPropertiesAfterModelChanged(); - this.reporting.reportLifeCycle('owners-submit-requirement-summary-start'); - this.modelLoader.loadBranchConfig(); - this.modelLoader.loadStatus(); - this.modelLoader.loadUserRole(); - } - - _computeIsLoading(branchConfig, status, userRole, pluginStatus) { - if (this._pluginFailed(pluginStatus)) { - return false; - } - return !branchConfig || !status || !userRole; - } - - _computeIsSignedInUser(userRole) { - return userRole && userRole !== UserRole.ANONYMOUS; - } - - _pluginFailed(pluginStatus) { - return pluginStatus && isPluginErrorState(pluginStatus.state); - } - - _onStatusChanged(status, userRole) { - if (!status || !userRole) { - this._statusCount = undefined; - this._newerPatchsetUploaded = undefined; - return; - } - const rawStatuses = status.rawStatuses; - this._statusCount = this._getStatusCount(rawStatuses); - this._newerPatchsetUploaded = status.newerPatchsetUploaded; - this.reporting.reportLifeCycle('owners-submit-requirement-summary-shown', - {...this._statusCount, user_role: userRole}); - } - - _computeOverrideInfoUrl(branchConfig) { - if (!branchConfig) { - return ''; - } - return branchConfig.general && branchConfig.general.override_info_url ? - branchConfig.general.override_info_url : ''; - } - - _computeIsOverriden(change, branchConfig) { - if (!change || !branchConfig || !branchConfig['override_approval']) { - // no override labels configured - return false; - } - - for (const requiredApprovalInfo of branchConfig['override_approval']) { - const overridenLabel = requiredApprovalInfo.label; - const overridenValue = Number(requiredApprovalInfo.value); - if (isNaN(overridenValue)) continue; - - if (this.change.labels[overridenLabel]) { - const votes = change.labels[overridenLabel].all || []; - if (votes.find(v => Number(v.value) >= overridenValue)) { - return true; - } - } - } - - // otherwise always reset it to false - return false; - } - - _getSuggestOwnersText(statusCount) { - return statusCount && statusCount.missing === 0 ? - 'Add owners' : 'Suggest owners'; - } - - _getStatusCount(rawStatuses) { - return rawStatuses - .reduce((prev, cur) => { - const oldPathStatus = cur.old_path_status; - const newPathStatus = cur.new_path_status; - if (newPathStatus && this._isMissing(newPathStatus.status)) { - prev.missing ++; - } else if (newPathStatus && this._isPending(newPathStatus.status)) { - prev.pending ++; - } else if (oldPathStatus) { - // check oldPath if newPath approved or the file is deleted - if (this._isMissing(oldPathStatus.status)) { - prev.missing ++; - } else if (this._isPending(oldPathStatus.status)) { - prev.pending ++; - } - } else { - prev.approved ++; - } - return prev; - }, {missing: 0, pending: 0, approved: 0}); - } - - _computeStatusText(statusCount, isOverriden) { - if (statusCount === undefined || isOverriden === undefined) return ''; - const statusText = []; - if (statusCount.missing) { - statusText.push(`${statusCount.missing} missing`); - } - - if (statusCount.pending) { - statusText.push(`${statusCount.pending} pending`); - } - - if (!statusText.length) { - statusText.push(isOverriden ? 'Approved (Owners-Override)' : 'Approved'); - } - - return statusText.join(', '); - } - - _isMissing(status) { - return status === OwnerStatus.INSUFFICIENT_REVIEWERS; - } - - _isPending(status) { - return status === OwnerStatus.PENDING; - } - - _openReplyDialog() { - this.model.setShowSuggestions(true); - this.dispatchEvent( - new CustomEvent('open-reply-dialog', { - detail: {}, - composed: true, - bubbles: true, - }) - ); - this.reporting.reportInteraction('suggest-owners-from-submit-requirement', - {user_role: this.model.userRole}); - } - - _showFailDetails() { - showPluginFailedMessage(this, this.model.pluginStatus); - } -} - -customElements.define(OwnerRequirementValue.is, OwnerRequirementValue);
diff --git a/ui/owner-status-column.js b/ui/owner-status-column.js deleted file mode 100644 index c9c9d91..0000000 --- a/ui/owner-status-column.js +++ /dev/null
@@ -1,250 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {OwnerStatus} from './code-owners-fetcher.js'; -import {CodeOwnersModelMixin} from './code-owners-model-mixin.js'; - -const MAGIC_FILES = ['/COMMIT_MSG', '/MERGE_LIST', '/PATCHSET_LEVEL']; -const STATUS_CODE = { - PENDING: 'pending', - PENDING_OLD_PATH: 'pending-old-path', - MISSING: 'missing', - MISSING_OLD_PATH: 'missing-old-path', - APPROVED: 'approved', - ERROR: 'error', - ERROR_OLD_PATH: 'error-old-path', -}; -const STATUS_PRIORITY_ORDER = [ - STATUS_CODE.ERROR, - STATUS_CODE.ERROR_OLD_PATH, - STATUS_CODE.MISSING, - STATUS_CODE.PENDING, - STATUS_CODE.MISSING_OLD_PATH, - STATUS_CODE.PENDING_OLD_PATH, - STATUS_CODE.APPROVED, -]; -const STATUS_ICON = { - [STATUS_CODE.PENDING]: 'gr-icons:schedule', - [STATUS_CODE.MISSING]: 'gr-icons:close', - [STATUS_CODE.PENDING_OLD_PATH]: 'gr-icons:schedule', - [STATUS_CODE.MISSING_OLD_PATH]: 'gr-icons:close', - [STATUS_CODE.APPROVED]: 'gr-icons:check', - [STATUS_CODE.ERROR]: 'gr-icons:info-outline', - [STATUS_CODE.ERROR]: 'gr-icons:info-outline', -}; -const STATUS_TOOLTIP = { - [STATUS_CODE.PENDING]: 'Pending code owner approval', - [STATUS_CODE.MISSING]: 'Missing code owner approval', - [STATUS_CODE.PENDING_OLD_PATH]: - 'Pending code owner approval on pre-renamed file', - [STATUS_CODE.MISSING_OLD_PATH]: - 'Missing code owner approval on pre-renamed file', - [STATUS_CODE.APPROVED]: 'Approved by code owner', - [STATUS_CODE.ERROR]: 'Failed to fetch code owner status', - [STATUS_CODE.ERROR_OLD_PATH]: 'Failed to fetch code owner status', -}; - -class BaseEl extends CodeOwnersModelMixin(Polymer.Element) { - static get properties() { - return { - patchRange: Object, - - hidden: { - type: Boolean, - reflectToAttribute: true, - computed: 'computeHidden(change, patchRange, ' + - 'model.status.newerPatchsetUploaded)', - }, - }; - } - - computeHidden(change, patchRange, newerPatchsetUploaded) { - if ([change, patchRange, newerPatchsetUploaded].includes(undefined)) { - return true; - } - // if code-owners is not a submit requirement, don't show status column - if (change.requirements && - !change.requirements.find(r => r.type === 'code-owners')) { - return true; - } - - if (newerPatchsetUploaded) return true; - - const latestPatchset = change.revisions[change.current_revision]; - // Note: in some special cases, patchNum is undefined on latest patchset - // like after publishing the edit, still show for them - // TODO: this should be fixed in Gerrit - if (patchRange.patchNum === undefined) return false; - // only show if its latest patchset - if (`${patchRange.patchNum}` !== `${latestPatchset._number}`) return true; - return false; - } -} - -/** - * Column header element for owner status. - */ -export class OwnerStatusColumnHeader extends BaseEl { - static get is() { - return 'owner-status-column-header'; - } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host(:not([hidden])) { - display: block; - padding-right: var(--spacing-m); - width: 3em; - } - </style> - <div></div> - `; - } -} - -customElements.define(OwnerStatusColumnHeader.is, OwnerStatusColumnHeader); - -/** - * Row content element for owner status. - */ -export class OwnerStatusColumnContent extends BaseEl { - static get is() { - return 'owner-status-column-content'; - } - - static get properties() { - return { - path: String, - oldPath: String, - cleanlyMergedPaths: Array, - cleanlyMergedOldPaths: Array, - ownerService: Object, - statusIcon: { - type: String, - computed: '_computeIcon(status)', - }, - statusInfo: { - type: String, - computed: '_computeTooltip(status)', - }, - status: { - type: String, - reflectToAttribute: true, - }, - }; - } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host(:not([hidden])) { - display:block; - padding-right: var(--spacing-m); - width: 3em; - text-align: center; - } - iron-icon { - padding: var(--spacing-xs) 0px; - } - :host([status=approved]) iron-icon { - color: var(--positive-green-text-color); - } - :host([status=pending]) iron-icon { - color: #ffa62f; - } - :host([status=missing]) iron-icon, - :host([status=error]) iron-icon { - color: var(--negative-red-text-color); - } - </style> - <gr-tooltip-content title="[[statusInfo]]" has-tooltip> - <iron-icon icon="[[statusIcon]]"></iron-icon> - </gr-tooltip-content> - `; - } - - static get observers() { - return [ - 'computeStatusIcon(model.status,path, oldPath, cleanlyMergedPaths, ' + - 'cleanlyMergedOldPaths)', - ]; - } - - loadPropertiesAfterModelChanged() { - super.loadPropertiesAfterModelChanged(); - this.modelLoader.loadStatus(); - } - - computeStatusIcon( - modelStatus, - path, - oldPath, - cleanlyMergedPaths, - cleanlyMergedOldPaths - ) { - if ( - modelStatus === undefined || - ([path, oldPath].includes(undefined) && cleanlyMergedPaths === undefined) - ) { - return; - } - const codeOwnerStatusMap = modelStatus.codeOwnerStatusMap; - const paths = path === undefined ? cleanlyMergedPaths : [path]; - const oldPaths = oldPath === undefined ? cleanlyMergedOldPaths : [oldPath]; - - const statuses = paths - .filter(path => !MAGIC_FILES.includes(path)) - .map(path => this._computeStatus(codeOwnerStatusMap.get(path))); - // oldPath may contain null, so filter that as well. - const oldStatuses = oldPaths - .filter(path => !MAGIC_FILES.includes(path) && !!path) - .map(path => this._computeStatus(codeOwnerStatusMap.get(path), true)); - const allStatuses = statuses.concat(oldStatuses); - if (allStatuses.length === 0) { - return; - } - this.status = allStatuses.reduce((a, b) => { - return STATUS_PRIORITY_ORDER.indexOf(a) < - STATUS_PRIORITY_ORDER.indexOf(b) - ? a - : b; - }); - } - - _computeIcon(status) { - return STATUS_ICON[status]; - } - - _computeTooltip(status) { - return STATUS_TOOLTIP[status]; - } - - _computeStatus(statusItem, oldPath = false) { - if (statusItem === undefined) { - return oldPath ? STATUS_CODE.ERROR_OLD_PATH : STATUS_CODE.ERROR; - } else if (statusItem.status === OwnerStatus.INSUFFICIENT_REVIEWERS) { - return oldPath ? STATUS_CODE.MISSING_OLD_PATH : STATUS_CODE.MISSING; - } else if (statusItem.status === OwnerStatus.PENDING) { - return oldPath ? STATUS_CODE.PENDING_OLD_PATH : STATUS_CODE.PENDING; - } else { - return STATUS_CODE.APPROVED; - } - } -} - -customElements.define(OwnerStatusColumnContent.is, OwnerStatusColumnContent);
diff --git a/ui/plugin.js b/ui/plugin.js deleted file mode 100644 index 2539908..0000000 --- a/ui/plugin.js +++ /dev/null
@@ -1,108 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {SuggestOwners} from './suggest-owners.js'; -import {OwnerStatusColumnContent, OwnerStatusColumnHeader} from './owner-status-column.js'; -import {OwnerRequirementValue} from './owner-requirement.js'; -import {SuggestOwnersTrigger} from './suggest-owners-trigger.js'; -import {CodeOwnersBanner, CodeOwnersPluginStatusNotifier} from './code-owners-banner.js'; - -// A temporary property for interop with find-owners plugin. -// It should be set as earlier as possible, so find-owners plugin can check -// it in Gerrit.install callback. -window.__gerrit_code_owners_plugin = { - state: { - // See CodeOwnersPluginStatusNotifier._stateForFindOwnersPlugin for - // a list of possible values. - branchState: 'LOADING', - }, - stateChanged: new EventTarget(), -}; - -Gerrit.install(plugin => { - const restApi = plugin.restApi(); - const reporting = plugin.reporting(); - - plugin.registerCustomComponent('banner', CodeOwnersBanner.is); - - const stateForFindOwnerPluginChanged = evt => { - window.__gerrit_code_owners_plugin.state = evt.detail.value; - window.__gerrit_code_owners_plugin.stateChanged - .dispatchEvent(new CustomEvent('state-changed')); - }; - - plugin.registerCustomComponent( - 'change-view-integration', CodeOwnersPluginStatusNotifier.is) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - view.addEventListener('_state-for-find-owners-plugin-changed', - stateForFindOwnerPluginChanged); - }) - .onDetached(view => { - view.removeEventListener('_state-for-find-owners-plugin-changed', - stateForFindOwnerPluginChanged); - }); - - // owner status column / rows for file list - plugin.registerDynamicCustomComponent( - 'change-view-file-list-header-prepend', OwnerStatusColumnHeader.is) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - }); - plugin.registerDynamicCustomComponent( - 'change-view-file-list-content-prepend', OwnerStatusColumnContent.is) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - }); - - // old submit requirement value for owner's requirement - plugin.registerCustomComponent( - 'submit-requirement-item-code-owners', - OwnerRequirementValue.is, {slot: 'value'} - ) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - }); - - // new submit requirement value for owner's requirement - plugin.registerCustomComponent( - 'submit-requirement-codeowners', - OwnerRequirementValue.is, {replace: true} - ) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - }); - - // suggest owners for reply dialog - plugin.registerCustomComponent( - 'reply-reviewers', SuggestOwnersTrigger.is, {slot: 'right'}) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - }); - plugin.registerCustomComponent( - 'reply-reviewers', SuggestOwners.is, {slot: 'below'}) - .onAttached(view => { - view.restApi = restApi; - view.reporting = reporting; - }); -});
diff --git a/ui/suggest-owners-trigger.js b/ui/suggest-owners-trigger.js deleted file mode 100644 index 518210d..0000000 --- a/ui/suggest-owners-trigger.js +++ /dev/null
@@ -1,99 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {CodeOwnersModelMixin} from './code-owners-model-mixin.js'; -import {PluginState} from './code-owners-model.js'; - -export class SuggestOwnersTrigger extends - CodeOwnersModelMixin(Polymer.Element) { - static get is() { - return 'suggest-owners-trigger'; - } - - static get properties() { - return { - hidden: { - type: Boolean, - computed: '_computeHidden(model.pluginStatus, model.branchConfig)', - reflectToAttribute: true, - }, - }; - } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host { - display: flex; - } - a { - text-decoration: none; - } - gr-button { - --gr-button-padding: var(--spacing-xs) var(--spacing-s); - } - </style> - <gr-button - link - on-click="toggleControlContent" - has-tooltip - title="Suggest owners for your change" - > - [[computeButtonText(model.showSuggestions)]] - </gr-button> - <span> - <a on-click="_reportDocClick" href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/how-to-use.md" target="_blank"> - <iron-icon icon="gr-icons:help-outline" title="read documentation"></iron-icon> - </a> - </span> - `; - } - - loadPropertiesAfterModelChanged() { - super.loadPropertiesAfterModelChanged(); - this.modelLoader.loadUserRole(); - this.modelLoader.loadPluginStatus(); - this.modelLoader.loadAreAllFilesApproved(); - this.modelLoader.loadBranchConfig(); - } - - _computeHidden(pluginStatus, branchConfig) { - if (pluginStatus === undefined || branchConfig === undefined) { - return true; - } - return !!branchConfig.no_code_owners_defined || - pluginStatus.state !== PluginState.Enabled; - } - - toggleControlContent() { - this.model.setShowSuggestions(!this.model.showSuggestions); - this.reporting.reportInteraction('toggle-suggest-owners', { - expanded: this.expanded, - user_role: this.model.userRole ? - this.model.userRole : 'UNKNOWN', - }); - } - - computeButtonText(expanded) { - return expanded ? 'Hide owners' : 'Suggest owners'; - } - - _reportDocClick() { - this.reporting.reportInteraction('code-owners-doc-click'); - } -} - -customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
diff --git a/ui/suggest-owners-util.js b/ui/suggest-owners-util.js deleted file mode 100644 index fb18a0f..0000000 --- a/ui/suggest-owners-util.js +++ /dev/null
@@ -1,123 +0,0 @@ -/** - * @license - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import {BestSuggestionsLimit} from './code-owners-model.js'; - -/** - * For each file calculates owners to display and group all files by those - * owners. The function creates "fake" groups when one or more - * reviewers are included in all owners of a file, but none of reviewers is - * included in best reviewers for the file. - * - * Such situations are possible when user turns on "Show all owners", selects - * one of newly displayed owners and then turns off "Show all owners". Without - * "fake" groups a user can see inconsistent state in dialog. - */ - -export function getDisplayOwnersGroups(files, allOwnersByPathMap, - reviewersIdSet, allowAllOwnersSubstition) { - const getDisplayOwnersFunc = - !allowAllOwnersSubstition || allOwnersByPathMap.size === 0 || - reviewersIdSet.size === 0 ? - file => file.info.owners : - file => getDisplayOwners(file, allOwnersByPathMap, reviewersIdSet); - return groupFilesByOwners(files, getDisplayOwnersFunc); -} - -function getDisplayOwners(file, allOwnersByPathMap, reviewersIdSet) { - const ownerSelected = owner => reviewersIdSet.has(owner.account._account_id); - const defaultOwners = file.info.owners; - if (!defaultOwners || - defaultOwners.owned_by_all_users || - defaultOwners.code_owners.some(ownerSelected)) { - return defaultOwners; - } - const allOwners = allOwnersByPathMap.get(file.path); - if (!allOwners) return defaultOwners; - if (allOwners.owned_by_all_users) return allOwners; - const selectedAllOwners = allOwners.code_owners.filter(ownerSelected); - if (selectedAllOwners.length === 0) return defaultOwners; - return { - code_owners: selectedAllOwners.slice(0, BestSuggestionsLimit), - }; -} - -function groupFilesByOwners(files, getDisplayOwnersFunc) { - // Note: for renamed or moved files, they will have two entries in the map - // we will treat them as two entries when group as well - const ownersFilesMap = new Map(); - const failedToFetchFiles = new Set(); - for (const file of files) { - // for files failed to fetch, add them to the special group - if (file.info.error) { - failedToFetchFiles.add(file); - continue; - } - - // do not include files still in fetching - if (!file.info.owners) { - continue; - } - const displayOwners = getDisplayOwnersFunc(file); - - const ownersKey = getOwnersGroupKey(displayOwners); - ownersFilesMap.set( - ownersKey, - ownersFilesMap.get(ownersKey) || {files: [], owners: displayOwners} - ); - ownersFilesMap.get(ownersKey).files.push(file); - } - const groupedItems = []; - for (const ownersKey of ownersFilesMap.keys()) { - const groupName = getGroupName(ownersFilesMap.get(ownersKey).files); - groupedItems.push({ - groupName, - files: ownersFilesMap.get(ownersKey).files, - owners: ownersFilesMap.get(ownersKey).owners, - }); - } - - if (failedToFetchFiles.size > 0) { - const failedFiles = [...failedToFetchFiles]; - groupedItems.push({ - groupName: getGroupName(failedFiles), - files: failedFiles, - error: new Error( - 'Failed to fetch code owner info. Try to refresh the page.'), - }); - } - return groupedItems; -} - -function getOwnersGroupKey(owners) { - if (owners.owned_by_all_users) { - return '__owned_by_all_users__'; - } - const code_owners = owners.code_owners; - return code_owners - .map(owner => owner.account._account_id) - .sort() - .join(','); -} - -function getGroupName(files) { - const fileName = files[0].path.split('/').pop(); - return { - name: fileName, - prefix: files.length > 1 ? `+ ${files.length - 1} more` : '', - }; -}
diff --git a/ui/suggest-owners.js b/ui/suggest-owners.js deleted file mode 100644 index f07003f..0000000 --- a/ui/suggest-owners.js +++ /dev/null
@@ -1,683 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {CodeOwnersModelMixin} from './code-owners-model-mixin.js'; -import { - SuggestionsState, - SuggestionsType, -} from './code-owners-model.js'; -import {getDisplayOwnersGroups} from './suggest-owners-util.js'; - -const SUGGESTION_POLLING_INTERVAL = 1000; - -class OwnerGroupFileList extends Polymer.Element { - static get is() { - return 'owner-group-file-list'; - } - - static get properties() { - return { - files: Array, - }; - } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host { - display: block; - max-height: 500px; - overflow: auto; - } - span { - display: inline-block; - border-radius: var(--border-radius); - margin-left: var(--spacing-s); - padding: 0 var(--spacing-m); - color: var(--primary-text-color); - font-size: var(--font-size-small); - } - .Renamed { - background-color: var(--dark-remove-highlight-color); - margin: var(--spacing-s) 0; - } - </style> - <ul> - <template - is="dom-repeat" - items="[[files]]" - as="file" - > - <li> - [[computeFilePath(file)]]<!-- - --><strong>[[computeFileName(file)]]</strong> - <template is="dom-if" if="[[file.status]]"> - <span class$="[[file.status]]"> - [[file.status]] - </span> - </template> - </li> - </template> - </ul> - `; - } - - computeFilePath(file) { - const parts = file.path.split('/'); - return parts.slice(0, parts.length - 1).join('/') + '/'; - } - - computeFileName(file) { - const parts = file.path.split('/'); - return parts.pop(); - } -} - -customElements.define(OwnerGroupFileList.is, OwnerGroupFileList); - -export class SuggestOwners extends CodeOwnersModelMixin(Polymer.Element) { - static get is() { - return 'suggest-owners'; - } - - static get template() { - return Polymer.html` - <style include="shared-styles"> - :host { - display: block; - background-color: var(--view-background-color); - border: 1px solid var(--view-background-color); - border-radius: var(--border-radius); - box-shadow: var(--elevation-level-1); - padding: 0 var(--spacing-m); - margin: var(--spacing-m) 0; - } - .loadingSpin { - display: inline-block; - } - li { - list-style: none; - } - .suggestion-container { - /* TODO: TBD */ - max-height: 300px; - overflow-y: auto; - } - .flex-break { - height: 0; - flex-basis: 100%; - } - .suggestion-row, .show-all-owners-row { - display: flex; - flex-direction: row; - align-items: flex-start; - } - .suggestion-row { - flex-wrap: wrap; - border-top: 1px solid var(--border-color); - padding: var(--spacing-s) 0; - } - .show-all-owners-row { - padding: var(--spacing-m) var(--spacing-xl) var(--spacing-s) 0; - } - .show-all-owners-row .loading { - padding: 0; - } - .show-all-owners-row .show-all-label { - margin-left: auto; /* align label to the right */ - } - .suggestion-row-indicator { - margin-right: var(--spacing-s); - visibility: hidden; - line-height: 26px; - } - .suggestion-row-indicator[visible] { - visibility: visible; - } - .suggestion-row-indicator[visible] iron-icon { - color: var(--link-color); - vertical-align: top; - position: relative; - --iron-icon-height: 18px; - --iron-icon-width: 18px; - top: 4px; /* (26-18)/2 - 26px line-height and 18px icon */ - } - .suggestion-group-name { - width: 260px; - line-height: 26px; - text-overflow: ellipsis; - overflow: hidden; - padding-right: var(--spacing-s); - white-space: nowrap; - } - .group-name-content { - display: flex; - align-items: center; - } - .group-name-content .group-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - .group-name-prefix { - padding-left: var(--spacing-s); - white-space: nowrap; - color: var(--deemphasized-text-color); - } - .suggested-owners { - --account-gap: var(--spacing-xs); - --negative-account-gap: calc(-1*var(--account-gap)); - margin: var(--negative-account-gap) 0 0 var(--negative-account-gap); - flex: 1; - } - .fetch-error-content, - .owned-by-all-users-content, - .no-owners-content { - line-height: 26px; - flex: 1; - padding-left: var(--spacing-m); - } - - .owned-by-all-users-content iron-icon { - width: 16px; - height: 16px; - padding-top: 5px; - } - - .fetch-error-content { - color: var(--error-text-color); - } - .no-owners-content a { - padding-left: var(--spacing-s); - } - .no-owners-content a iron-icon { - width: 16px; - height: 16px; - padding-top: 5px; - } - gr-account-label { - display: inline-block; - padding: var(--spacing-xs) var(--spacing-m); - user-select: none; - border: 1px solid transparent; - --label-border-radius: 8px; - /* account-max-length defines the max text width inside account-label. - With 60px the gr-account-label always has width <= 100px and 5 labels - are always fit in a single row */ - --account-max-length: 60px; - border: 1px solid var(--border-color); - margin: var(--account-gap) 0 0 var(--account-gap) - } - gr-account-label:focus { - outline: none; - } - gr-account-label:hover { - box-shadow: var(--elevation-level-1); - cursor: pointer; - } - gr-account-label[selected] { - background-color: var(--chip-selected-background-color); - border: 1px solid var(--chip-selected-background-color); - color: var(--chip-selected-text-color); - } - gr-hovercard { - max-width: 800px; - } - .loading { - display: flex; - align-content: center; - align-items: center; - justify-content: center; - padding: var(--spacing-m); - } - .loadingSpin { - width: 18px; - height: 18px; - margin-right: var(--spacing-m); - } - </style> - <ul class="suggestion-container"> - <li class="show-all-owners-row"> - <p class="loading" hidden="[[!isLoading]]"> - <span class="loadingSpin"></span> - [[progressText]] - </p> - <label class="show-all-label"> - <input - id="showAllOwnersCheckbox" - type="checkbox" - checked="{{_showAllOwners::change}}" - /> - Show all owners - </label> - </li> - </ul> - <ul class="suggestion-container"> - <template - is="dom-repeat" - items="[[suggestedOwners]]" - as="suggestion" - index-as="suggestionIndex" - > - <li class="suggestion-row"> - <div - class="suggestion-row-indicator" - visible$="[[suggestion.hasSelected]]" - > - <iron-icon icon="gr-icons:check-circle"></iron-icon> - </div> - <div class="suggestion-group-name"> - <div class="group-name-content"> - <span class="group-name"> - [[suggestion.groupName.name]] - </span> - <template is="dom-if" if="[[suggestion.groupName.prefix]]"> - <span class="group-name-prefix"> - ([[suggestion.groupName.prefix]]) - </span> - </template> - <gr-hovercard hidden="[[suggestion.expanded]]"> - <owner-group-file-list - files="[[suggestion.files]]" - > - </owner-group-file-list> - </gr-hovercard> - </div> - <owner-group-file-list - hidden="[[!suggestion.expanded]]" - files="[[suggestion.files]]" - ></owner-group-file-list> - </div> - <template is="dom-if" if="[[suggestion.error]]"> - <div class="fetch-error-content"> - [[suggestion.error]] - <a on-click="_showErrorDetails" - </div> - </template> - <template is="dom-if" if="[[!suggestion.error]]"> - <template is="dom-if" if="[[!_areOwnersFound(suggestion.owners)]]"> - <div class="no-owners-content"> - <span>Not found</span> - <a on-click="_reportDocClick" href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/how-to-use.md#no-code-owners-found" target="_blank"> - <iron-icon icon="gr-icons:help-outline" title="read documentation"></iron-icon> - </a> - </div> - </template> - <template is="dom-if" if="[[suggestion.owners.owned_by_all_users]]"> - <div class="owned-by-all-users-content"> - <iron-icon icon="gr-icons:info" ></iron-icon> - <span>[[_getOwnedByAllUsersContent(isLoading, suggestedOwners)]]</span> - </div> - </template> - <template is="dom-if" if="[[!suggestion.owners.owned_by_all_users]]"> - <template is="dom-if" if="[[_showAllOwners]]"> - <div class="flex-break"></div> - </template> - <ul class="suggested-owners"> - <template - is="dom-repeat" - items="[[suggestion.owners.code_owners]]" - as="owner" - index-as="ownerIndex" - ><!-- - --><gr-account-label - data-suggestion-index$="[[suggestionIndex]]" - data-owner-index$="[[ownerIndex]]" - account="[[owner.account]]" - selected$="[[isSelected(owner)]]" - on-click="toggleAccount"> - </gr-account-label><!-- - --></template> - </ul> - </template> - </template> - </li> - </template> - </ul> - `; - } - - static get properties() { - return { - // @internal attributes - hidden: { - type: Boolean, - value: true, - reflectToAttribute: true, - computed: '_isHidden(model.showSuggestions)', - }, - suggestedOwners: Array, - isLoading: { - type: Boolean, - value: true, - }, - reviewers: { - type: Array, - }, - _reviewersIdSet: { - type: Object, - computed: '_getReviewersIdSet(reviewers)', - }, - pendingReviewers: Array, - _showAllOwners: { - type: Boolean, - value: false, - observer: '_showAllOwnersChanged', - }, - _allOwnersByPathMap: { - type: Object, - computed: - `_getOwnersByPathMap(model.suggestionsByTypes.${SuggestionsType.ALL_SUGGESTIONS}.files)`, - }, - }; - } - - static get observers() { - return [ - '_onReviewerChanged(reviewers)', - '_onShowSuggestionsChanged(model.showSuggestions)', - '_onShowSuggestionsTypeChanged(model.showSuggestions,' + - 'model.selectedSuggestionsType)', - '_onSuggestionsStateChanged(model.selectedSuggestions.state)', - '_onSuggestionsFilesChanged(model.selectedSuggestions.files, ' + - '_allOwnersByPathMap, _reviewersIdSet, model.selectedSuggestionsType,' + - 'model.selectedSuggestions.state)', - '_onSuggestionsLoadProgressChanged(' + - 'model.selectedSuggestions.loadProgress)', - ]; - } - - constructor() { - super(); - // To prevent multiple reporting when switching back and forth showAllOwners - this.reportedEvents = {}; - for (const suggestionType of Object.values(SuggestionsType)) { - this.reportedEvents[suggestionType] = { - fetchingStart: false, - fetchingFinished: false, - }; - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - this._stopUpdateProgressTimer(); - if (this.modelLoader) { - this.modelLoader.pauseActiveSuggestedOwnersLoading(); - } - } - - _onShowSuggestionsChanged(showSuggestions) { - if (!showSuggestions) { - return; - } - // this is more of a hack to let review input lose focus - // to avoid suggestion dropdown - // gr-autocomplete has a internal state for tracking focus - // that will be canceled if any click happens outside of - // it's target - // Can not use `this.async` as it's only available in - // legacy element mixin which not used in this plugin. - Polymer.Async.timeOut.run(() => this.click(), 100); - } - - _onShowSuggestionsTypeChanged(showSuggestion, selectedSuggestionsType) { - if (!showSuggestion) { - this.modelLoader.pauseActiveSuggestedOwnersLoading(); - return; - } - this.modelLoader.loadSuggestions(selectedSuggestionsType); - // The progress is updated at the next _progressUpdateTimer tick. - // Without excplicit call to updateLoadSuggestionsProgress it looks like - // a slow reaction to checkbox. - this.modelLoader.updateLoadSelectedSuggestionsProgress(); - - if (!this.reportedEvents[selectedSuggestionsType].fetchingStart) { - this.reportedEvents[selectedSuggestionsType].fetchingStart = true; - this.reporting.reportLifeCycle('owners-suggestions-fetching-start', { - type: selectedSuggestionsType, - }); - } - } - - _startUpdateProgressTimer() { - if (this._progressUpdateTimer) return; - this._progressUpdateTimer = setInterval(() => { - this.modelLoader.updateLoadSelectedSuggestionsProgress(); - }, SUGGESTION_POLLING_INTERVAL); - } - - _stopUpdateProgressTimer() { - if (!this._progressUpdateTimer) return; - clearInterval(this._progressUpdateTimer); - this._progressUpdateTimer = undefined; - } - - _onSuggestionsStateChanged(state) { - this._stopUpdateProgressTimer(); - if (state === SuggestionsState.Loading) { - this._startUpdateProgressTimer(); - } - this.isLoading = state === SuggestionsState.Loading; - } - - _isHidden(showSuggestions) { - return !showSuggestions; - } - - loadPropertiesAfterModelChanged() { - super.loadPropertiesAfterModelChanged(); - this._stopUpdateProgressTimer(); - this.modelLoader.loadAreAllFilesApproved(); - } - - _getReviewersIdSet(reviewers) { - return new Set((reviewers || []).map(account => account._account_id)); - } - - _onSuggestionsFilesChanged(files, allOwnersByPathMap, reviewersIdSet, - selectedSuggestionsType, suggestionsState) { - if (files === undefined || allOwnersByPathMap === undefined || - reviewersIdSet === undefined || selectedSuggestionsType === undefined || - suggestionsState === undefined) return; - - const groups = getDisplayOwnersGroups( - files, allOwnersByPathMap, reviewersIdSet, - selectedSuggestionsType !== SuggestionsType.ALL_SUGGESTIONS); - // The updateLoadSuggestionsProgress method also updates suggestions - this._updateSuggestions(groups); - this._updateAllChips(this._currentReviewers); - - if (suggestionsState !== SuggestionsState.Loaded) return; - if (!this.reportedEvents[selectedSuggestionsType].fetchingFinished) { - this.reportedEvents[selectedSuggestionsType].fetchingFinished = true; - const reportDetails = groups.reduce((details, cur) => { - details.totalGroups++; - details.stats.push([cur.files.length, - cur.owners && cur.owners.code_owners ? - cur.owners.code_owners.length : 0]); - return details; - }, {totalGroups: 0, stats: [], type: selectedSuggestionsType}); - this.reporting.reportLifeCycle( - 'owners-suggestions-fetching-finished', reportDetails); - } - } - - _getOwnersByPathMap(files) { - return new Map((files || []) - .filter(file => !file.info.error && file.info.owners) - .map(file => [file.path, file.info.owners]) - ); - } - - _onSuggestionsLoadProgressChanged(progress) { - this.progressText = progress; - } - - _updateSuggestions(suggestions) { - // update group names and files, no modification on owners or error - const suggestedOwners = suggestions.map(suggestion => { - return this.formatSuggestionInfo(suggestion); - }); - // move owned_by_all_users to the bottom: - const index = suggestedOwners - .findIndex(suggestion => suggestion.owners.owned_by_all_users); - if (index >= 0) { - suggestedOwners.push(suggestedOwners.splice(index, 1)[0]); - } - this.suggestedOwners = suggestedOwners; - } - - _onReviewerChanged(reviewers) { - this._currentReviewers = reviewers; - this._updateAllChips(reviewers); - } - - formatSuggestionInfo(suggestion) { - const res = {}; - res.groupName = suggestion.groupName; - res.files = suggestion.files.slice(); - if (suggestion.owners) { - const codeOwners = (suggestion.owners.code_owners || []).map(owner => { - const updatedOwner = {...owner}; - const reviewers = this.change.reviewers.REVIEWER; - if (reviewers && - reviewers.find( - reviewer => reviewer._account_id === owner._account_id) - ) { - updatedOwner.selected = true; - } - return updatedOwner; - }); - res.owners = { - owned_by_all_users: !!suggestion.owners.owned_by_all_users, - code_owners: codeOwners, - }; - } else { - res.owners = { - owned_by_all_users: false, - code_owners: [], - }; - } - - res.error = suggestion.error; - return res; - } - - addAccount(owner) { - owner.selected = true; - this.dispatchEvent( - new CustomEvent('add-reviewer', { - detail: { - reviewer: {...owner.account, _pendingAdd: true}, - }, - composed: true, - bubbles: true, - }) - ); - this.reporting.reportInteraction('add-reviewer'); - } - - removeAccount(owner) { - owner.selected = false; - this.dispatchEvent( - new CustomEvent('remove-reviewer', { - detail: { - reviewer: {...owner.account, _pendingAdd: true}, - }, - composed: true, - bubbles: true, - }) - ); - this.reporting.reportInteraction('remove-reviewer'); - } - - toggleAccount(e) { - const grAccountLabel = e.currentTarget; - const owner = this.suggestedOwners[grAccountLabel.dataset.suggestionIndex] - .owners.code_owners[grAccountLabel.dataset.ownerIndex]; - if (this.isSelected(owner)) { - this.removeAccount(owner); - } else { - this.addAccount(owner); - } - } - - _updateAllChips(accounts) { - if (!this.suggestedOwners || !accounts) return; - // update all occurences - this.suggestedOwners.forEach((suggestion, sId) => { - let hasSelected = false; - suggestion.owners.code_owners.forEach((owner, oId) => { - if (accounts.some( - account => account._account_id === owner.account._account_id)) { - this.set( - ['suggestedOwners', sId, 'owners', 'code_owners', oId], - {...owner, - selected: true, - } - ); - hasSelected = true; - } else { - this.set( - ['suggestedOwners', sId, 'owners', 'code_owners', oId], - {...owner, selected: false} - ); - } - }); - const nonServiceUser = account => - !account.tags || account.tags.indexOf('SERVICE_USER') < 0; - if (suggestion.owners.owned_by_all_users && - accounts.some(nonServiceUser)) { - hasSelected = true; - } - this.set(['suggestedOwners', sId, 'hasSelected'], hasSelected); - }); - } - - isSelected(owner) { - return owner.selected; - } - - _reportDocClick() { - this.reporting.reportInteraction('code-owners-doc-click', - {section: 'no owners found'}); - } - - _areOwnersFound(owners) { - return owners.code_owners.length > 0 || !!owners.owned_by_all_users; - } - - _getOwnedByAllUsersContent(isLoading, suggestedOwners) { - if (isLoading) { - return 'Any user can approve'; - } - // If all users own all the files in the change suggestedOwners.length === 1 - // (suggestedOwners - collection of owners groupbed by owners) - return suggestedOwners && suggestedOwners.length === 1 ? - 'Any user can approve. Please select a user manually' : - 'Any user from the other files can approve'; - } - - _showAllOwnersChanged(showAll) { - // The first call to this method happens before model is set. - if (!this.model) return; - this.model.setSelectedSuggestionType(showAll ? - SuggestionsType.ALL_SUGGESTIONS : SuggestionsType.BEST_SUGGESTIONS); - } -} - -customElements.define(SuggestOwners.is, SuggestOwners);
diff --git a/web/.eslintrc.js b/web/.eslintrc.js new file mode 100644 index 0000000..2144c1c --- /dev/null +++ b/web/.eslintrc.js
@@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +__plugindir = "code-owners/web"; +module.exports = { + extends: "../../.eslintrc.js", +};
diff --git a/web/BUILD b/web/BUILD new file mode 100644 index 0000000..ace2af9 --- /dev/null +++ b/web/BUILD
@@ -0,0 +1,63 @@ + +load("//tools/js:eslint.bzl", "plugin_eslint") +load("//tools/bzl:js.bzl", "gerrit_js_bundle", "karma_test") +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") + +package_group( + name = "visibility", + packages = ["//plugins/code-owners/..."], +) + +package(default_visibility = [":visibility"]) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//plugins:tsconfig-plugins-base.json", + ], +) + +ts_project( + name = "code-owners-ts", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*test*"], + ), + incremental = True, + out_dir = "_bazel_ts_out", + tsc = "//tools/node_tools:tsc-bin", + tsconfig = ":tsconfig", + deps = [ + "@plugins_npm//@gerritcodereview/typescript-api", + "@plugins_npm//lit", + "@plugins_npm//rxjs", + ], +) + +ts_project( + name = "code-owners-ts-tests", + srcs = glob(["**/*.ts"]), + incremental = True, + out_dir = "_bazel_ts_out_tests", + tsc = "//tools/node_tools:tsc-bin", + tsconfig = ":tsconfig", + deps = [ + "@plugins_npm//:node_modules", + "@ui_dev_npm//:node_modules" + ], +) + +gerrit_js_bundle( + name = "code-owners", + srcs = [":code-owners-ts"], + entry_point = "_bazel_ts_out/plugin.js", +) + +karma_test( + name = "karma_test", + srcs = ["karma_test.sh"], + data = [":code-owners-ts-tests"], +) + +plugin_eslint()
diff --git a/web/code-owners-api.ts b/web/code-owners-api.ts new file mode 100644 index 0000000..8f5ace1 --- /dev/null +++ b/web/code-owners-api.ts
@@ -0,0 +1,333 @@ +/** + * @license + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {HttpMethod, RestPluginApi} from '@gerritcodereview/typescript-api/rest'; +import { + AccountDetailInfo, + AccountInfo, + BranchName, + ChangeInfo, + NumericChangeId, + RepoName, +} from '@gerritcodereview/typescript-api/rest-api'; + +// TODO: Try to remove it. The ResponseError and getErrorMessage duplicates +// code from the gr-plugin-rest-api.ts. This code is required because +// we want custom error processing in some functions. For details see +// the original gr-plugin-rest-api.ts file/ +class ResponseError extends Error { + constructor(readonly response: Response) { + super(); + } +} + +export class ServerConfigurationError extends Error { + constructor(msg: string) { + super(msg); + } +} + +async function getErrorMessage(response: Response) { + const text = await response.text(); + return text ? `${response.status}: ${text}` : `${response.status}`; +} + +export enum MergeCommitStrategy { + ALL_CHANGED_FILES = 'ALL_CHANGED_FILES', + FILES_WITH_CONFLICT_RESOLUTION = 'FILES_WITH_CONFLICT_RESOLUTION', +} + +export enum FallbackCodeOwners { + NONE = 'NONE', + ALL_USERS = 'ALL_USERS', +} + +export interface GeneralInfo { + file_extension?: string; + merge_commit_strategy: MergeCommitStrategy; + implicit_approvals?: boolean; + override_info_url?: string; + invalid_code_owner_config_info_url?: string; + fallback_code_owners: FallbackCodeOwners; +} + +export interface CodeOwnerBranchConfigInfo { + general?: GeneralInfo; + disabled?: boolean; + backend_id?: string; + required_approval?: Array<RequiredApprovalInfo>; + override_approval?: Array<RequiredApprovalInfo>; +} + +export interface RequiredApprovalInfo { + label: string; + value: number; +} + +export enum ChangeType { + ADDED = 'ADDED', + MODIFIED = 'MODIFIED', + DELETED = 'DELETED', + RENAMED = 'RENAMED', + COPIED = 'COPIED', +} + +export enum OwnerStatus { + INSUFFICIENT_REVIEWERS = 'INSUFFICIENT_REVIEWERS', + PENDING = 'PENDING', + APPROVED = 'APPROVED', +} +export interface PathCodeOwnerStatusInfo { + path: string; + status: OwnerStatus; + reasons?: Array<string>; +} + +export interface FileCodeOwnerStatusInfo { + change_type: ChangeType; + old_path_status?: PathCodeOwnerStatusInfo; + new_path_status?: PathCodeOwnerStatusInfo; +} + +export interface CodeOwnerStatusInfo { + patch_set_number: number; + file_code_owner_statuses: Array<FileCodeOwnerStatusInfo>; + more?: boolean; + accounts?: {[account_id: number]: AccountInfo}; +} + +export interface CodeOwnersStatusInfo { + disabled?: boolean; + disabled_branches?: Array<string>; +} + +export interface CodeOwnerInfo { + account?: AccountInfo; + selected?: boolean; +} +export interface CodeOwnersInfo { + code_owners: Array<CodeOwnerInfo>; + owned_by_all_users?: boolean; +} + +export interface FetchedOwner { + owners?: CodeOwnersInfo; + error?: unknown; +} + +export interface FetchedFile { + path: string; + info: FetchedOwner; + status?: string; +} + +export interface OwnedPathInfo { + path: string; + owned?: boolean; + owners?: Array<AccountInfo>; +} + +export interface OwnedChangedFileInfo { + new_path?: OwnedPathInfo; + old_path?: OwnedPathInfo; +} + +export interface OwnedPathsInfo { + owned_changed_files?: Array<OwnedChangedFileInfo>; +} + +/** + * Responsible for communicating with the rest-api + * + * @see resources/Documentation/rest-api.md + */ +export class CodeOwnersApi { + constructor(readonly restApi: RestPluginApi) {} + + /** + * Send a get request and provides custom response-code handling + */ + private async get(url: string): Promise<unknown> { + const errFn = (response?: Response | null, error?: Error) => { + if (error) throw error; + if (response) throw new ResponseError(response); + throw new Error('Generic REST API error'); + }; + try { + return await this.restApi.send(HttpMethod.GET, url, undefined, errFn); + } catch (err) { + if (err instanceof ResponseError && err.response.status === 409) { + return getErrorMessage(err.response).then(msg => { + throw new ServerConfigurationError(msg); + }); + } + throw err; + } + } + + /** + * Returns a promise fetching the owner statuses for all files within the + * change. + * + * @doc + * https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#change-endpoints + */ + listOwnerStatus(changeId: NumericChangeId): Promise<CodeOwnerStatusInfo> { + return this.get( + `/changes/${changeId}/code_owners.status?limit=100000` + ) as Promise<CodeOwnerStatusInfo>; + } + + /** + * Returns a promise fetching which files are owned by a given user as well + * as which reviewers own which files. + */ + listOwnedPaths(changeId: NumericChangeId, account: AccountInfo) { + if (!account.email && !account._account_id) + return Promise.resolve(undefined); + const user = account.email ?? account._account_id; + return this.get( + `/changes/${changeId}/revisions/current/owned_paths?user=${user}&limit=10000&check_reviewers` + ) as Promise<OwnedPathsInfo>; + } + + /** + * Returns a promise fetching the owners for a given path. + * + * @doc + * https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#list-code-owners-for-path-in-branch + */ + listOwnersForPath( + changeId: NumericChangeId, + path: string, + limit: number + ): Promise<CodeOwnersInfo> { + return this.get( + `/changes/${changeId}/revisions/current/code_owners` + + `/${encodeURIComponent(path)}?limit=${limit}&o=DETAILS` + ) as Promise<CodeOwnersInfo>; + } + + /** + * Returns a promise fetching the owners config for a given path. + * + * @doc + * https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#branch-endpoints + */ + getConfigForPath(project: string, branch: string, path: string) { + return this.get( + `/projects/${encodeURIComponent(project)}/` + + `branches/${encodeURIComponent(branch)}/` + + `code_owners.config/${encodeURIComponent(path)}` + ); + } + + /** + * Returns a promise fetching the owners config for a given branch. + * + * @doc + * https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/rest-api.md#branch-endpoints + */ + async getBranchConfig(project: RepoName, branch: BranchName) { + try { + const config = (await this.get( + `/projects/${encodeURIComponent(project)}/` + + `branches/${encodeURIComponent(branch)}/` + + `code_owners.branch_config` + )) as CodeOwnerBranchConfigInfo; + return config; + } catch (err) { + if (err instanceof ResponseError) { + if (err.response.status === 404) { + // The 404 error means that the branch doesn't exist and + // the plugin should be disabled. + return {disabled: true}; + } + return getErrorMessage(err.response).then(msg => { + throw new Error(msg); + }); + } + throw err; + } + } +} + +/** + * Wrapper around codeOwnerApi, sends each requests only once and then cache + * the response. A new CodeOwnersCacheApi instance is created every time when a + * new change object is assigned. + * Gerrit never updates existing change object, but instead always assigns a new + * change object. Particularly, a new change object is assigned when a change + * is updated and user clicks reload toasts to see the updated change. + * As a result, the lifetime of a cache is the same as a lifetime of an assigned + * change object. + * Periodical cache invalidation can lead to inconsistency in UI, i.e. + * user can see the old reviewers list (reflects a state when a change was + * loaded) and code-owners status for the current reviewer list. To avoid + * this inconsistency, the cache doesn't invalidate. + */ +export class CodeOwnersCacheApi { + private promises = new Map<string, Promise<unknown>>(); + + constructor( + private readonly codeOwnerApi: CodeOwnersApi, + private readonly change: ChangeInfo + ) {} + + private fetchOnce( + cacheKey: string, + asyncFn: () => Promise<unknown> + ): Promise<unknown> { + let promise = this.promises.get(cacheKey); + if (promise) return promise; + promise = asyncFn(); + this.promises.set(cacheKey, promise); + return promise; + } + + getAccount(): Promise<AccountDetailInfo | undefined> { + return this.fetchOnce('getAccount', () => this.getAccountImpl()) as Promise< + AccountDetailInfo | undefined + >; + } + + private async getAccountImpl() { + const loggedIn = await this.codeOwnerApi.restApi.getLoggedIn(); + if (!loggedIn) return undefined; + return await this.codeOwnerApi.restApi.getAccount(); + } + + listOwnerStatus(): Promise<CodeOwnerStatusInfo> { + return this.fetchOnce('listOwnerStatus', () => + this.codeOwnerApi.listOwnerStatus(this.change._number) + ) as Promise<CodeOwnerStatusInfo>; + } + + async listOwnedPaths(): Promise<OwnedPathsInfo | undefined> { + const account = await this.getAccount(); + if (!account) return undefined; + return this.fetchOnce('listOwnedPaths', () => + this.codeOwnerApi.listOwnedPaths(this.change._number, account) + ) as Promise<OwnedPathsInfo | undefined>; + } + + getBranchConfig(): Promise<CodeOwnerBranchConfigInfo> { + return this.fetchOnce('getBranchConfig', () => + this.codeOwnerApi.getBranchConfig(this.change.project, this.change.branch) + ) as Promise<CodeOwnerBranchConfigInfo>; + } +}
diff --git a/web/code-owners-banner.ts b/web/code-owners-banner.ts new file mode 100644 index 0000000..9eccb65 --- /dev/null +++ b/web/code-owners-banner.ts
@@ -0,0 +1,166 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {css, html, LitElement, nothing, PropertyValues} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {CodeOwnersModelMixin} from './code-owners-model-mixin.js'; +import { + PluginState, + isPluginErrorState, + PluginStatus, +} from './code-owners-model.js'; + +// There are 2 elements in this file: +// CodeOwnersBanner - visual elements. This element is shown at the top +// of the page when pluginStatus are changed to failed. +// The CodeOwnersBanner is added to the 'banner' endpoint. The endpoint +// is placed at the top level of the gerrit and doesn't provide a current +// change, so it is impossible to get the pluginStatus directly in the +// CodeOwnersBanner. +// To solve this problem, this files provides non-visual +// CodeOwnersPluginStatusNotifier element. This element is added to the +// change-view-integration endpoint, so it can track the plugin state. +// When a plugin state is changed, the CodeOwnersPluginStatusNotifier updates +// the pluginStatus property of the banner. + +// CodeOwnersBanner and CodeOwnersPluginStatusNotifier can be attached and +// detached in any order. The following piece of code ensures that these +// banner and notifier are always connected correctly. The code assumes, +// that max one banner and one notifier can be connected at any time. +let activeBanner: CodeOwnersBanner | undefined; +let activeStatusNotifier: CodeOwnersPluginStatusNotifier | undefined; + +function setActiveBanner(banner: CodeOwnersBanner | undefined) { + // banner is null when CodeOwnersBanner has been disconnected + activeBanner = banner; + if (activeStatusNotifier) { + activeStatusNotifier.banner = banner; + } +} + +function setActiveStatusNotifier( + notifier: CodeOwnersPluginStatusNotifier | undefined +) { + // notifier is null when CodeOwnersBanner has been disconnected + if (activeStatusNotifier) { + if (activeStatusNotifier.banner) { + activeStatusNotifier.banner.pluginStatus = undefined; + } + activeStatusNotifier.banner = undefined; + } + activeStatusNotifier = notifier; + if (activeStatusNotifier) { + activeStatusNotifier.banner = activeBanner; + } +} + +export const CODE_OWNERS_BANNER = 'gr-code-owners-banner'; +@customElement(CODE_OWNERS_BANNER) +export class CodeOwnersBanner extends LitElement { + @property({type: Object}) + pluginStatus?: PluginStatus; + + static override get styles() { + return css` + :host { + display: block; + overflow: hidden; + background: red; + } + .text { + color: white; + font-family: var(--header-font-family); + font-size: var(--font-size-h3); + font-weight: var(--font-weight-h3); + line-height: var(--line-height-h3); + margin-left: var(--spacing-l); + } + `; + } + + override render() { + if (!this.pluginStatus || !isPluginErrorState(this.pluginStatus.state)) + return nothing; + return html` + <span class="text">${this.getErrorText()}</span> + <gr-button link @click=${this.showFailDetails}> Details </gr-button> + `; + } + + override connectedCallback() { + super.connectedCallback(); + setActiveBanner(this); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + setActiveBanner(undefined); + } + + private getErrorText() { + return !this.pluginStatus || this.pluginStatus.state === PluginState.Failed + ? 'Error: Code-owners plugin has failed' + : 'The code-owners plugin has configuration issue. ' + + 'Please contact the project owner or the host admin.'; + } + + private showFailDetails() { + showPluginFailedMessage(this, this.pluginStatus!); + } +} + +export const CODE_OWNERS_PLUGIN_STATUS_NOTIFIER = + 'owners-plugin-status-notifier'; +const base = CodeOwnersModelMixin(LitElement); +@customElement(CODE_OWNERS_PLUGIN_STATUS_NOTIFIER) +export class CodeOwnersPluginStatusNotifier extends base { + @property({type: Object}) + banner?: CodeOwnersBanner; + + override connectedCallback() { + super.connectedCallback(); + setActiveStatusNotifier(this); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + setActiveStatusNotifier(undefined); + } + + protected override willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + if (!this.banner || !this.model) return; + this.banner.pluginStatus = this.pluginStatus; + } + + override loadPropertiesAfterModelChanged() { + this.modelLoader?.loadPluginStatus(); + this.modelLoader?.loadBranchConfig(); + } +} + +export function showPluginFailedMessage( + sourceEl: HTMLElement, + pluginStatus: PluginStatus +) { + sourceEl.dispatchEvent( + new CustomEvent('show-error', { + detail: {message: pluginStatus.failedMessage}, + composed: true, + bubbles: true, + }) + ); +}
diff --git a/web/code-owners-fetcher.ts b/web/code-owners-fetcher.ts new file mode 100644 index 0000000..2057641 --- /dev/null +++ b/web/code-owners-fetcher.ts
@@ -0,0 +1,226 @@ +/** + * @license + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ChangeInfo, + NumericChangeId, +} from '@gerritcodereview/typescript-api/rest-api.js'; +import {RestPluginApi} from '@gerritcodereview/typescript-api/rest.js'; +import {CodeOwnersApi, FetchedOwner, OwnerStatus} from './code-owners-api.js'; +import {FileStatus} from './code-owners-model.js'; + +/** + * @enum + */ +export const FetchStatus = { + /** Fetch hasn't been started */ + NOT_STARTED: 0, + /** + * Fetch has been started, but not all files has been finished. + * Pausing during fetching doesn't change state. + */ + FETCHING: 1, + /** + * All owners has been loaded. resume/pause call doesn't change state. + */ + FINISHED: 2, +}; + +/** + * Fetch owners for files. The class fetches owners in parallel and allows to + * pause/resume fetch. + */ +class OwnersFetcher { + private paused = true; + + private fetchedOwners = new Map<string, FetchedOwner>(); + + private pausedFilesFetcher: Array<() => void> = []; + + private fetchFilesPromises: Array<Promise<void>> = []; + + /** + * Creates a fetcher in paused state. Actual fetching starts after resume() + * is called. + */ + constructor( + private readonly codeOwnerApi: CodeOwnersApi, + private readonly changeId: NumericChangeId, + private readonly filesToFetch: Array<string>, + private readonly ownersLimit: number, + maxConcurrentRequest: number + ) { + for (let i = 0; i < maxConcurrentRequest; i++) { + this.fetchFilesPromises.push(this.fetchFiles()); + } + } + + private async fetchFiles() { + for (;;) { + const filePath = await this.getNextFilePath(); + if (!filePath) return; + try { + this.fetchedOwners.set(filePath, { + owners: await this.codeOwnerApi.listOwnersForPath( + this.changeId, + filePath, + this.ownersLimit + ), + }); + } catch (error) { + this.fetchedOwners.set(filePath, {error}); + } + } + } + + private async getNextFilePath() { + if (this.paused) { + await new Promise<void>(resolve => this.pausedFilesFetcher.push(resolve)); + } + if (this.filesToFetch.length === 0) return null; + return this.filesToFetch.splice(0, 1)[0]; + } + + async waitFetchComplete() { + // Simplified polyfill allSettled + // TODO: Replace with allSettled once minimal node version is updated. + await Promise.all( + this.fetchFilesPromises.map(p => + p.then(() => 'succeeded').catch(_reason => 'rejected') + ) + ); + } + + resume() { + if (!this.paused) return; + this.paused = false; + for (const fetcher of this.pausedFilesFetcher.splice( + 0, + this.pausedFilesFetcher.length + )) { + fetcher(); + } + } + + pause() { + this.paused = true; + } + + getFetchedOwners() { + return this.fetchedOwners; + } + + getFiles(): Array<{path: string; info: FetchedOwner}> { + const result = []; + for (const [path, info] of this.fetchedOwners.entries()) { + result.push({path, info}); + } + return result; + } +} + +export class OwnersProvider { + private status = FetchStatus.NOT_STARTED; + + private totalFetchCount = 0; + + private codeOwnerApi: CodeOwnersApi; + + private ownersFetcher?: OwnersFetcher; + + constructor( + restApi: RestPluginApi, + private readonly change: ChangeInfo, + private readonly options: { + maxConcurrentRequests: number; + ownersLimit: number; + } + ) { + this.codeOwnerApi = new CodeOwnersApi(restApi); + } + + getStatus() { + return this.status; + } + + getProgressString() { + return !this.ownersFetcher || this.totalFetchCount === 0 + ? 'Loading suggested owners ...' + : `${this.ownersFetcher.getFetchedOwners().size} out of ` + + `${this.totalFetchCount} files have returned suggested owners.`; + } + + getFiles() { + if (!this.ownersFetcher) return []; + return this.ownersFetcher.getFiles(); + } + + async fetchSuggestedOwners(codeOwnerStatusMap: Map<string, FileStatus>) { + if (this.status !== FetchStatus.NOT_STARTED) { + await this.ownersFetcher!.waitFetchComplete(); + return; + } + const filesToFetch = this._getFilesToFetch(codeOwnerStatusMap); + this.totalFetchCount = filesToFetch.length; + this.ownersFetcher = new OwnersFetcher( + this.codeOwnerApi, + this.change._number, + filesToFetch, + this.options.ownersLimit, + this.options.maxConcurrentRequests + ); + this.status = FetchStatus.FETCHING; + this.ownersFetcher.resume(); + await this.ownersFetcher.waitFetchComplete(); + this.status = FetchStatus.FINISHED; + } + + _getFilesToFetch(codeOwnerStatusMap: Map<string, FileStatus>) { + // only fetch those not approved yet + const filesGroupByStatus = [...codeOwnerStatusMap.entries()].reduce( + (list, [file, fileInfo]) => { + if (list[fileInfo.status]) list[fileInfo.status].push(file); + return list; + }, + { + [OwnerStatus.PENDING]: [] as Array<string>, + [OwnerStatus.INSUFFICIENT_REVIEWERS]: [] as Array<string>, + [OwnerStatus.APPROVED]: [] as Array<string>, + } + ); + // always fetch INSUFFICIENT_REVIEWERS first, then pending and then approved + return filesGroupByStatus[OwnerStatus.INSUFFICIENT_REVIEWERS] + .concat(filesGroupByStatus[OwnerStatus.PENDING]) + .concat(filesGroupByStatus[OwnerStatus.APPROVED]); + } + + pause() { + if (!this.ownersFetcher) return; + this.ownersFetcher.pause(); + } + + resume() { + if (!this.ownersFetcher) return; + this.ownersFetcher.resume(); + } + + reset() { + this.totalFetchCount = 0; + this.ownersFetcher = undefined; + this.status = FetchStatus.NOT_STARTED; + } +}
diff --git a/web/code-owners-model-loader.ts b/web/code-owners-model-loader.ts new file mode 100644 index 0000000..c9a79e9 --- /dev/null +++ b/web/code-owners-model-loader.ts
@@ -0,0 +1,199 @@ +/** + * @license + * Copyright ( + * ) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CodeOwnersModel, + CodeOwnersState, + SuggestionsState, + SuggestionsType, +} from './code-owners-model.js'; +import {ServerConfigurationError} from './code-owners-api.js'; +import {CodeOwnerService} from './code-owners-service.js'; + +/** + * ModelLoader provides a method for loading data into the model. + * It is a bridge between an ownersModel and an ownersService. + * When UI elements depends on a model property XXX, the element should + * observe (or bind to) the property and call modelLoader.loadXXX method + * to load the value. + * It is recommended to use CodeOwnersModelMixin and call all load... methods + * in the loadPropertiesAfterModelChanged method + */ +export class ModelLoader { + private activeLoadSuggestionType?: SuggestionsType; + + constructor( + private readonly ownersService: CodeOwnerService, + private readonly ownersModel: CodeOwnersModel + ) {} + + async _loadProperty<K extends keyof CodeOwnersState, T>( + propertyName: K, + propertyLoader: () => Promise<T>, + propertySetter: (value: T) => void + ) { + if (this.ownersModel.state[propertyName] !== undefined) return; + let newValue; + try { + newValue = await propertyLoader(); + } catch (e) { + if (e instanceof ServerConfigurationError) { + this.ownersModel.setServerConfigurationError(e.message); + return; + } + console.error(e); + this.ownersModel.setPluginFailed((e as Error).message); + return; + } + // It is possible, that several requests is made in parallel. + // Store only the first result and discard all other results. + // (also, due to the CodeOwnersCacheApi all result must be identical) + if (this.ownersModel.state[propertyName] !== undefined) return; + propertySetter(newValue); + } + + async loadBranchConfig() { + await this._loadProperty( + 'branchConfig', + () => this.ownersService.getBranchConfig(), + value => this.ownersModel.setBranchConfig(value) + ); + } + + async loadStatus() { + await this._loadProperty( + 'status', + () => this.ownersService.getStatus(), + value => this.ownersModel.setStatus(value) + ); + } + + async loadOwnedPaths() { + await this._loadProperty( + 'ownedPaths', + () => this.ownersService.getOwnedPaths(), + paths => this.ownersModel.setOwnedPaths(paths) + ); + } + + async loadUserRole() { + await this._loadProperty( + 'userRole', + () => this.ownersService.getLoggedInUserInitialRole(), + value => this.ownersModel.setUserRole(value) + ); + } + + async loadPluginStatus() { + await this._loadProperty( + 'pluginStatus', + () => this.ownersService.isCodeOwnerEnabled(), + value => this.ownersModel.setPluginEnabled(value) + ); + } + + async loadAreAllFilesApproved() { + await this._loadProperty( + 'areAllFilesApproved', + () => this.ownersService.areAllFilesApproved(), + value => this.ownersModel.setAreAllFilesApproved(value) + ); + } + + async loadSuggestions(suggestionsType: SuggestionsType) { + // NOTE: This line is necessary to avoid an infinite loop with + // SuggestOwners.onShowSuggestionsTypeChanged + if (this.activeLoadSuggestionType === suggestionsType) return; + this.pauseActiveSuggestedOwnersLoading(); + this.activeLoadSuggestionType = suggestionsType; + if ( + this.ownersModel.getSuggestionsByType(suggestionsType)!.state === + SuggestionsState.Loading + ) { + this.ownersService.resumeSuggestedOwnersLoading(suggestionsType); + return; + } + + // If a loading has been started already, do nothing + if ( + this.ownersModel.getSuggestionsByType(suggestionsType)!.state !== + SuggestionsState.NotLoaded + ) + return; + + this.ownersModel.setSuggestionsState( + suggestionsType, + SuggestionsState.Loading + ); + let suggestedOwners; + try { + suggestedOwners = await this.ownersService.getSuggestedOwners( + suggestionsType + ); + } catch (e) { + console.error(e); + this.ownersModel.setSuggestionsState( + suggestionsType, + SuggestionsState.LoadFailed + ); + // The selectedSuggestionsType can be changed while getSuggestedOwners + // is executed. The plugin should fail only if the selectedSuggestionsType + // is the same. + if (this.ownersModel.state.selectedSuggestionsType === suggestionsType) { + this.ownersModel.setPluginFailed((e as Error).message); + } + return; + } + this.ownersModel.setSuggestionsFiles( + suggestionsType, + suggestedOwners.files + ); + this.ownersModel.setSuggestionsState( + suggestionsType, + SuggestionsState.Loaded + ); + } + + pauseActiveSuggestedOwnersLoading() { + if (!this.activeLoadSuggestionType) return; + this.ownersService.pauseSuggestedOwnersLoading( + this.activeLoadSuggestionType + ); + } + + async updateLoadSelectedSuggestionsProgress() { + const suggestionsType = this.ownersModel.state.selectedSuggestionsType; + let suggestedOwners; + try { + suggestedOwners = await this.ownersService.getSuggestedOwnersProgress( + suggestionsType + ); + } catch { + // Ignore any error, keep progress unchanged. + return; + } + this.ownersModel.setSuggestionsLoadProgress( + suggestionsType, + suggestedOwners.progress + ); + this.ownersModel.setSuggestionsFiles( + suggestionsType, + suggestedOwners.files + ); + } +}
diff --git a/web/code-owners-model-mixin.ts b/web/code-owners-model-mixin.ts new file mode 100644 index 0000000..23bb9fe --- /dev/null +++ b/web/code-owners-model-mixin.ts
@@ -0,0 +1,230 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Subscription} from 'rxjs'; +import {CodeOwnerService} from './code-owners-service'; +import {ModelLoader} from './code-owners-model-loader'; +import { + CodeOwnersModel, + OwnedPathsInfoOpt, + PluginStatus, + Status, + SuggestionsState, + SuggestionsType, + UserRole, +} from './code-owners-model'; +import {LitElement, PropertyValues} from 'lit'; +import {property, state} from 'lit/decorators'; +import {ChangeInfo} from '@gerritcodereview/typescript-api/rest-api'; +import {ReportingPluginApi} from '@gerritcodereview/typescript-api/reporting'; +import {RestPluginApi} from '@gerritcodereview/typescript-api/rest'; +import {CodeOwnerBranchConfigInfo, FetchedFile} from './code-owners-api'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Constructor<T> = new (...args: any[]) => T; + +export interface CodeOwnersModelMixinInterface { + change?: ChangeInfo; + reporting?: ReportingPluginApi; + restApi?: RestPluginApi; + modelLoader?: ModelLoader; + model?: CodeOwnersModel; + + pluginStatus?: PluginStatus; + branchConfig?: CodeOwnerBranchConfigInfo; + userRole?: UserRole; + areAllFilesApproved?: boolean; + status?: Status; + showSuggestions?: boolean; + allOwnersLoaded?: boolean; + selectedSuggestionsType?: SuggestionsType; + selectedSuggestionsFiles?: Array<FetchedFile>; + selectedSuggestionsState?: SuggestionsState; + selectedSuggestionsLoadProgress?: string; + ownedPaths?: OwnedPathsInfoOpt; + + loadPropertiesAfterModelChanged(): void; +} +/** + * The CodeOwnersMixin adds several properties to a class and translates + * 'model-property-changed' events from the model to the notifyPath calls. + * This allows to use properties of the model in observers, calculated + * properties and bindings. + * It is guaranteed, that model and modelLoader are set only if change, + * reporting and restApi properties are set. + */ + +export const CodeOwnersModelMixin = <T extends Constructor<LitElement>>( + superClass: T +) => { + /** + * @lit + * @mixinClass + */ + class Mixin extends superClass { + @property({type: Object}) + change?: ChangeInfo; + + @property({type: Object}) + reporting?: ReportingPluginApi; + + @property({type: Object}) + restApi?: RestPluginApi; + + @state() + pluginStatus?: PluginStatus; + + @state() + branchConfig?: CodeOwnerBranchConfigInfo; + + @state() + userRole?: UserRole; + + @state() + areAllFilesApproved?: boolean; + + @state() + status?: Status; + + @property({type: Boolean, attribute: 'show-suggestions', reflect: true}) + showSuggestions?: boolean; + + @state() + selectedSuggestionsType?: SuggestionsType; + + @state() + selectedSuggestionsFiles?: Array<FetchedFile>; + + @state() + selectedSuggestionsState?: SuggestionsState; + + @state() + selectedSuggestionsLoadProgress?: string; + + @state() + ownedPaths?: OwnedPathsInfoOpt; + + private model_?: CodeOwnersModel; + + private subscriptions: Array<Subscription> = []; + + modelLoader?: ModelLoader; + + get model() { + return this.model_; + } + + set model(model: CodeOwnersModel | undefined) { + if (this.model_ === model) return; + for (const s of this.subscriptions) { + s.unsubscribe(); + } + this.subscriptions = []; + this.model_ = model; + if (!model) return; + this.subscriptions.push( + model.state$.subscribe(s => { + this.pluginStatus = s.pluginStatus; + }) + ); + this.subscriptions.push( + model.state$.subscribe(s => { + this.branchConfig = s.branchConfig; + }) + ); + this.subscriptions.push( + model.state$.subscribe(s => { + this.status = s.status; + }) + ); + this.subscriptions.push( + model.state$.subscribe(s => { + this.userRole = s.userRole; + }) + ); + this.subscriptions.push( + model.state$.subscribe(s => { + this.areAllFilesApproved = s.areAllFilesApproved; + }) + ); + this.subscriptions.push( + model.showSuggestions$.subscribe(s => { + this.showSuggestions = s; + }) + ); + this.subscriptions.push( + model.selectedSuggestionsType$.subscribe(s => { + this.selectedSuggestionsType = s; + }) + ); + this.subscriptions.push( + model.selectedSuggestionsFiles$.subscribe(f => { + this.selectedSuggestionsFiles = f; + }) + ); + this.subscriptions.push( + model.selectedSuggestionsLoadProgress$.subscribe(p => { + this.selectedSuggestionsLoadProgress = p; + }) + ); + this.subscriptions.push( + model.selectedSuggestionsState$.subscribe(s => { + this.selectedSuggestionsState = s; + }) + ); + this.subscriptions.push( + model.ownedPaths$.subscribe(s => { + this.ownedPaths = s; + }) + ); + this.loadPropertiesAfterModelChanged(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(...args: any[]) { + super(...args); + } + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if ( + changedProperties.has('change') || + changedProperties.has('restApi') || + changedProperties.has('reporting') + ) { + if (!this.restApi || !this.change || !this.reporting) { + this.model = undefined; + this.modelLoader = undefined; + return; + } + const ownerService = CodeOwnerService.getOwnerService( + this.restApi, + this.change + ); + const model = CodeOwnersModel.getModel(this.change); + this.modelLoader = new ModelLoader(ownerService, model); + // Assign model after modelLoader, so modelLoader can be used in + // the loadPropertiesAfterModelChanged method + this.model = model; + } + } + + protected loadPropertiesAfterModelChanged() {} + } + + return Mixin as unknown as T & Constructor<CodeOwnersModelMixinInterface>; +};
diff --git a/web/code-owners-model.ts b/web/code-owners-model.ts new file mode 100644 index 0000000..38d0f78 --- /dev/null +++ b/web/code-owners-model.ts
@@ -0,0 +1,372 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {BehaviorSubject, Observable} from 'rxjs'; +import { + AccountInfo, + type ChangeInfo, +} from '@gerritcodereview/typescript-api/rest-api'; +import { + ChangeType, + CodeOwnerBranchConfigInfo, + FetchedFile, + FileCodeOwnerStatusInfo, + OwnerStatus, + OwnedPathsInfo, +} from './code-owners-api'; +import {deepEqual} from './deep-util'; +import {select} from './observable-util'; + +export enum SuggestionsState { + NotLoaded = 'NotLoaded', + Loaded = 'Loaded', + Loading = 'Loading', + LoadFailed = 'LoadFailed', +} + +export enum PluginState { + Enabled = 'Enabled', + Disabled = 'Disabled', + ServerConfigurationError = 'ServerConfigurationError', + Failed = 'Failed', +} + +export interface PluginStatus { + state: PluginState; + failedMessage?: string; +} + +export function isPluginErrorState(state: PluginState) { + return ( + state === PluginState.ServerConfigurationError || + state === PluginState.Failed + ); +} + +export enum SuggestionsType { + BEST_SUGGESTIONS = 'BEST_SUGGESTIONS', + ALL_SUGGESTIONS = 'ALL_SUGGESTIONS', +} + +/** + * @enum + */ +export enum UserRole { + ANONYMOUS = 'ANONYMOUS', + AUTHOR = 'AUTHOR', + CHANGE_OWNER = 'CHANGE_OWNER', + REVIEWER = 'REVIEWER', + CC = 'CC', + REMOVED_REVIEWER = 'REMOVED_REVIEWER', + OTHER = 'OTHER', +} + +export interface Suggestion { + files?: Array<FetchedFile>; + state: SuggestionsState; + loadProgress?: string; +} + +export interface Status { + patchsetNumber: number; + enabled: boolean; + codeOwnerStatusMap: Map<string, FileStatus>; + rawStatuses: Array<FileCodeOwnerStatusInfo>; + newerPatchsetUploaded: boolean; +} + +export interface FileStatus { + changeType: ChangeType; + status: OwnerStatus; + newPath?: string | null; + oldPath?: string | null; +} + +export const BestSuggestionsLimit = 5; +export const AllSuggestionsLimit = 1000; + +let codeOwnersModel: CodeOwnersModel | undefined; + +export interface OwnedPathsInfoOpt { + oldPaths: Set<string>; + newPaths: Set<string>; + oldPathOwners: Map<string, Array<AccountInfo>>; + newPathOwners: Map<string, Array<AccountInfo>>; +} + +export interface CodeOwnersState { + showSuggestions: boolean; + selectedSuggestionsType: SuggestionsType; + suggestionsByTypes: Map<SuggestionsType, Suggestion>; + pluginStatus?: PluginStatus; + branchConfig?: CodeOwnerBranchConfigInfo; + ownedPaths?: OwnedPathsInfoOpt; + userRole?: UserRole; + areAllFilesApproved?: boolean; + status?: Status; +} + +/** + * Maintain the state of code-owners. + * Raises 'model-property-changed' event when a property is changed. + * The plugin shares the same model between all UI elements (if it is not, + * the plugin can't maintain showSuggestions state across different UI + * elements). UI elements use values from this model to display information and + * listens for the model-property-changed event. To do so, UI elements add + * CodeOwnersModelMixin, which is doing the listening and the translation from + * model-property-changed event to Polymer property-changed-event. The + * translation allows to use model properties in observables, bindings, + * computed properties, etc... + * The CodeOwnersModelLoader updates the model. + * + * It would be good to use RxJs Observable for implementing model properties. + * However, RxJs library is imported by Gerrit and there is no + * good way to reuse the same library in the plugin. + */ +export class CodeOwnersModel extends EventTarget { + private subject$: BehaviorSubject<CodeOwnersState> = new BehaviorSubject({ + showSuggestions: false, + selectedSuggestionsType: SuggestionsType.BEST_SUGGESTIONS, + suggestionsByTypes: new Map(), + } as CodeOwnersState); + + public state$: Observable<CodeOwnersState> = this.subject$.asObservable(); + + public showSuggestions$ = select(this.state$, state => state.showSuggestions); + + public selectedSuggestionsType$ = select( + this.state$, + state => state.selectedSuggestionsType + ); + + public selectedSuggestionsFiles$ = select( + this.state$, + state => state.suggestionsByTypes.get(state.selectedSuggestionsType)?.files + ); + + public selectedSuggestionsState$ = select( + this.state$, + state => state.suggestionsByTypes.get(state.selectedSuggestionsType)!.state + ); + + public selectedSuggestionsLoadProgress$ = select( + this.state$, + state => + state.suggestionsByTypes.get(state.selectedSuggestionsType)!.loadProgress + ); + + public ownedPaths$ = select(this.state$, state => state.ownedPaths); + + constructor(readonly change: ChangeInfo) { + super(); + // Side-effectful initialization of state$ is ok because this happens + // during construction time. + const current = this.subject$.getValue(); + for (const suggestionType of Object.values(SuggestionsType)) { + current.suggestionsByTypes.set(suggestionType, { + files: undefined, + state: SuggestionsState.NotLoaded, + loadProgress: undefined, + }); + } + } + + get state() { + return this.subject$.getValue(); + } + + get selectedSuggestions() { + const current = this.subject$.getValue(); + return current.suggestionsByTypes.get(current.selectedSuggestionsType); + } + + getSuggestionsByType(suggestionsType: SuggestionsType) { + const current = this.subject$.getValue(); + return Object.freeze(current.suggestionsByTypes.get(suggestionsType)!); + } + + private setState(state: CodeOwnersState) { + this.subject$.next(Object.freeze(state)); + } + + setBranchConfig(branchConfig: CodeOwnerBranchConfigInfo) { + const current = this.subject$.getValue(); + if (current.branchConfig === branchConfig) return; + this.setState({...current, branchConfig}); + } + + setStatus(status: Status) { + const current = this.subject$.getValue(); + if (current.status === status) return; + this.setState({...current, status}); + } + + setUserRole(userRole: UserRole) { + const current = this.subject$.getValue(); + if (current.userRole === userRole) return; + this.setState({...current, userRole}); + } + + setAreAllFilesApproved(areAllFilesApproved: boolean) { + const current = this.subject$.getValue(); + if (current.areAllFilesApproved === areAllFilesApproved) return; + this.setState({...current, areAllFilesApproved}); + } + + setShowSuggestions(showSuggestions: boolean) { + const current = this.subject$.getValue(); + if (current.showSuggestions === showSuggestions) return; + this.setState({...current, showSuggestions}); + } + + setSelectedSuggestionType(selectedSuggestionsType: SuggestionsType) { + const current = this.subject$.getValue(); + if (current.selectedSuggestionsType === selectedSuggestionsType) return; + this.setState({...current, selectedSuggestionsType}); + } + + _setPluginStatus(pluginStatus: PluginStatus) { + const current = this.subject$.getValue(); + if (this._arePluginStatusesEqual(current.pluginStatus, pluginStatus)) + return; + this.setState({...current, pluginStatus}); + } + + private updateSuggestion( + suggestionsType: SuggestionsType, + suggestionsUpdate: Partial<Suggestion> + ) { + const current = this.subject$.getValue(); + const nextState = { + ...current, + suggestionsByTypes: new Map(current.suggestionsByTypes), + }; + nextState.suggestionsByTypes.set(suggestionsType, { + ...nextState.suggestionsByTypes.get(suggestionsType)!, + ...suggestionsUpdate, + }); + this.setState(nextState); + } + + setSuggestionsFiles( + suggestionsType: SuggestionsType, + files: Array<FetchedFile> + ) { + const current = this.subject$.getValue(); + const suggestions = current.suggestionsByTypes.get(suggestionsType); + if (!suggestions) return; + if (suggestions.files === files) return; + if (deepEqual(suggestions.files, files)) return; + this.updateSuggestion(suggestionsType, {files}); + } + + setSuggestionsState( + suggestionsType: SuggestionsType, + state: SuggestionsState + ) { + const current = this.subject$.getValue(); + const suggestions = current.suggestionsByTypes.get(suggestionsType); + if (!suggestions) return; + if (suggestions.state === state) return; + this.updateSuggestion(suggestionsType, {state}); + } + + setSuggestionsLoadProgress( + suggestionsType: SuggestionsType, + loadProgress: string + ) { + const current = this.subject$.getValue(); + const suggestions = current.suggestionsByTypes.get(suggestionsType); + if (!suggestions) return; + if (suggestions.loadProgress === loadProgress) return; + this.updateSuggestion(suggestionsType, {loadProgress}); + } + + setOwnedPaths(ownedPathsInfo: OwnedPathsInfo | undefined) { + const current = this.subject$.getValue(); + const ownedPaths = { + oldPaths: new Set<string>(), + newPaths: new Set<string>(), + oldPathOwners: new Map<string, Array<AccountInfo>>(), + newPathOwners: new Map<string, Array<AccountInfo>>(), + }; + for (const changed_file of ownedPathsInfo?.owned_changed_files ?? []) { + if (changed_file.old_path?.owned) + ownedPaths.oldPaths.add(changed_file.old_path.path); + if (changed_file.new_path?.owned) + ownedPaths.newPaths.add(changed_file.new_path.path); + if (changed_file.old_path?.owners) { + ownedPaths.oldPathOwners.set( + changed_file.old_path.path, + ( + ownedPaths.oldPathOwners.get(changed_file.old_path.path) ?? [] + ).concat(changed_file.old_path.owners) + ); + } + if (changed_file.new_path?.owners) { + ownedPaths.newPathOwners.set( + changed_file.new_path.path, + ( + ownedPaths.newPathOwners.get(changed_file.new_path.path) ?? [] + ).concat(changed_file.new_path.owners) + ); + } + } + const nextState = { + ...current, + ownedPaths, + }; + this.setState(nextState); + } + + setPluginEnabled(enabled: boolean) { + this._setPluginStatus({ + state: enabled ? PluginState.Enabled : PluginState.Disabled, + }); + } + + setServerConfigurationError(failedMessage: string) { + this._setPluginStatus({ + state: PluginState.ServerConfigurationError, + failedMessage, + }); + } + + setPluginFailed(failedMessage: string) { + this._setPluginStatus({state: PluginState.Failed, failedMessage}); + } + + _arePluginStatusesEqual( + status1: PluginStatus | undefined, + status2: PluginStatus | undefined + ) { + if (status1 === undefined || status2 === undefined) { + return status1 === status2; + } + if (status1.state !== status2.state) return false; + return isPluginErrorState(status1.state) + ? status1.failedMessage === status2.failedMessage + : true; + } + + static getModel(change: ChangeInfo) { + if (!codeOwnersModel || codeOwnersModel.change !== change) { + codeOwnersModel = new CodeOwnersModel(change); + } + return codeOwnersModel; + } +}
diff --git a/web/code-owners-service.ts b/web/code-owners-service.ts new file mode 100644 index 0000000..17b16ba --- /dev/null +++ b/web/code-owners-service.ts
@@ -0,0 +1,388 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SuggestionsType, + BestSuggestionsLimit, + AllSuggestionsLimit, + UserRole, + Status, + FileStatus, +} from './code-owners-model'; +import {OwnersProvider, FetchStatus} from './code-owners-fetcher'; +import { + CodeOwnersApi, + CodeOwnersCacheApi, + FetchedFile, + FetchedOwner, + FileCodeOwnerStatusInfo, + OwnerStatus, +} from './code-owners-api'; +import {RestPluginApi} from '@gerritcodereview/typescript-api/rest'; +import { + AccountDetailInfo, + AccountInfo, + ChangeInfo, +} from '@gerritcodereview/typescript-api/rest-api'; + +/** + * Specifies status for a change. The same as ChangeStatus enum in gerrit + * + * @enum + */ +const ChangeStatus = { + ABANDONED: 'ABANDONED', + MERGED: 'MERGED', + NEW: 'NEW', +}; + +let ownerService: CodeOwnerService | undefined; + +interface CodeOwnerServiceOptions { + maxConcurrentRequests?: number; +} + +function noAwait(_promise: Promise<unknown>) {} + +/** + * Service for the data layer used in the plugin UI. + */ +export class CodeOwnerService { + private codeOwnersCacheApi: CodeOwnersCacheApi; + + private ownersProviders: Map<SuggestionsType, OwnersProvider>; + + constructor( + readonly restApi: RestPluginApi, + readonly change: ChangeInfo, + options: CodeOwnerServiceOptions = {} + ) { + const codeOwnersApi = new CodeOwnersApi(restApi); + this.codeOwnersCacheApi = new CodeOwnersCacheApi(codeOwnersApi, change); + + const providerOptions = { + maxConcurrentRequests: options.maxConcurrentRequests || 10, + }; + this.ownersProviders = new Map(); + this.ownersProviders.set( + SuggestionsType.BEST_SUGGESTIONS, + new OwnersProvider(restApi, change, { + ...providerOptions, + ownersLimit: BestSuggestionsLimit, + }) + ); + this.ownersProviders.set( + SuggestionsType.ALL_SUGGESTIONS, + new OwnersProvider(restApi, change, { + ...providerOptions, + ownersLimit: AllSuggestionsLimit, + }) + ); + } + + /** + * Fetch the account. + */ + getAccount(): Promise<AccountDetailInfo | undefined> { + return this.codeOwnersCacheApi.getAccount(); + } + + /** + * Prefetch data + */ + async prefetch() { + try { + await Promise.all([this.getAccount(), this.getStatus()]); + } catch { + // Ignore any errors during prefetch. + // The same call from a different place throws the same exception + // again. The CodeOwnerService is not responsible for error processing. + } + } + + /** + * Returns the role of the current user. The returned value reflects the + * role of the user at the time when the change is loaded. + * For example, if a user removes themselves as a reviewer, the returned + * role 'REVIEWER' remains unchanged until the change view is reloaded. + */ + async getLoggedInUserInitialRole(): Promise<UserRole> { + const account = await this.getAccount(); + if (!account) { + return UserRole.ANONYMOUS; + } + const change = this.change; + if ( + change.revisions && + change.current_revision && + change.revisions[change.current_revision] + ) { + const commit = change.revisions[change.current_revision].commit; + if ( + commit && + commit.author && + account.email && + commit.author.email === account.email + ) { + return UserRole.AUTHOR; + } + } + if (change.owner._account_id === account._account_id) { + return UserRole.CHANGE_OWNER; + } + if (change.reviewers) { + if (this.accountInReviewers(change.reviewers.REVIEWER, account)) { + return UserRole.REVIEWER; + } else if (this.accountInReviewers(change.reviewers.CC, account)) { + return UserRole.CC; + } else if (this.accountInReviewers(change.reviewers.REMOVED, account)) { + return UserRole.REMOVED_REVIEWER; + } + } + return UserRole.OTHER; + } + + private accountInReviewers( + reviewers: AccountInfo[] | undefined, + account: AccountDetailInfo + ) { + if (!reviewers) { + return false; + } + return reviewers.some( + reviewer => reviewer._account_id === account._account_id + ); + } + + async getStatus(): Promise<Status> { + const status = await this.getStatusImpl(); + if (status.enabled && this.isOnOlderPatchset(status.patchsetNumber)) { + // status is returned for an older patchset. Abort, re-init and refetch + // new status - it is expected, that after several retry a status + // for the newest patchset is returned + this.reset(); + noAwait(this.prefetch()); + return await this.getStatus(); + } + return status; + } + + private async getStatusImpl() { + const enabled = await this.isCodeOwnerEnabled(); + if (!enabled) { + return { + patchsetNumber: 0, + enabled: false, + codeOwnerStatusMap: new Map<string, FileStatus>(), + rawStatuses: [], + newerPatchsetUploaded: false, + }; + } + + const ownerStatus = await this.codeOwnersCacheApi.listOwnerStatus(); + + return { + enabled: true, + patchsetNumber: ownerStatus.patch_set_number, + codeOwnerStatusMap: this.formatStatuses( + ownerStatus.file_code_owner_statuses + ), + rawStatuses: ownerStatus.file_code_owner_statuses, + newerPatchsetUploaded: this.isOnNewerPatchset( + ownerStatus.patch_set_number + ), + }; + } + + async areAllFilesApproved() { + const {rawStatuses} = await this.getStatus(); + return !rawStatuses.some(status => { + const oldPathStatus = status.old_path_status; + const newPathStatus = status.new_path_status; + // For deleted files, no new_path_status exists + return ( + (newPathStatus && newPathStatus.status !== OwnerStatus.APPROVED) || + (oldPathStatus && oldPathStatus.status !== OwnerStatus.APPROVED) + ); + }); + } + + private ownersProvider(suggestionsType: SuggestionsType) { + return this.ownersProviders.get(suggestionsType)!; + } + + /** + * Gets which files are owned by the given user. + */ + async getOwnedPaths() { + const enabled = await this.isCodeOwnerEnabled(); + if (!enabled) { + return Promise.resolve(undefined); + } + return this.codeOwnersCacheApi.listOwnedPaths(); + } + + /** + * Gets owner suggestions. + */ + async getSuggestedOwners(suggestionsType: SuggestionsType) { + const {codeOwnerStatusMap} = await this.getStatus(); + const ownersProvider = this.ownersProvider(suggestionsType); + + await ownersProvider!.fetchSuggestedOwners(codeOwnerStatusMap); + + return { + finished: ownersProvider.getStatus() === FetchStatus.FINISHED, + status: ownersProvider.getStatus(), + progress: ownersProvider.getProgressString(), + files: this.getFilesWithStatuses( + codeOwnerStatusMap, + ownersProvider.getFiles() + ), + }; + } + + async getSuggestedOwnersProgress(suggestionsType: SuggestionsType) { + const {codeOwnerStatusMap} = await this.getStatus(); + const ownersProvider = this.ownersProvider(suggestionsType); + return { + finished: ownersProvider.getStatus() === FetchStatus.FINISHED, + status: ownersProvider.getStatus(), + progress: ownersProvider.getProgressString(), + files: this.getFilesWithStatuses( + codeOwnerStatusMap, + ownersProvider.getFiles() + ), + }; + } + + pauseSuggestedOwnersLoading(suggestionsType: SuggestionsType) { + this.ownersProvider(suggestionsType).pause(); + } + + resumeSuggestedOwnersLoading(suggestionsType: SuggestionsType) { + this.ownersProvider(suggestionsType).resume(); + } + + private formatStatuses(statuses?: Array<FileCodeOwnerStatusInfo>) { + // convert the array of statuses to map between file path -> status + return (statuses ?? []).reduce((prev, cur) => { + const newPathStatus = cur.new_path_status; + const oldPathStatus = cur.old_path_status; + if (oldPathStatus) { + prev.set(oldPathStatus.path, { + changeType: cur.change_type, + status: oldPathStatus.status, + newPath: newPathStatus ? newPathStatus.path : null, + }); + } + if (newPathStatus) { + prev.set(newPathStatus.path, { + changeType: cur.change_type, + status: newPathStatus.status, + oldPath: oldPathStatus ? oldPathStatus.path : null, + }); + } + return prev; + }, new Map<string, FileStatus>()); + } + + private computeFileStatus( + fileStatusMap: Map<string, FileStatus>, + path: string + ) { + // empty for modified files and old-name files + // Show `Renamed` for renamed file + const status = fileStatusMap.get(path); + if (status && status.oldPath) { + return 'Renamed'; + } + return; + } + + private getFilesWithStatuses( + codeOwnerStatusMap: Map<string, FileStatus>, + files: Array<{ + path: string; + info: FetchedOwner; + }> + ): Array<FetchedFile> { + return files.map(file => { + return { + path: file.path, + info: file.info, + status: this.computeFileStatus(codeOwnerStatusMap, file.path), + }; + }); + } + + private isOnNewerPatchset(patchsetId: number) { + if (this.change.current_revision === undefined) return false; + const latestRevision = this.change.revisions![this.change.current_revision]; + return patchsetId > latestRevision._number; + } + + private isOnOlderPatchset(patchsetId: number) { + if (this.change.current_revision === undefined) return false; + const latestRevision = this.change.revisions![this.change.current_revision]; + return patchsetId < latestRevision._number; + } + + reset() { + for (const provider of Object.values(this.ownersProviders)) { + provider.reset(); + } + const codeOwnersApi = new CodeOwnersApi(this.restApi); + this.codeOwnersCacheApi = new CodeOwnersCacheApi( + codeOwnersApi, + this.change + ); + } + + async getBranchConfig() { + return this.codeOwnersCacheApi.getBranchConfig(); + } + + async isCodeOwnerEnabled() { + if ( + this.change.status === ChangeStatus.ABANDONED || + this.change.status === ChangeStatus.MERGED + ) { + return false; + } + const config = await this.getBranchConfig(); + return config && !config.disabled; + } + + static getOwnerService(restApi: RestPluginApi, change: ChangeInfo) { + if (!ownerService || ownerService.change !== change) { + ownerService = new CodeOwnerService(restApi, change, { + // Chrome has a limit of 6 connections per host name, and a max of 10 connections. + maxConcurrentRequests: 6, + }); + ownerService.prefetch(); + } + return ownerService; + } + + // Only used for tests + static reset() { + if (!ownerService) return; + ownerService.reset(); + ownerService = undefined; + } +}
diff --git a/web/code-owners-service_test.ts b/web/code-owners-service_test.ts new file mode 100644 index 0000000..e9abbb1 --- /dev/null +++ b/web/code-owners-service_test.ts
@@ -0,0 +1,258 @@ +import {CodeOwnerService} from './code-owners-service.js'; +import { + RequestPayload, + RestPluginApi, +} from '@gerritcodereview/typescript-api/rest.js'; +import { + ChangeInfo, + HttpMethod, +} from '@gerritcodereview/typescript-api/rest-api.js'; +import {SuggestionsType} from './code-owners-model.js'; +import {assert} from '@open-wc/testing'; + +function flush() { + return new Promise((resolve, _reject) => { + setTimeout(resolve, 0); + }); +} + +suite('code owners service tests', () => { + let codeOwnersService: CodeOwnerService; + const fakeRestApi = { + send( + _method: HttpMethod, + _url: string, + _payload?: RequestPayload, + _errFn?: ErrorCallback, + _contentType?: string + ) { + return Promise.resolve({}); + }, + getLoggedIn() { + return Promise.resolve(true); + }, + getAccount() { + return Promise.resolve({email: 'someone@google.com'}); + }, + } as unknown as RestPluginApi; + + const fakeStatus = { + // fake data with fake files + patch_set_number: 1, + file_code_owner_statuses: [ + { + new_path_status: { + path: 'a.js', + status: 'INSUFFICIENT_REVIEWERS', + }, + }, + { + new_path_status: { + path: 'b.js', + status: 'INSUFFICIENT_REVIEWERS', + }, + }, + { + new_path_status: { + path: 'c.ts', + status: 'INSUFFICIENT_REVIEWERS', + }, + }, + { + change_type: 'RENAMED', + old_path_status: { + path: 'd.ts', + status: 'INSUFFICIENT_REVIEWERS', + }, + new_path_status: { + path: 'd_edit.ts', + status: 'INSUFFICIENT_REVIEWERS', + }, + }, + { + new_path_status: { + path: 'e.ts', + status: 'PENDING', + }, + }, + { + new_path_status: { + path: 'f.ts', + status: 'PENDING', + }, + }, + { + new_path_status: { + path: 'g.ts', + status: 'PENDING', + }, + }, + ], + }; + const fakeChange = { + project: 'test', + branch: 'main', + _number: '123', + revisions: { + a: { + _number: 1, + }, + }, + current_revision: 'a', + } as unknown as ChangeInfo; + + let getApiStub: sinon.SinonStub; + + setup(() => { + getApiStub = sinon.stub(fakeRestApi, 'send'); + getApiStub.returns(Promise.resolve({})); + }); + + teardown(() => { + getApiStub.reset(); + CodeOwnerService.reset(); + sinon.restore(); + }); + + suite('basic api request tests', () => { + setup(async () => { + getApiStub + .withArgs( + sinon.match.any, + `/changes/${fakeChange._number}/code_owners.status?limit=100000`, + sinon.match.any, + sinon.match.any + ) + .returns(Promise.resolve(fakeStatus)); + codeOwnersService = CodeOwnerService.getOwnerService(fakeRestApi, { + ...fakeChange, + }); + await flush(); + }); + + test('getOwnerService - same change returns the same instance', () => { + assert.equal( + CodeOwnerService.getOwnerService(fakeRestApi, fakeChange), + CodeOwnerService.getOwnerService(fakeRestApi, fakeChange) + ); + }); + + test('return a new instance when change changed', () => { + assert.notEqual( + CodeOwnerService.getOwnerService(fakeRestApi, {...fakeChange}), + CodeOwnerService.getOwnerService(fakeRestApi, {...fakeChange}) + ); + }); + + test('should fetch status after init', () => { + assert.isTrue(getApiStub.called); + assert.equal( + getApiStub.lastCall.args[1], + `/changes/${fakeChange._number}/code_owners.status?limit=100000` + ); + }); + + test('getSuggestion should kickoff the fetch', async () => { + assert.equal(getApiStub.callCount, 2); + getApiStub.resetHistory(); + await codeOwnersService.getSuggestedOwners( + SuggestionsType.ALL_SUGGESTIONS + ); + // 8 requests for getting file owners + assert.equal(getApiStub.callCount, 8); + }); + + test('approved status calculation', async () => { + const approved = await codeOwnersService.areAllFilesApproved(); + assert.equal(approved, false); + }); + }); + + suite('getOwnedPaths', () => { + async function setupBranchConfig(disabled: boolean | undefined) { + getApiStub + .withArgs( + sinon.match.any, + `/projects/${fakeChange.project}/branches/` + + `${fakeChange.branch}/code_owners.branch_config`, + sinon.match.any, + sinon.match.any + ) + .returns(Promise.resolve({disabled})); + codeOwnersService = CodeOwnerService.getOwnerService(fakeRestApi, { + ...fakeChange, + }); + await flush(); + getApiStub.resetHistory(); + } + test('should not fetch if disabled', async () => { + await setupBranchConfig(true); + + await codeOwnersService.getOwnedPaths(); + assert.equal(getApiStub.callCount, 0); + }); + + test('should fetch if enabled', async () => { + await setupBranchConfig(false); + + await codeOwnersService.getOwnedPaths(); + assert.equal(getApiStub.callCount, 1); + }); + + test('should fetch if enabled by default', async () => { + await setupBranchConfig(undefined); + + await codeOwnersService.getOwnedPaths(); + assert.equal(getApiStub.callCount, 1); + }); + }); + + suite('all approved case', () => { + setup(async () => { + getApiStub + .withArgs( + sinon.match.any, + `/changes/${fakeChange._number}/code_owners.status?limit=100000`, + sinon.match.any, + sinon.match.any + ) + .returns( + Promise.resolve({ + // fake data with fake files + patch_set_number: 1, + file_code_owner_statuses: [ + { + new_path_status: { + path: 'a.js', + status: 'APPROVED', + }, + }, + { + new_path_status: { + path: 'b.js', + status: 'APPROVED', + }, + }, + { + old_path_status: { + path: 'd.js', + status: 'APPROVED', + }, + change_type: 'DELETED', + }, + ], + }) + ); + + codeOwnersService = CodeOwnerService.getOwnerService(fakeRestApi, { + ...fakeChange, + }); + await flush(); + }); + + test('approved status calculation', async () => { + const approved = await codeOwnersService.areAllFilesApproved(); + assert.equal(approved, true); + }); + }); +});
diff --git a/web/deep-util.ts b/web/deep-util.ts new file mode 100644 index 0000000..fbe1400 --- /dev/null +++ b/web/deep-util.ts
@@ -0,0 +1,45 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export function deepEqual<T>(a: T, b: T): boolean { + if (a === b) return true; + if (a === undefined || b === undefined) return false; + if (a === null || b === null) return false; + if (a instanceof Date || b instanceof Date) { + if (!(a instanceof Date && b instanceof Date)) return false; + return a.getTime() === b.getTime(); + } + + if (a instanceof Set || b instanceof Set) { + if (!(a instanceof Set && b instanceof Set)) return false; + if (a.size !== b.size) return false; + for (const ai of a) if (!b.has(ai)) return false; + return true; + } + if (a instanceof Map || b instanceof Map) { + if (!(a instanceof Map && b instanceof Map)) return false; + if (a.size !== b.size) return false; + for (const [aKey, aValue] of a.entries()) { + if (!b.has(aKey) || !deepEqual(aValue, b.get(aKey))) return false; + } + return true; + } + + if (typeof a === 'object') { + if (typeof b !== 'object') return false; + const aObj = a as Record<string, unknown>; + const bObj = b as Record<string, unknown>; + const aKeys = Object.keys(aObj); + const bKeys = Object.keys(bObj); + if (aKeys.length !== bKeys.length) return false; + for (const key of aKeys) { + if (!deepEqual(aObj[key], bObj[key])) return false; + } + return true; + } + + return false; +}
diff --git a/web/karma_test.sh b/web/karma_test.sh new file mode 100755 index 0000000..fd686da --- /dev/null +++ b/web/karma_test.sh
@@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail +./$1 start $2 --single-run \ + --root 'plugins/code-owners/web/_bazel_ts_out_tests/' \ + --test-files '*_test.js' \ + --browsers ${3:-ChromeHeadless}
diff --git a/web/observable-util.ts b/web/observable-util.ts new file mode 100644 index 0000000..9a03455 --- /dev/null +++ b/web/observable-util.ts
@@ -0,0 +1,16 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import {Observable} from 'rxjs'; +import {distinctUntilChanged, map, shareReplay} from 'rxjs/operators'; +import {deepEqual} from './deep-util'; + +export function select<A, B>(obs$: Observable<A>, mapper: (_: A) => B) { + return obs$.pipe( + map(mapper), + distinctUntilChanged(deepEqual), + shareReplay(1) + ); +}
diff --git a/web/owner-requirement.ts b/web/owner-requirement.ts new file mode 100644 index 0000000..a1cc7a8 --- /dev/null +++ b/web/owner-requirement.ts
@@ -0,0 +1,277 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {CodeOwnersModelMixin} from './code-owners-model-mixin'; +import {showPluginFailedMessage} from './code-owners-banner'; +import {isPluginErrorState, UserRole} from './code-owners-model'; +import {css, html, LitElement} from 'lit'; +import {customElement} from 'lit/decorators'; +import {when} from 'lit/directives/when'; +import { + ApprovalInfo, + DetailedLabelInfo, +} from '@gerritcodereview/typescript-api/rest-api'; +import {OwnerStatus} from './code-owners-api'; + +const base = CodeOwnersModelMixin(LitElement); + +export const OWNER_REQUIREMENT_VALUE = 'owner-requirement-value'; +/** + * Owner requirement control for `submit-requirement-item-code-owners` endpoint. + * + * This will show the status and suggest owners button next to + * the code-owners submit requirement. + */ +@customElement(OWNER_REQUIREMENT_VALUE) +export class OwnerRequirementValue extends base { + static override get styles() { + return [ + css` + :host { + --gr-button: { + padding: 0px; + } + } + p.loading { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + } + .loadingSpin { + display: inline-block; + margin-right: var(--spacing-m); + width: 18px; + height: 18px; + } + gr-button { + padding-left: var(--spacing-m); + } + gr-button::part(paper-button) { + padding: 0 var(--spacing-s); + } + a { + text-decoration: none; + } + `, + ]; + } + + override render() { + // Compute whether plugin failed first because it might mean some of the + // model parameters are not set which would result in a loading screen. + if (this.pluginFailed()) { + return html` + <span>Code-owners plugin has failed</span> + <gr-button link @click=${this.showFailDetails}>Details</gr-button> + `; + } + if (!this.branchConfig || !this.status || !this.userRole) { + return html` + <p class="loading"> + <span class="loadingSpin"></span> + Loading status ... + </p> + `; + } + + if (this.status.newerPatchsetUploaded) { + return html`<span>A newer patch set has been uploaded.</span>`; + } + const overrideInfoUrl = this.computeOverrideInfoUrl(); + const statusCount = this.getStatusCount(); + this.reporting?.reportLifeCycle('owners-submit-requirement-summary-shown', { + ...statusCount, + user_role: this.userRole, + }); + return html` + <span>${this.computeStatusText(statusCount)}</span> + ${when( + !!overrideInfoUrl, + () => html` + <a + @click=${this.reportDocClick} + href=${overrideInfoUrl} + target="_blank" + > + <gr-icon + icon="help" + title="Documentation for overriding code owners" + ></gr-icon> + </a> + ` + )} + ${when( + this.computeIsSignedInUser(this.userRole), + () => html` + <gr-button link @click=${this.openReplyDialog}> + ${this.getSuggestOwnersText(statusCount)} + </gr-button> + ` + )} + `; + } + + override loadPropertiesAfterModelChanged() { + super.loadPropertiesAfterModelChanged(); + this.reporting?.reportLifeCycle('owners-submit-requirement-summary-start'); + this.modelLoader?.loadBranchConfig(); + this.modelLoader?.loadStatus(); + this.modelLoader?.loadUserRole(); + } + + private computeIsSignedInUser(userRole: UserRole) { + return userRole && userRole !== UserRole.ANONYMOUS; + } + + private pluginFailed() { + return this.pluginStatus && isPluginErrorState(this.pluginStatus.state); + } + + private computeOverrideInfoUrl() { + if (!this.branchConfig) { + return ''; + } + return this.branchConfig.general && + this.branchConfig.general.override_info_url + ? this.branchConfig.general.override_info_url + : ''; + } + + private computeIsOverriden() { + if ( + !this.change || + !this.branchConfig || + !this.branchConfig['override_approval'] + ) { + // no override labels configured + return false; + } + + for (const requiredApprovalInfo of this.branchConfig['override_approval']) { + const overridenLabel = requiredApprovalInfo.label; + const overridenValue = Number(requiredApprovalInfo.value); + if (isNaN(overridenValue)) continue; + + if (this.change.labels?.[overridenLabel]) { + const votes = + (this.change.labels[overridenLabel] as DetailedLabelInfo).all || []; + if ( + votes.find((v: ApprovalInfo) => Number(v.value) >= overridenValue) + ) { + return true; + } + } + } + + // otherwise always reset it to false + return false; + } + + private getSuggestOwnersText(statusCount: { + missing: number; + pending: number; + approved: number; + }) { + return statusCount && statusCount.missing === 0 + ? 'Add owners' + : 'Suggest owners'; + } + + private getStatusCount() { + return (this.status?.rawStatuses ?? []).reduce( + (prev, cur) => { + const oldPathStatus = cur.old_path_status; + const newPathStatus = cur.new_path_status; + if (newPathStatus && this.isMissing(newPathStatus.status)) { + prev.missing++; + } else if (newPathStatus && this.isPending(newPathStatus.status)) { + prev.pending++; + } else if (oldPathStatus) { + // check oldPath if newPath approved or the file is deleted + if (this.isMissing(oldPathStatus.status)) { + prev.missing++; + } else if (this.isPending(oldPathStatus.status)) { + prev.pending++; + } + } else { + prev.approved++; + } + return prev; + }, + {missing: 0, pending: 0, approved: 0} + ); + } + + private computeStatusText(statusCount: { + missing: number; + pending: number; + approved: number; + }) { + if (this.model === undefined || this.change === undefined) return ''; + const isOverriden = this.computeIsOverriden(); + const statusText = []; + if (statusCount.missing) { + statusText.push(`${statusCount.missing} missing`); + } + + if (statusCount.pending) { + statusText.push(`${statusCount.pending} pending`); + } + + if (!statusText.length) { + statusText.push(isOverriden ? 'Approved (Owners-Override)' : 'Approved'); + } + + return statusText.join(', '); + } + + private isMissing(status: OwnerStatus | undefined) { + return status === OwnerStatus.INSUFFICIENT_REVIEWERS; + } + + private isPending(status: OwnerStatus | undefined) { + return status === OwnerStatus.PENDING; + } + + private openReplyDialog() { + this.model!.setShowSuggestions(true); + this.dispatchEvent( + new CustomEvent('open-reply-dialog', { + detail: {}, + composed: true, + bubbles: true, + }) + ); + this.reporting?.reportInteraction( + 'suggest-owners-from-submit-requirement', + { + user_role: this.userRole, + } + ); + } + + private reportDocClick() { + this.reporting?.reportInteraction('code-owners-doc-click', { + section: 'no owners found', + }); + } + + private showFailDetails() { + showPluginFailedMessage(this, this.pluginStatus!); + } +}
diff --git a/web/owner-status-column.ts b/web/owner-status-column.ts new file mode 100644 index 0000000..d8b9f8f --- /dev/null +++ b/web/owner-status-column.ts
@@ -0,0 +1,387 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AccountInfo, + BasePatchSetNum, + RevisionPatchSetNum, +} from '@gerritcodereview/typescript-api/rest-api'; +import {css, html, LitElement, nothing, PropertyValues} from 'lit'; +import {customElement, property} from 'lit/decorators'; +import {OwnerStatus} from './code-owners-api'; +import {FileStatus} from './code-owners-model'; +import {CodeOwnersModelMixin} from './code-owners-model-mixin'; + +// TODO: Extend the API for plugins. +export interface PatchRange { + patchNum: RevisionPatchSetNum; + basePatchNum: BasePatchSetNum; +} + +const MAGIC_FILES = ['/COMMIT_MSG', '/MERGE_LIST', '/PATCHSET_LEVEL']; +const STATUS_CODE = { + NO_STATUS: 'no-status', + PENDING: 'pending', + PENDING_OLD_PATH: 'pending-old-path', + MISSING: 'missing', + MISSING_OLD_PATH: 'missing-old-path', + APPROVED: 'approved', +}; + +const STATUS_PRIORITY_ORDER = [ + STATUS_CODE.NO_STATUS, + STATUS_CODE.MISSING, + STATUS_CODE.PENDING, + STATUS_CODE.MISSING_OLD_PATH, + STATUS_CODE.PENDING_OLD_PATH, + STATUS_CODE.APPROVED, +]; + +const STATUS_ICON = { + [STATUS_CODE.PENDING]: 'schedule', + [STATUS_CODE.MISSING]: 'close', + [STATUS_CODE.PENDING_OLD_PATH]: 'schedule', + [STATUS_CODE.MISSING_OLD_PATH]: 'close', + [STATUS_CODE.APPROVED]: 'check', + [STATUS_CODE.NO_STATUS]: 'check_circle', +}; + +const STATUS_SUMMARY = { + [STATUS_CODE.PENDING]: 'Pending', + [STATUS_CODE.MISSING]: 'Missing', + [STATUS_CODE.PENDING_OLD_PATH]: 'Pending Old Path', + [STATUS_CODE.MISSING_OLD_PATH]: 'Missing Old Path', + [STATUS_CODE.APPROVED]: 'Approved', + [STATUS_CODE.NO_STATUS]: 'Does not need approval', +}; + +const STATUS_TOOLTIP = { + [STATUS_CODE.PENDING]: 'Pending code owner approval', + [STATUS_CODE.MISSING]: 'Missing code owner approval', + [STATUS_CODE.PENDING_OLD_PATH]: + 'Pending code owner approval on pre-renamed file', + [STATUS_CODE.MISSING_OLD_PATH]: + 'Missing code owner approval on pre-renamed file', + [STATUS_CODE.APPROVED]: 'Approved by code owner', + [STATUS_CODE.NO_STATUS]: 'Does not need approval', +}; + +export function hasPath(ownedPaths: Set<string>, path: string | undefined) { + if (!path) return false; + if (path.charAt(0) !== '/') path = '/' + path; + return ownedPaths.has(path); +} + +export function getOwners( + owners: Map<string, Array<AccountInfo>>, + path: string | undefined +): Array<AccountInfo> { + if (!path) return []; + if (path.charAt(0) !== '/') path = '/' + path; + return owners.get(path) ?? []; +} + +export function uniqueAccountId( + account: AccountInfo, + index: number, + accountArray: AccountInfo[] +) { + return ( + index === + accountArray.findIndex(other => account._account_id === other._account_id) + ); +} + +const base = CodeOwnersModelMixin(LitElement); + +class BaseEl extends base { + @property({type: Object}) + patchRange?: PatchRange; + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + this.hidden = this.computeHidden(); + } + + computeHidden() { + const newerPatchsetUploaded = this.status?.newerPatchsetUploaded; + if ( + this.change === undefined || + this.patchRange === undefined || + newerPatchsetUploaded === undefined + ) { + return true; + } + if (this.change.status === 'MERGED') return true; + // if code-owners is not a submit requirement, don't show status column + if ( + this.change.requirements && + !this.change.requirements.find(r => r.type === 'code-owners') + ) { + return false; + } + + if (newerPatchsetUploaded) return true; + + const latestPatchset = + this.change.revisions![this.change.current_revision!]; + // Note: in some special cases, patchNum is undefined on latest patchset + // like after publishing the edit, still show for them + // TODO: this should be fixed in Gerrit + if (this.patchRange?.patchNum === undefined) return false; + // only show if its latest patchset + if (`${this.patchRange.patchNum}` !== `${latestPatchset._number}`) + return true; + return false; + } +} + +export const OWNERS_STATUS_COLUMN_HEADER = 'owner-status-column-header'; +/** + * Column header element for owner status. + */ +@customElement(OWNERS_STATUS_COLUMN_HEADER) +export class OwnerStatusColumnHeader extends BaseEl { + static override get styles() { + return [ + css` + :host() { + display: block; + padding-right: var(--spacing-m); + width: 5em; + } + :host[hidden] { + display: none; + } + `, + ]; + } + + override render() { + if (this.computeHidden()) return nothing; + return html`<div>Owners</div>`; + } +} + +export const OWNER_STATUS_COLUMN_CONTENT = 'owner-status-column-content'; +/** + * Row content element for owner status. + */ +@customElement(OWNER_STATUS_COLUMN_CONTENT) +export class OwnerStatusColumnContent extends BaseEl { + @property({type: String}) + path?: string; + + @property({type: String}) + oldPath?: string; + + @property({type: Array}) + cleanlyMergedPaths?: Array<string>; + + @property({type: Array}) + cleanlyMergedOldPaths?: Array<string>; + + @property({type: String, reflect: true, attribute: 'owner-status'}) + ownerStatus?: string; + + static override get styles() { + return [ + css` + :host { + display: flex; + padding-right: var(--spacing-m); + width: 5em; + text-align: center; + } + :host[hidden] { + display: none; + } + gr-icon { + padding: var(--spacing-xs) 0px; + } + :host([owner-status='approved']) gr-icon.status { + color: var(--positive-green-text-color); + } + :host([owner-status='pending']) gr-icon.status { + color: #ffa62f; + } + :host([owner-status='missing']) gr-icon.status { + color: var(--negative-red-text-color); + } + gr-avatar-stack { + padding: var(--spacing-xs) 0px; + display: flex; + --avatar-size: 20px; + } + .ellipsis { + /* These are required to get the ... to line up with the bottom of + the avatar icons. */ + margin-bottom: -2px; + display: flex; + align-items: flex-end; + } + .error { + color: var(--negative-red-text-color); + } + .fallback-icon { + /* Undo the padding for the gr-avatar-stack in case of fallback */ + padding: calc(-1 * var(--spacing-xs)) 0px; + } + `, + ]; + } + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + this.computeStatus(); + } + + override render() { + if ( + this.computeHidden() || + this.status === undefined || + !this.path || + MAGIC_FILES.includes(this.path) + ) + return nothing; + return html`${this.renderStatus()}${this.renderOwnership()}`; + } + + private renderStatus() { + const info = STATUS_TOOLTIP[this.ownerStatus!]; + const summary = STATUS_SUMMARY[this.ownerStatus!]; + const icon = STATUS_ICON[this.ownerStatus!]; + return html` + <gr-tooltip-content + title=${info} + aria-label=${summary} + aria-description=${info} + has-tooltip + > + <gr-icon class="status" icon=${icon} aria-hidden="true"></gr-icon> + </gr-tooltip-content> + `; + } + + private renderOwnership() { + if (this.isOwned()) { + return html` + <gr-tooltip-content + title="You are in OWNERS for this file" + aria-label="owned" + aria-description="You are an owner of this file" + has-tooltip + > + <gr-icon filled icon="policy" aria-hidden="true"></gr-icon> + </gr-tooltip-content> + `; + } else if (this.ownedPaths) { + let oldOwners = getOwners(this.ownedPaths.oldPathOwners, this.oldPath); + const newOwners = getOwners(this.ownedPaths.newPathOwners, this.path); + if (this.oldPath === undefined || this.oldPath === null) { + // In case of a file deletion, the Gerrit FE gives 'path' but not 'oldPath' + // but code-owners considers a deleted file an oldpath so check the oldpath owners. + oldOwners = getOwners(this.ownedPaths.oldPathOwners, this.path); + } + const allOwners = oldOwners.concat(newOwners).filter(uniqueAccountId); + + return html` <gr-avatar-stack + .accounts=${allOwners.slice(0, 3)} + .forceFetch=${true} + > + <gr-tooltip-content + slot="fallback" + title="No reviewer owns this file" + aria-label="missing owner" + aria-description="No reviewer owns this file" + has-tooltip + > + <gr-icon icon="help" class="error fallback-icon"></gr-icon> + </gr-tooltip-content> + </gr-avatar-stack> + ${allOwners.length > 3 + ? html`<div class="ellipsis">…</div>` + : nothing}`; + } + return nothing; + } + + private isOwned() { + if (!this.ownedPaths) return false; + if ( + hasPath(this.ownedPaths.newPaths, this.path) || + hasPath(this.ownedPaths.oldPaths, this.oldPath) || + // In case of deletions, the FE gives a path, but code-owners + // computes this as being part of the old path. + ((this.oldPath === undefined || this.oldPath === null) && + hasPath(this.ownedPaths.oldPaths, this.path)) + ) + return true; + return false; + } + + override loadPropertiesAfterModelChanged() { + super.loadPropertiesAfterModelChanged(); + this.modelLoader?.loadStatus(); + this.modelLoader?.loadOwnedPaths(); + } + + private computeStatus() { + if ( + this.status === undefined || + (this.cleanlyMergedPaths === undefined && + (this.path === undefined || this.oldPath === undefined)) + ) { + return; + } + + const codeOwnerStatusMap = this.status.codeOwnerStatusMap; + const paths = + this.path === undefined ? this.cleanlyMergedPaths : [this.path]; + const oldPaths = + this.oldPath === undefined ? this.cleanlyMergedOldPaths : [this.oldPath]; + + const statuses = (paths ?? []) + .filter(path => !MAGIC_FILES.includes(path)) + .map(path => this.extractStatus(codeOwnerStatusMap.get(path)!, false)); + // oldPath may contain null, so filter that as well. + const oldStatuses = (oldPaths ?? []) + .filter(path => !MAGIC_FILES.includes(path) && !!path) + .map(path => this.extractStatus(codeOwnerStatusMap.get(path)!, true)); + const allStatuses = statuses.concat(oldStatuses); + if (allStatuses.length === 0) { + return; + } + this.ownerStatus = allStatuses.reduce((a, b) => + STATUS_PRIORITY_ORDER.indexOf(a) < STATUS_PRIORITY_ORDER.indexOf(b) + ? a + : b + ); + } + + private extractStatus(statusItem: FileStatus, oldPath: boolean) { + if (statusItem === undefined) { + return STATUS_CODE.NO_STATUS; + } else if (statusItem.status === OwnerStatus.INSUFFICIENT_REVIEWERS) { + return oldPath ? STATUS_CODE.MISSING_OLD_PATH : STATUS_CODE.MISSING; + } else if (statusItem.status === OwnerStatus.PENDING) { + return oldPath ? STATUS_CODE.PENDING_OLD_PATH : STATUS_CODE.PENDING; + } else { + return STATUS_CODE.APPROVED; + } + } +}
diff --git a/web/owner-status-column_test.ts b/web/owner-status-column_test.ts new file mode 100644 index 0000000..2fd223a --- /dev/null +++ b/web/owner-status-column_test.ts
@@ -0,0 +1,10 @@ +import {assert} from '@open-wc/testing'; + +import {hasPath} from './owner-status-column'; + +suite('owner-status-column', () => { + test('hasPath', () => { + assert(hasPath(new Set(['/COMMIT']), '/COMMIT')); + assert(hasPath(new Set(['/some/file']), 'some/file')); + }); +});
diff --git a/web/plugin.ts b/web/plugin.ts new file mode 100644 index 0000000..9270544 --- /dev/null +++ b/web/plugin.ts
@@ -0,0 +1,108 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {SUGGEST_OWNERS} from './suggest-owners'; +import {OWNER_REQUIREMENT_VALUE} from './owner-requirement'; +import {SUGGEST_OWNERS_TRIGGER} from './suggest-owners-trigger'; +import '@gerritcodereview/typescript-api/gerrit'; +import { + CODE_OWNERS_BANNER, + CODE_OWNERS_PLUGIN_STATUS_NOTIFIER, +} from './code-owners-banner'; +import { + OWNERS_STATUS_COLUMN_HEADER, + OWNER_STATUS_COLUMN_CONTENT, +} from './owner-status-column'; +import {CodeOwnersModelMixinInterface} from './code-owners-model-mixin'; + +window.Gerrit.install(plugin => { + const restApi = plugin.restApi(); + const reporting = plugin.reporting(); + + plugin.registerCustomComponent('banner', CODE_OWNERS_BANNER); + + plugin + .registerCustomComponent( + 'change-view-integration', + CODE_OWNERS_PLUGIN_STATUS_NOTIFIER + ) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); + // owner status column / rows for file list + plugin + .registerDynamicCustomComponent( + 'change-view-file-list-header-prepend', + OWNERS_STATUS_COLUMN_HEADER + ) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); + plugin + .registerDynamicCustomComponent( + 'change-view-file-list-content-prepend', + OWNER_STATUS_COLUMN_CONTENT + ) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); + + // old submit requirement value for owner's requirement + plugin + .registerCustomComponent( + 'submit-requirement-item-code-owners', + OWNER_REQUIREMENT_VALUE, + {slot: 'value'} + ) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); + + // new submit requirement value for owner's requirement + plugin + .registerCustomComponent( + 'submit-requirement-codeowners', + OWNER_REQUIREMENT_VALUE, + {replace: true} + ) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); + + // suggest owners for reply dialog + plugin + .registerCustomComponent('reply-reviewers', SUGGEST_OWNERS_TRIGGER, { + slot: 'right', + }) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); + plugin + .registerCustomComponent('reply-reviewers', SUGGEST_OWNERS, { + slot: 'below', + }) + .onAttached(view => { + (view as unknown as CodeOwnersModelMixinInterface).restApi = restApi; + (view as unknown as CodeOwnersModelMixinInterface).reporting = reporting; + }); +});
diff --git a/web/suggest-owners-trigger.ts b/web/suggest-owners-trigger.ts new file mode 100644 index 0000000..fb2ca98 --- /dev/null +++ b/web/suggest-owners-trigger.ts
@@ -0,0 +1,92 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {CodeOwnersModelMixin} from './code-owners-model-mixin'; +import {css, html, LitElement, nothing} from 'lit'; +import {customElement} from 'lit/decorators'; + +const base = CodeOwnersModelMixin(LitElement); + +export const SUGGEST_OWNERS_TRIGGER = 'suggest-owners-trigger'; + +@customElement(SUGGEST_OWNERS_TRIGGER) +export class SuggestOwnersTrigger extends base { + static override get styles() { + return [ + css` + :host { + display: flex; + } + a { + text-decoration: none; + } + gr-button { + --gr-button-padding: var(--spacing-s); + } + .extras { + padding-top: var(--spacing-s); + } + `, + ]; + } + + override render() { + if (!this.status?.enabled) return nothing; + return html` + <gr-button + link + @click=${this.toggleControlContent} + has-tooltip + title="Suggest owners for your change" + > + ${this.computeButtonText()} + </gr-button> + <span class="extras"> + <a + @click=${this.reportDocClick} + href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/how-to-use.md" + target="_blank" + > + <gr-icon icon="help" title="read documentation"></gr-icon> + </a> + </span> + `; + } + + override loadPropertiesAfterModelChanged() { + super.loadPropertiesAfterModelChanged(); + this.modelLoader?.loadUserRole(); + this.modelLoader?.loadPluginStatus(); + this.modelLoader?.loadAreAllFilesApproved(); + this.modelLoader?.loadBranchConfig(); + } + + toggleControlContent() { + this.model?.setShowSuggestions(!this.showSuggestions); + this.reporting?.reportInteraction('toggle-suggest-owners', { + expanded: this.showSuggestions, + user_role: this.userRole ? this.userRole : 'UNKNOWN', + }); + } + + computeButtonText() { + return this.showSuggestions ? 'Hide owners' : 'Suggest owners'; + } + + private reportDocClick() { + this.reporting?.reportInteraction('code-owners-doc-click'); + } +}
diff --git a/web/suggest-owners-util.ts b/web/suggest-owners-util.ts new file mode 100644 index 0000000..3a32d8e --- /dev/null +++ b/web/suggest-owners-util.ts
@@ -0,0 +1,143 @@ +/** + * @license + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {AccountId, GroupId} from '@gerritcodereview/typescript-api/rest-api'; +import {CodeOwnerInfo, CodeOwnersInfo, FetchedFile} from './code-owners-api'; +import {BestSuggestionsLimit} from './code-owners-model'; + +export interface GroupedFiles { + groupName: {name: string|undefined; prefix: string;}; + files: Array<FetchedFile>; + owners?: CodeOwnersInfo; + error?: Error; + hasSelected?: boolean; + expanded?: boolean; +} + +/** + * For each file calculates owners to display and group all files by those + * owners. The function creates "fake" groups when one or more + * reviewers are included in all owners of a file, but none of reviewers is + * included in best reviewers for the file. + * + * Such situations are possible when user turns on "Show all owners", selects + * one of newly displayed owners and then turns off "Show all owners". Without + * "fake" groups a user can see inconsistent state in dialog. + */ +export function getDisplayOwnersGroups( + files: Array<FetchedFile>, + allOwnersByPathMap: Map<string, CodeOwnersInfo|undefined>, + reviewersIdSet: Set<AccountId|GroupId>, + allowAllOwnersSubstition: boolean): Array<GroupedFiles> { + const getDisplayOwnersFunc = !allowAllOwnersSubstition || + allOwnersByPathMap.size === 0 || reviewersIdSet.size === 0 ? + (file: FetchedFile) => file.info.owners : + (file: FetchedFile) => + getDisplayOwners(file, allOwnersByPathMap, reviewersIdSet); + return groupFilesByOwners(files, getDisplayOwnersFunc); +} + +function getDisplayOwners( + file: FetchedFile, + allOwnersByPathMap: Map<String, CodeOwnersInfo|undefined>, + reviewersIdSet: Set<AccountId|GroupId>) { + const ownerSelected = (owner: CodeOwnerInfo) => + owner?.account?._account_id !== undefined && + reviewersIdSet.has(owner.account._account_id); + const defaultOwners = file.info.owners; + if (!defaultOwners || defaultOwners.owned_by_all_users || + defaultOwners.code_owners.some(ownerSelected)) { + return defaultOwners; + } + const allOwners = allOwnersByPathMap.get(file.path); + if (!allOwners) return defaultOwners; + if (allOwners.owned_by_all_users) return allOwners; + const selectedAllOwners = allOwners.code_owners.filter(ownerSelected); + if (selectedAllOwners.length === 0) return defaultOwners; + return { + code_owners: selectedAllOwners.slice(0, BestSuggestionsLimit), + }; +} + +function groupFilesByOwners( + files: Array<FetchedFile>, + getDisplayOwnersFunc: (file: FetchedFile) => + CodeOwnersInfo | undefined): Array<GroupedFiles> { + // Note: for renamed or moved files, they will have two entries in the map + // we will treat them as two entries when group as well + const ownersFilesMap = + new Map<string, {files: Array<FetchedFile>, owners: CodeOwnersInfo}>(); + const failedToFetchFiles = new Set<FetchedFile>(); + for (const file of files) { + // for files failed to fetch, add them to the special group + if (file.info.error) { + failedToFetchFiles.add(file); + continue; + } + + // do not include files still in fetching + if (!file.info.owners) { + continue; + } + const displayOwners = getDisplayOwnersFunc(file); + if (displayOwners === undefined) continue; + + const ownersKey = getOwnersGroupKey(displayOwners); + ownersFilesMap.set( + ownersKey, + ownersFilesMap.get(ownersKey) ?? {files: [], owners: displayOwners}); + ownersFilesMap.get(ownersKey)!.files.push(file); + } + const groupedItems = []; + for (const ownersKey of ownersFilesMap.keys()) { + const groupName = getGroupName(ownersFilesMap.get(ownersKey)!.files); + groupedItems.push({ + groupName, + files: ownersFilesMap.get(ownersKey)!.files, + owners: ownersFilesMap.get(ownersKey)!.owners, + }); + } + + if (failedToFetchFiles.size > 0) { + const failedFiles = [...failedToFetchFiles]; + groupedItems.push({ + groupName: getGroupName(failedFiles), + files: failedFiles, + error: new Error( + 'Failed to fetch code owner info. Try to refresh the page.'), + }); + } + return groupedItems; +} + +function getOwnersGroupKey(owners: CodeOwnersInfo) { + if (owners.owned_by_all_users) { + return '__owned_by_all_users__'; + } + const code_owners = owners.code_owners; + return code_owners.map(owner => owner.account?._account_id).sort().join(','); +} + +function getGroupName(files: Array<FetchedFile>) { + const path = files[0].path.split('/'); + const fileName = path.pop(); + const shortenedDirectory = path.map(d => d.charAt(0)).join('/'); + return { + name: `${shortenedDirectory}/${fileName}`, + prefix: files.length > 1 ? `+ ${files.length - 1} more` : '', + }; +}
diff --git a/web/suggest-owners.ts b/web/suggest-owners.ts new file mode 100644 index 0000000..4fe2a7d --- /dev/null +++ b/web/suggest-owners.ts
@@ -0,0 +1,791 @@ +/** + * @license + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {CodeOwnersModelMixin} from './code-owners-model-mixin'; +import {SuggestionsState, SuggestionsType} from './code-owners-model'; +import {getDisplayOwnersGroups, GroupedFiles} from './suggest-owners-util'; +import {customElement, property, state} from 'lit/decorators'; +import {css, html, LitElement, nothing, PropertyValues} from 'lit'; +import {when} from 'lit/directives/when'; +import {ifDefined} from 'lit/directives/if-defined'; +import {CodeOwnerInfo, CodeOwnersInfo, FetchedFile} from './code-owners-api'; +import { + AccountId, + AccountInfo, + GroupInfo, +} from '@gerritcodereview/typescript-api/rest-api'; + +const SUGGESTION_POLLING_INTERVAL = 1000; + +export const OWNER_GROUP_FILE_LIST = 'owner-group-file-list'; + +@customElement(OWNER_GROUP_FILE_LIST) +export class OwnerGroupFileList extends LitElement { + @property({type: Array}) + files?: Array<FetchedFile>; + + static override get styles() { + return [ + css` + :host { + display: block; + max-height: 500px; + overflow: auto; + } + span { + display: inline-block; + border-radius: var(--border-radius); + margin-left: var(--spacing-s); + padding: 0 var(--spacing-m); + color: var(--primary-text-color); + font-size: var(--font-size-small); + } + .Renamed { + background-color: var(--dark-remove-highlight-color); + margin: var(--spacing-s) 0; + } + `, + ]; + } + + override render() { + return html` + <ul> + ${this.files?.map(file => this.renderFile(file))} + </ul> + `; + } + + private renderFile(file: FetchedFile) { + return html` + <li> + ${this.computeFilePath(file)} + <strong>${this.computeFileName(file)}</strong> + ${when( + !!file.status, + () => html` + <span class=${ifDefined(file.status)}> ${file.status} </span> + ` + )} + </li> + `; + } + + computeFilePath(file: FetchedFile) { + const parts = file.path.split('/'); + return parts.slice(0, parts.length - 1).join('/') + '/'; + } + + computeFileName(file: FetchedFile) { + const parts = file.path.split('/'); + return parts.pop(); + } +} + +const base = CodeOwnersModelMixin(LitElement); +export const SUGGEST_OWNERS = 'suggest-owners'; +@customElement(SUGGEST_OWNERS) +export class SuggestOwners extends base { + private reportedEvents: Map< + SuggestionsType, + { + fetchingStart: boolean; + fetchingFinished: boolean; + } + >; + + // Bound by the extension point. + @property({type: Array}) + reviewers?: Array<AccountInfo | GroupInfo>; + + @state() + suggestedOwners?: Array<GroupedFiles>; + + @state() + showAllOwners = false; + + private progressUpdateTimer?: number; + + constructor() { + super(); + this.reportedEvents = new Map(); + for (const suggestionType of Object.values(SuggestionsType)) { + this.reportedEvents.set(suggestionType, { + fetchingStart: false, + fetchingFinished: false, + }); + } + } + + static override get styles() { + return [ + css` + :host([show-suggestions]) { + display: block; + background-color: var(--view-background-color); + border: 1px solid var(--view-background-color); + border-radius: var(--border-radius); + box-shadow: var(--elevation-level-1); + padding: 0 var(--spacing-m); + margin: var(--spacing-m) 0; + } + .loadingSpin { + display: inline-block; + } + li { + list-style: none; + } + .suggestion-container { + max-height: 300px; + overflow-y: auto; + } + .flex-break { + height: 0; + flex-basis: 100%; + } + .suggestion-row, + .show-all-owners-row { + display: flex; + flex-direction: row; + align-items: flex-start; + } + .suggestion-row { + flex-wrap: wrap; + border-top: 1px solid var(--border-color); + padding: var(--spacing-s) 0; + } + .show-all-owners-row { + padding: var(--spacing-m) var(--spacing-xl) var(--spacing-s) 0; + } + .show-all-owners-row .loading { + padding: 0; + } + .show-all-owners-row .show-all-label { + margin-left: auto; /* align label to the right */ + } + .suggestion-row-indicator { + margin-right: var(--spacing-s); + visibility: hidden; + line-height: 26px; + } + .suggestion-row-indicator[visible] { + visibility: visible; + } + .suggestion-row-indicator[visible] gr-icon { + color: var(--link-color); + vertical-align: top; + position: relative; + font-size: 18px; + top: 4px; /* (26-18)/2 - 26px line-height and 18px icon */ + } + .suggestion-group-name { + width: 260px; + line-height: 26px; + text-overflow: ellipsis; + overflow: hidden; + padding-right: var(--spacing-s); + white-space: nowrap; + } + .group-name-content { + display: flex; + align-items: center; + } + .group-name-content .group-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .group-name-prefix { + padding-left: var(--spacing-s); + white-space: nowrap; + color: var(--deemphasized-text-color); + } + .suggested-owners { + --account-gap: var(--spacing-xs); + --negative-account-gap: calc(-1 * var(--account-gap)); + margin: var(--negative-account-gap) 0 0 var(--negative-account-gap); + flex: 1; + } + .fetch-error-content, + .owned-by-all-users-content, + .no-owners-content { + line-height: 26px; + flex: 1; + padding-left: var(--spacing-m); + } + + .owned-by-all-users-content gr-icon { + font-size: 16px; + padding-top: 5px; + } + + .fetch-error-content { + color: var(--error-text-color); + } + .no-owners-content a { + padding-left: var(--spacing-s); + } + .no-owners-content a gr-icon { + font-size: 16px; + padding-top: 5px; + } + gr-account-label { + display: inline-block; + padding: var(--spacing-xs) var(--spacing-m); + user-select: none; + border: 1px solid transparent; + --label-border-radius: 8px; + /* account-max-length defines the max text width inside account-label. + With 59 the gr-account-label always has width <= 100px and 5 labels + are always fit in a single row */ + --account-max-length: 59px; + border: 1px solid var(--border-color); + margin: var(--account-gap) 0 0 var(--account-gap); + } + gr-account-label:focus { + outline: none; + } + gr-account-label:hover { + box-shadow: var(--elevation-level-1); + cursor: pointer; + } + gr-account-label[selected] { + background-color: var(--chip-selected-background-color); + border: 1px solid var(--chip-selected-background-color); + color: var(--chip-selected-text-color); + } + gr-hovercard { + max-width: 800px; + } + .loading { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + padding: var(--spacing-m); + } + .loadingSpin { + width: 18px; + height: 18px; + margin-right: var(--spacing-m); + } + /* Copied from shared-styles */ + ul { + border: 0; + box-sizing: border-box; + font-size: 100%; + margin: 0; + padding: 0; + vertical-align: baseline; + } + *::after, + *::before { + box-sizing: border-box; + } + `, + ]; + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.stopUpdateProgressTimer(); + this.modelLoader?.pauseActiveSuggestedOwnersLoading(); + } + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + if (changedProperties.has('showAllOwners')) { + this.showAllOwnersChanged(); + } + if (changedProperties.has('reviewers')) { + this.onReviewerChanged(); + } + if (changedProperties.has('showSuggestions')) { + this.onShowSuggestionsChanged(); + } + if ( + changedProperties.has('showSuggestions') || + changedProperties.has('selectedSuggestionsType') + ) { + this.onShowSuggestionsTypeChanged(); + } + if (changedProperties.has('selectedSuggestionsState')) { + this.onSuggestionsStateChanged(); + } + if ( + changedProperties.has('selectedSuggestionsFiles') || + changedProperties.has('selectedSuggestionsType') || + changedProperties.has('selectedSuggestionsState') || + changedProperties.has('reviewers') + ) { + this.onSuggestionsFilesChanged(); + } + } + + override render() { + if (!this.showSuggestions) return nothing; + return html` + <ul class="suggestion-container"> + <li class="show-all-owners-row"> + ${this.renderLoading()} ${this.renderShowAllOwnersCheckbox()} + </li> + </ul> + <ul class="suggestion-container"> + ${when( + this.suggestedOwners && this.suggestedOwners.length > 0, + () => + (this.suggestedOwners ?? []).map((s, i) => + this.renderSuggestion(s, i) + ), + () => html`<li class="suggestion-row">No owners found</li>` + )} + </ul> + `; + } + + private renderLoading() { + const isLoading = + this.selectedSuggestionsState === SuggestionsState.Loading; + if (!isLoading) return nothing; + return html` + <p class="loading"> + <span class="loadingSpin"><gr-icon icon="sync"></gr-icon></span> + ${this.selectedSuggestionsLoadProgress} + </p> + `; + } + + private renderShowAllOwnersCheckbox() { + return html` + <label class="show-all-label"> + <input + id="showAllOwnersCheckbox" + type="checkbox" + .checked=${this.showAllOwners} + @click=${() => { + this.showAllOwners = !this.showAllOwners; + }} + /> + Show all owners + </label> + `; + } + + private renderSuggestion(suggestion: GroupedFiles, suggestionIndex: number) { + return html` + <li class="suggestion-row"> + <div + class="suggestion-row-indicator" + ?visible=${suggestion.hasSelected} + > + <gr-icon filled icon="check_circle"></gr-icon> + </div> + ${this.renderSuggestionGroupName(suggestion)} + ${this.renderSuggestionOwners(suggestion, suggestionIndex)} + </li> + `; + } + + private renderSuggestionGroupName(suggestion: GroupedFiles) { + return html` + <div class="suggestion-group-name"> + <div class="group-name-content"> + <span class="group-name"> ${suggestion.groupName.name} </span> + ${when( + suggestion.groupName.prefix, + () => html` + <span class="group-name-prefix"> + (${suggestion.groupName.prefix}) + </span> + ` + )} + ${when( + !suggestion.expanded, + () => html` + <gr-hovercard> + <owner-group-file-list + .files=${suggestion.files} + ></owner-group-file-list> + </gr-hovercard> + ` + )} + </div> + ${when( + suggestion.expanded, + () => html` + <owner-group-file-list + .files=${suggestion.files} + ></owner-group-file-list> + ` + )} + </div> + `; + } + + private renderSuggestionOwners( + suggestion: GroupedFiles, + suggestionIndex: number + ) { + if (suggestion.error) { + return html` <div class="fetch-error-content">${suggestion.error}</div> `; + } + + return html` + ${this.renderSuggestionOwnersNotFound(suggestion)} + ${this.renderSuggestedOwners(suggestion, suggestionIndex)} + `; + } + + private renderSuggestionOwnersNotFound(suggestion: GroupedFiles) { + if (this.areOwnersFound(suggestion.owners)) return nothing; + return html` + <div class="no-owners-content"> + <span>Not found</span> + <a + @click=${this.reportDocClick} + href="https://gerrit.googlesource.com/plugins/code-owners/+/HEAD/resources/Documentation/how-to-use.md#no-code-owners-found" + target="_blank" + > + <gr-icon icon="help" title="read documentation"></gr-icon> + </a> + </div> + `; + } + + private renderSuggestedOwners( + suggestion: GroupedFiles, + suggestionIndex: number + ) { + if (suggestion.owners?.owned_by_all_users) { + return html` + <div class="owned-by-all-users-content"> + <gr-icon icon="info" filled></gr-icon> + <span>${this.getOwnedByAllUsersContent()}</span> + </div> + `; + } + + return html` + ${when(this.showAllOwners, () => html`<div class="flex-break"></div>`)} + <ul class="suggested-owners"> + ${(suggestion.owners?.code_owners ?? []).map((owner, ownerIndex) => + this.renderSuggestedOwner(owner, suggestionIndex, ownerIndex) + )} + </ul> + `; + } + + private renderSuggestedOwner( + owner: CodeOwnerInfo, + suggestionIndex: number, + ownerIndex: number + ) { + return html`<gr-account-label + .account=${owner.account} + ?selected=${!!owner.selected} + @click=${() => this.toggleAccount(suggestionIndex, ownerIndex)} + ></gr-account-label>`; + } + + private onShowSuggestionsChanged() { + if (!this.showSuggestions) { + return; + } + // this is more of a hack to let review input lose focus + // to avoid suggestion dropdown + // gr-autocomplete has a internal state for tracking focus + // that will be canceled if any click happens outside of + // it's target + window.setTimeout(() => this.click(), 100); + } + + private onShowSuggestionsTypeChanged() { + if (!this.showSuggestions) { + this.modelLoader?.pauseActiveSuggestedOwnersLoading(); + return; + } + const selectedSuggestionsType = this.selectedSuggestionsType; + if (selectedSuggestionsType === undefined) { + return; + } + this.modelLoader?.loadSuggestions(selectedSuggestionsType); + // The progress is updated at the next _progressUpdateTimer tick. + // Without explicit call to updateLoadSuggestionsProgress it looks like + // a slow reaction to checkbox. + this.modelLoader?.updateLoadSelectedSuggestionsProgress(); + + if (!this.reportedEvents.get(selectedSuggestionsType)?.fetchingStart) { + this.reportedEvents.get(selectedSuggestionsType)!.fetchingStart = true; + this.reporting?.reportLifeCycle('owners-suggestions-fetching-start', { + type: selectedSuggestionsType, + }); + } + } + + _startUpdateProgressTimer() { + if (this.progressUpdateTimer) return; + this.progressUpdateTimer = window.setInterval(() => { + this.modelLoader?.updateLoadSelectedSuggestionsProgress(); + }, SUGGESTION_POLLING_INTERVAL); + } + + private stopUpdateProgressTimer() { + if (!this.progressUpdateTimer) return; + window.clearInterval(this.progressUpdateTimer); + this.progressUpdateTimer = undefined; + } + + private onSuggestionsStateChanged() { + this.stopUpdateProgressTimer(); + if (this.selectedSuggestionsState === SuggestionsState.Loading) { + this._startUpdateProgressTimer(); + } + } + + override loadPropertiesAfterModelChanged() { + super.loadPropertiesAfterModelChanged(); + this.stopUpdateProgressTimer(); + this.modelLoader?.loadAreAllFilesApproved(); + } + + private getReviewersIdSet(): Set<AccountId> { + return new Set( + (this.reviewers ?? []) + // Ugly hack, but we only care about accounts. + .map(account => (account as unknown as AccountInfo)._account_id) + .filter(account_id => account_id !== undefined) + ) as Set<AccountId>; + } + + private onSuggestionsFilesChanged() { + if ( + this.selectedSuggestionsFiles === undefined || + this.reviewers === undefined || + this.selectedSuggestionsType === undefined || + this.selectedSuggestionsState === undefined + ) + return; + + const allOwners = + this.model?.getSuggestionsByType(SuggestionsType.ALL_SUGGESTIONS) + ?.files ?? []; + + const allOwnersByPathMap = this.getOwnersByPathMap(allOwners); + const reviewersIdSet = this.getReviewersIdSet(); + const selectedSuggestionsType = this.selectedSuggestionsType; + const suggestionsState = this.selectedSuggestionsState; + + const groups = getDisplayOwnersGroups( + this.selectedSuggestionsFiles, + allOwnersByPathMap, + reviewersIdSet, + selectedSuggestionsType !== SuggestionsType.ALL_SUGGESTIONS + ); + // The updateLoadSuggestionsProgress method also updates suggestions + this.updateSuggestions(groups); + this.updateAllChips(this.reviewers); + + if (suggestionsState !== SuggestionsState.Loaded) return; + if (!this.reportedEvents.get(selectedSuggestionsType)!.fetchingFinished) { + this.reportedEvents.get(selectedSuggestionsType)!.fetchingFinished = true; + const reportDetails = groups.reduce( + (details, cur) => { + details.totalGroups++; + details.stats.push([ + cur.files.length, + cur.owners && cur.owners.code_owners + ? cur.owners.code_owners.length + : 0, + ]); + return details; + }, + { + totalGroups: 0, + stats: [] as Array<Array<number>>, + type: selectedSuggestionsType, + } + ); + this.reporting?.reportLifeCycle( + 'owners-suggestions-fetching-finished', + reportDetails + ); + } + } + + private getOwnersByPathMap(files?: Array<FetchedFile>) { + return new Map( + (files || []) + .filter(file => !file.info.error && file.info.owners) + .map(file => [file.path, file.info.owners]) + ); + } + + private updateSuggestions(suggestions: Array<GroupedFiles>) { + // update group names and files, no modification on owners or error + const suggestedOwners = suggestions.map(suggestion => + this.formatSuggestionInfo(suggestion) + ); + // move owned_by_all_users to the bottom: + const index = suggestedOwners.findIndex( + suggestion => suggestion.owners?.owned_by_all_users + ); + if (index >= 0) { + suggestedOwners.push(suggestedOwners.splice(index, 1)[0]); + } + this.suggestedOwners = suggestedOwners; + } + + onReviewerChanged() { + this.updateAllChips(this.reviewers); + } + + formatSuggestionInfo(suggestion: GroupedFiles): GroupedFiles { + const res = { + groupName: suggestion.groupName, + files: suggestion.files.slice(), + owners: undefined as CodeOwnersInfo | undefined, + error: undefined as Error | undefined, + } as GroupedFiles; + if (suggestion.owners) { + const codeOwners = (suggestion.owners.code_owners || []).map(owner => { + const updatedOwner = {...owner}; + const reviewers = this.change?.reviewers.REVIEWER; + if ( + reviewers && + reviewers.find( + reviewer => reviewer._account_id === owner.account?._account_id + ) + ) { + updatedOwner.selected = true; + } + return updatedOwner; + }); + res.owners = { + owned_by_all_users: !!suggestion.owners.owned_by_all_users, + code_owners: codeOwners, + }; + } else { + res.owners = { + owned_by_all_users: false, + code_owners: [], + }; + } + + res.error = suggestion.error; + return res; + } + + addAccount(owner: CodeOwnerInfo) { + owner.selected = true; + this.dispatchEvent( + new CustomEvent('add-reviewer', { + detail: { + reviewer: {...owner.account, _pendingAdd: true}, + }, + composed: true, + bubbles: true, + }) + ); + this.reporting?.reportInteraction('add-reviewer'); + } + + removeAccount(owner: CodeOwnerInfo) { + owner.selected = false; + this.dispatchEvent( + new CustomEvent('remove-reviewer', { + detail: { + reviewer: {...owner.account, _pendingAdd: true}, + }, + composed: true, + bubbles: true, + }) + ); + this.reporting?.reportInteraction('remove-reviewer'); + } + + toggleAccount(suggestionIndex: number, ownerIndex: number) { + const owner = + this.suggestedOwners![suggestionIndex]?.owners?.code_owners[ownerIndex]; + if (!owner) return; + if (owner.selected) { + this.removeAccount(owner); + } else { + this.addAccount(owner); + } + } + + private updateAllChips(accounts: Array<AccountInfo> | undefined) { + if (!this.suggestedOwners || !accounts) return; + // update all occurences + this.suggestedOwners.forEach((suggestion, sId) => { + let hasSelected = false; + suggestion.owners?.code_owners.forEach((owner, oId) => { + if ( + accounts.some( + account => account._account_id === owner!.account!._account_id + ) + ) { + this.suggestedOwners![sId].owners!.code_owners[oId].selected = true; + hasSelected = true; + } else { + this.suggestedOwners![sId].owners!.code_owners[oId].selected = false; + } + }); + const nonServiceUser = (account: AccountInfo) => + !account.tags || account.tags.indexOf('SERVICE_USER') < 0; + if ( + suggestion.owners?.owned_by_all_users && + accounts.some(nonServiceUser) + ) { + hasSelected = true; + } + this.suggestedOwners![sId].hasSelected = hasSelected; + }); + } + + private reportDocClick() { + this.reporting?.reportInteraction('code-owners-doc-click', { + section: 'no owners found', + }); + } + + private areOwnersFound(owners: CodeOwnersInfo | undefined) { + return ( + owners && (owners.code_owners.length > 0 || !!owners.owned_by_all_users) + ); + } + + private getOwnedByAllUsersContent() { + if (this.selectedSuggestionsState === SuggestionsState.Loading) { + return 'Any user can approve'; + } + // If all users own all the files in the change suggestedOwners.length === 1 + // (suggestedOwners - collection of owners groupbed by owners) + return this.suggestedOwners && this.suggestedOwners.length === 1 + ? 'Any user can approve. Please select a user manually' + : 'Any user from the other files can approve'; + } + + private showAllOwnersChanged() { + // The first call to this method happens before model is set. + if (!this.model) return; + this.model.setSelectedSuggestionType( + this.showAllOwners + ? SuggestionsType.ALL_SUGGESTIONS + : SuggestionsType.BEST_SUGGESTIONS + ); + } +}
diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..00f6c7e --- /dev/null +++ b/web/tsconfig.json
@@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig-plugins-base.json", + "compilerOptions": { + "experimentalDecorators": true, + /* outDir for IDE (overridden by Bazel rule arg) */ + "outDir": "../../../.ts-out/plugins/code-owners/ui", + }, + "include": [ + "**/*", + ], +}