Merge branch 'master' into stable-3.5
* master:
TaskTree: remove 'ERROR:' prefix from log statement
TaskTree: fix errorprone warning about Flogger
Most development has happened in the stable-3.5 branch, but master has a
couple commits missing from there. Merge them in before creating new
stable branches based on stable-3.5.
Change-Id: I0698db5f5cdadb5b208fdac09e0a4ffece20b2d0
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.bazelrc b/.bazelrc
index 3ae03ff..fb8e4d1 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,2 +1,18 @@
-build --workspace_status_command="python ./tools/workspace_status.py"
+build --java_language_version=11
+build --java_runtime_version=remotejdk_11
+build --tool_java_language_version=11
+build --tool_java_runtime_version=remotejdk_11
+
+build --workspace_status_command="python3 ./tools/workspace_status.py"
+build --repository_cache=~/.gerritcodereview/bazel-cache/repository
+build --action_env=PATH
+build --disk_cache=~/.gerritcodereview/bazel-cache/cas
+
+# Enable strict_action_env flag to. For more information on this feature see
+# https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk.
+# This will be the new default behavior at some point (and the flag was flipped
+# shortly in 0.21.0 - https://github.com/bazelbuild/bazel/issues/7026). Remove
+# this flag here once flipped in Bazel again.
+build --incompatible_strict_action_env
+
test --build_tests_only
diff --git a/.bazelversion b/.bazelversion
index fcdb2e1..c7cb131 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-4.0.0
+5.3.1
diff --git a/.eslintrc.json b/.eslintrc.json
index 0c290fa..358c95d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,7 +4,7 @@
"google"
],
"parserOptions": {
- "ecmaVersion": 8,
+ "ecmaVersion": 2020,
"sourceType": "module"
},
"env": {
diff --git a/.zuul.yaml b/.zuul.yaml
index 27c8491..6e83fbf 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -2,7 +2,9 @@
name: plugins-task-build
parent: gerrit-plugin-build
pre-run:
- tools/playbooks/install_docker.yaml
+ - tools/playbooks/install_maven.yaml
+ - tools/playbooks/install_docker.yaml
+ - tools/playbooks/install_python3-distutils.yaml
vars:
bazelisk_test_targets: "plugins/task/lint_test plugins/task/..."
diff --git a/BUILD b/BUILD
index 0b04753..537b627 100644
--- a/BUILD
+++ b/BUILD
@@ -1,8 +1,48 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load(
+ "//tools/bzl:plugin.bzl",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+ "gerrit_plugin",
+)
load("//tools/bzl:js.bzl", "gerrit_js_bundle")
load("//tools/js:eslint.bzl", "eslint")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
+load("@rules_antlr//antlr:antlr4.bzl", "antlr")
+
plugin_name = "task"
+java_plugin(
+ name = "auto-value-plugin",
+ processor_class = "com.google.auto.value.processor.AutoValueProcessor",
+ deps = [
+ "@auto-value-annotations//jar",
+ "@auto-value//jar",
+ ],
+)
+
+java_library(
+ name = "auto-value",
+ exported_plugins = [
+ ":auto-value-plugin",
+ ],
+ visibility = ["//visibility:public"],
+ exports = ["@auto-value//jar"],
+)
+
+antlr(
+ name = "task_reference",
+ srcs = ["src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4"],
+ package = "com.googlesource.gerrit.plugins.task",
+ visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "task_reference_parser",
+ srcs = [":task_reference"],
+ deps = ["@antlr4_runtime//jar"],
+)
+
gerrit_plugin(
name = plugin_name,
srcs = glob(["src/main/java/**/*.java"]),
@@ -14,6 +54,11 @@
],
resource_jars = [":gr-task-plugin"],
resources = glob(["src/main/resources/**/*"]),
+ deps = [
+ ":auto-value",
+ ":task_reference_parser",
+ "@antlr4_runtime//jar",
+ ],
javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"],
)
@@ -23,6 +68,13 @@
entry_point = "gr-task-plugin/plugin.js",
)
+junit_tests(
+ name = "junit-tests",
+ size = "small",
+ srcs = glob(["src/test/java/**/*Test.java"]),
+ deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [plugin_name],
+)
+
sh_test(
name = "docker-tests",
size = "medium",
@@ -30,6 +82,7 @@
args = ["--task-plugin-jar", "$(location :task)"],
data = [plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]),
local = True,
+ tags = ["docker"],
)
eslint(
@@ -43,10 +96,5 @@
".js",
],
ignore = ".eslintignore",
- plugins = [
- "@npm//eslint-config-google",
- "@npm//eslint-plugin-html",
- "@npm//eslint-plugin-import",
- "@npm//eslint-plugin-jsdoc",
- ],
+ plugins = [],
)
diff --git a/WORKSPACE b/WORKSPACE
index c217ecf..0cab5f6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,14 +1,12 @@
workspace(
name = "task",
- managed_directories = {
- "@npm": ["node_modules"],
- },
)
load("//:bazlets.bzl", "load_bazlets")
load_bazlets(
- commit = "6ebb3cfa1332a0dc0d2b7ea904a4703656f2ba54",
+ commit = "b6120a9fa50945d38f0a4d55d5879e3ec465c5e5",
+ shallow_since = "1701477032 -0700",
#local_path = "/home/<user>/projects/bazlets",
)
@@ -20,12 +18,23 @@
gerrit_polymer()
-load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
+load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
+
+build_bazel_rules_nodejs_dependencies()
+
+load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
+
+node_repositories(
+ node_version = "16.13.2",
+ yarn_version = "1.22.17",
+)
yarn_install(
name = "npm",
+ exports_directories_only = False,
frozen_lockfile = False,
package_json = "//:package.json",
+ symlink_node_modules = True,
yarn_lock = "//:yarn.lock",
)
@@ -35,4 +44,9 @@
"gerrit_api",
)
+# Release Plugin API
gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
index f089af4..457bfec 100644
--- a/bazlets.bzl
+++ b/bazlets.bzl
@@ -4,12 +4,14 @@
def load_bazlets(
commit,
+ shallow_since = None,
local_path = None):
if not local_path:
git_repository(
name = NAME,
remote = "https://gerrit.googlesource.com/bazlets",
commit = commit,
+ shallow_since = shallow_since,
)
else:
native.local_repository(
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..901cd0f
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,56 @@
+load("@bazel_tools//tools/build_defs/repo:maven_rules.bzl", "maven_jar")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+def external_plugin_deps():
+ AUTO_VALUE_VERSION = "1.7.4"
+
+ maven_jar(
+ name = "auto-value",
+ artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
+ sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
+ )
+
+ maven_jar(
+ name = "auto-value-annotations",
+ artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
+ sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
+ )
+
+ http_archive(
+ name = "rules_antlr",
+ sha256 = "26e6a83c665cf6c1093b628b3a749071322f0f70305d12ede30909695ed85591",
+ strip_prefix = "rules_antlr-0.5.0",
+ urls = ["https://github.com/marcohu/rules_antlr/archive/0.5.0.tar.gz"],
+ )
+
+ maven_jar(
+ name = "antlr3_runtime",
+ artifact = "org.antlr:antlr-runtime:3.5.2",
+ sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd",
+ )
+
+ ANTLR_VERSION = "4.9.3"
+
+ maven_jar(
+ name = "antlr4_runtime",
+ artifact = "org.antlr:antlr4-runtime:" + ANTLR_VERSION,
+ sha1 = "81befc16ebedb8b8aea3e4c0835dd5ca7e8523a8",
+ )
+
+ maven_jar(
+ name = "antlr4_tool",
+ artifact = "org.antlr:antlr4:" + ANTLR_VERSION,
+ sha1 = "9d47afaa75d70903b5b77413b034d6b201d7d5d6",
+ )
+
+ maven_jar(
+ name = "stringtemplate4",
+ artifact = "org.antlr:ST4:4.3.1",
+ sha1 = "9c61ac6d17b7f450b4048742c2cc73787972518e",
+ )
+
+ maven_jar(
+ name = "javax_json",
+ artifact = "org.glassfish:javax.json:1.0.4",
+ sha1 = "3178f73569fd7a1e5ffc464e680f7a8cc784b85a",
+ )
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js
new file mode 100644
index 0000000..8e46bd0
--- /dev/null
+++ b/gr-task-plugin/gr-task-chip.js
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * 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.
+ */
+
+import './gr-task-plugin.js';
+import {htmlTemplate} from './gr-task-chip_html.js';
+
+class GrTaskChip extends Polymer.Element {
+ static get is() {
+ return 'gr-task-chip';
+ }
+
+ static get template() {
+ return htmlTemplate;
+ }
+
+ static get properties() {
+ return {
+ chip_style: {
+ type: String,
+ notify: true,
+ value: 'ready',
+ },
+ };
+ }
+
+ _setTasksTabActive() {
+ // TODO: Identify a better way as current implementation is fragile
+ const endPointDecorators = document.querySelector('gr-app')
+ .shadowRoot.querySelector('gr-app-element')
+ .shadowRoot.querySelector('main')
+ .querySelector('gr-change-view')
+ .shadowRoot.querySelector('#mainContent')
+ .getElementsByTagName('gr-endpoint-decorator');
+ if (endPointDecorators) {
+ for (let i = 0; i <= endPointDecorators.length; i++) {
+ const el = endPointDecorators[i]
+ ?.shadowRoot?.querySelector('gr-task-plugin');
+ if (el) {
+ el.shadowRoot.querySelector('paper-tabs')
+ .querySelector('paper-tab').scrollIntoView();
+ break;
+ }
+ }
+ }
+ }
+
+ _onChipClick() {
+ this._setTasksTabActive();
+ }
+}
+
+customElements.define(GrTaskChip.is, GrTaskChip);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-chip_html.js b/gr-task-plugin/gr-task-chip_html.js
new file mode 100644
index 0000000..27cee0f
--- /dev/null
+++ b/gr-task-plugin/gr-task-chip_html.js
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * 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.
+ */
+
+export const htmlTemplate = Polymer.html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ .taskSummaryChip {
+ color: var(--chip-color);
+ cursor: pointer;
+ display: inline-block;
+ padding: var(--spacing-xxs) var(--spacing-m) var(--spacing-xxs)
+ var(--spacing-s);
+ margin-right: var(--spacing-s);
+ border-radius: 12px;
+ border: 1px solid gray;
+ vertical-align: top;
+ /* centered position of 20px chips in 24px line-height inline flow */
+ vertical-align: top;
+ position: relative;
+ top: 2px;
+ }
+ .taskSummaryChip.loading {
+ border-color: var(--gray-foreground);
+ background: var(--gray-background);
+ }
+ .taskSummaryChip.loading:hover {
+ background: var(--gray-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.loading:focus-within {
+ background: var(--gray-background-focus);
+ }
+ .taskSummaryChip.success {
+ border-color: var(--success-foreground);
+ background: var(--success-background);
+ }
+ .taskSummaryChip.success:hover {
+ background: var(--success-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.success:focus-within {
+ background: var(--success-background-focus);
+ }
+ .taskSummaryChip.waiting {
+ border-color: var(--warning-foreground);
+ background: var(--warning-background);
+ }
+ .taskSummaryChip.waiting:hover {
+ background: var(--warning-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.waiting:focus-within {
+ background: var(--warning-background-focus);
+ }
+ .taskSummaryChip.ready {
+ border-color: var(--success-foreground);
+ background: var(--success-background);
+ }
+ .taskSummaryChip.ready:hover {
+ background: var(--success-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.ready:focus-within {
+ background: var(--success-background-focus);
+ }
+ .taskSummaryChip.invalid {
+ color: var(--error-foreground);
+ border-color: var(--error-foreground);
+ background: var(--error-background);
+ }
+ .taskSummaryChip.invalid:hover {
+ background: var(--error-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.invalid:focus-within {
+ background: var(--error-background-focus);
+ }
+ .taskSummaryChip.duplicate {
+ color: var(--success-foreground);
+ border-color: var(--success-foreground);
+ background: var(--success-background);
+ }
+ .taskSummaryChip.duplicate:hover {
+ background: var(--success-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.duplicate:focus-within {
+ background: var(--success-background-focus);
+ }
+ .taskSummaryChip.fail {
+ color: var(--error-foreground);
+ border-color: var(--error-foreground);
+ background: var(--error-background);
+ }
+ .taskSummaryChip.fail:hover {
+ background: var(--error-background-hover);
+ box-shadow: var(--elevation-level-1);
+ }
+ .taskSummaryChip.fail:focus-within {
+ background: var(--error-background-focus);
+ }
+ .font-small {
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--line-height-small);
+ }
+ </style>
+ <button
+ class$="taskSummaryChip font-small [[chip_style]]"
+ on-click="_onChipClick">
+ <slot></slot>
+ </button>
+`;
diff --git a/gr-task-plugin/gr-task-plugin-tasks.js b/gr-task-plugin/gr-task-plugin-tasks.js
index 54585d1..7653ed4 100644
--- a/gr-task-plugin/gr-task-plugin-tasks.js
+++ b/gr-task-plugin/gr-task-plugin-tasks.js
@@ -38,12 +38,21 @@
type: String,
notify: true,
},
+
+ config: {
+ type: Object,
+ value() { return {}; },
+ },
};
}
_can_show(show, task) {
return show === 'true' || task.showOnFilter;
}
+
+ _getChangeUrl(change) {
+ return '/c/' + change.toString();
+ }
}
customElements.define(GrTaskPluginTasks.is, GrTaskPluginTasks);
diff --git a/gr-task-plugin/gr-task-plugin-tasks_html.js b/gr-task-plugin/gr-task-plugin-tasks_html.js
index df1644f..60fbd32 100644
--- a/gr-task-plugin/gr-task-plugin-tasks_html.js
+++ b/gr-task-plugin/gr-task-plugin-tasks_html.js
@@ -20,12 +20,20 @@
<template is="dom-if" if="[[_can_show(show_all, task)]]">
<li>
<style>
- /* Matching colors with core code. */
+ /* Matching colors with core theme. */
.green {
- color: #9fcc6b;
+ color: var(--success-foreground);
}
.red {
- color: #FFA62F;
+ color: var(--error-foreground);
+ }
+ .orange {
+ color: var(--warning-foreground);
+ }
+ .links {
+ color: var(--link-color);
+ cursor: pointer;
+ text-decoration: underline;
}
</style>
<template is="dom-if" if="[[task.icon.id]]">
@@ -47,7 +55,21 @@
</iron-icon>
</gr-tooltip-content>
</template>
- [[task.message]]
+ <template is="dom-if" if="[[task.change]]">
+ <a class="links" href$="[[_getChangeUrl(task.change)]]">[[task.change]]</a>
+ </template>
+ <template is="dom-if" if="[[!task.change]]">
+ <template is="dom-if" if="[[!task.hint]]">
+ [[task.name]]
+ </template>
+ </template>
+ <template is="dom-if" if="[[task.hint]]">
+ <gr-linked-text style="display: -webkit-inline-box;"
+ pre=""
+ content="[[task.hint]]"
+ config="[[config]]">
+ </gr-linked-text>
+ </template>
</li>
</template>
<ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js
index c665d61..2ac355d 100644
--- a/gr-task-plugin/gr-task-plugin.js
+++ b/gr-task-plugin/gr-task-plugin.js
@@ -22,10 +22,10 @@
const Defs = {};
/**
* @typedef {{
- * message: string,
* sub_tasks: Array<Defs.Task>,
* hint: ?string,
* name: string,
+ * change: ?number,
* status: string
* }} Defs.Task
*/
@@ -82,6 +82,30 @@
notify: true,
value: 0,
},
+ _invalid_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+ _waiting_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+ _duplicate_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+ _pass_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+ _isPending: {
+ type: Boolean,
+ value: true,
+ },
};
}
@@ -95,15 +119,21 @@
this._getTasks();
}
+ _is_hidden(_isPending, _tasks) {
+ return (!_isPending && !_tasks.length);
+ }
+
_getTasks() {
if (!this.change) {
return;
}
+ this._isPending = true;
const endpoint =
`/changes/?q=change:${this.change._number}&--task--applicable`;
return this.plugin.restApi().get(endpoint).then(response => {
+ this._isPending = false;
if (response && response.length === 1) {
const cinfo = response[0];
if (cinfo.plugins) {
@@ -114,7 +144,20 @@
this._tasks = this._addTasks(taskPluginInfo.roots);
}
}
+ document.dispatchEvent(new CustomEvent('tasks-loaded', {
+ detail: {
+ ready_count: this._ready_count,
+ fail_count: this._fail_count,
+ invalid_count: this._invalid_count,
+ waiting_count: this._waiting_count,
+ duplicate_count: this._duplicate_count,
+ pass_count: this._pass_count,
+ },
+ composed: true, bubbles: true,
+ }));
}
+ }).catch(e => {
+ this._isPending = false;
});
}
@@ -127,7 +170,7 @@
icon.tooltip = 'Failed';
break;
case 'READY':
- icon.id = 'gr-icons:rebase';
+ icon.id = 'gr-icons:playArrow';
icon.color = 'green';
icon.tooltip = 'Ready';
break;
@@ -137,13 +180,18 @@
icon.tooltip = 'Invalid';
break;
case 'WAITING':
- icon.id = 'gr-icons:side-by-side';
- icon.color = 'red';
+ icon.id = 'gr-icons:pause';
+ icon.color = 'orange';
icon.tooltip = 'Waiting';
break;
- case 'PASS':
+ case 'DUPLICATE':
icon.id = 'gr-icons:check';
icon.color = 'green';
+ icon.tooltip = 'Duplicate';
+ break;
+ case 'PASS':
+ icon.id = 'gr-icons:check-circle';
+ icon.color = 'green';
icon.tooltip = 'Passed';
break;
}
@@ -160,10 +208,10 @@
return false;
}
- _computeShowOnNeedsAndBlockedFilter(task) {
+ _computeShowOnNeededAndBlockedFilter(task) {
return this._isFailOrReadyOrInvalid(task) ||
(task.sub_tasks && task.sub_tasks.some(t =>
- this._computeShowOnNeedsAndBlockedFilter(t)));
+ this._computeShowOnNeededAndBlockedFilter(t)));
}
_compute_counts(task) {
@@ -175,15 +223,26 @@
case 'READY':
this._ready_count++;
break;
+ case 'INVALID':
+ this._invalid_count++;
+ break;
+ case 'WAITING':
+ this._waiting_count++;
+ break;
+ case 'DUPLICATE':
+ this._duplicate_count++;
+ break;
+ case 'PASS':
+ this._pass_count++;
+ break;
}
}
_addTasks(tasks) { // rename to process, remove DOM bits
if (!tasks) return [];
tasks.forEach(task => {
- task.message = task.hint || task.name;
task.icon = this._computeIcon(task);
- task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task);
+ task.showOnFilter = this._computeShowOnNeededAndBlockedFilter(task);
this._compute_counts(task);
this._addTasks(task.sub_tasks);
});
@@ -195,7 +254,7 @@
this._expand_all = 'true';
}
- _needs_and_blocked_tap() {
+ _needed_and_blocked_tap() {
this._show_all = 'false';
this._expand_all = 'true';
}
@@ -203,6 +262,14 @@
_switch_expand() {
this._expand_all = !this._expand_all;
}
+
+ _computeShowAllLabelText(showAllSections) {
+ if (showAllSections) {
+ return 'Hide all';
+ } else {
+ return 'Show all';
+ }
+ }
}
customElements.define(GrTaskPlugin.is, GrTaskPlugin);
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js
index 71c995c..6ab5fb9 100644
--- a/gr-task-plugin/gr-task-plugin_html.js
+++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -16,65 +16,112 @@
*/
export const htmlTemplate = Polymer.html`
-<style>
- ul {
- padding-left: 0.5em;
- margin-top: 0;
- }
- h3 {
- padding-left: 0.1em;
- margin: 0 0 0 0;
- }
- .cursor { cursor: pointer; }
- .links {
- color: blue;
- cursor: pointer;
- text-decoration: underline;
- }
- #tasks_header {
- align-items: center;
- background-color: #fafafa;
- border-top: 1px solid #ddd;
- display: flex;
- padding: 6px 1rem;
- }
- .no-margins { margin: 0 0 0 0; }
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ .header {
+ align-items: center;
+ background-color: var(--background-color-primary);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ padding: var(--spacing-s) var(--spacing-l);
+ z-index: 99; /* Less than gr-overlay's backdrop */
+ }
+ .headerTitle {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ }
+ .headerSubject {
+ 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);
+ }
+ paper-tabs {
+ background-color: var(--background-color-tertiary);
+ margin-top: var(--spacing-m);
+ height: calc(var(--line-height-h3) + var(--spacing-m));
+ --paper-tabs-selection-bar-color: var(--link-color);
+ }
+ paper-tab {
+ box-sizing: border-box;
+ max-width: 12em;
+ --paper-tab-ink: var(--link-color);
+ }
+ section {
+ background-color: var(--view-background-color);
+ box-shadow: var(--elevation-level-1);
+ }
+ ul {
+ padding-left: 0.5em;
+ margin-top: 0;
+ }
+ .links {
+ color: var(--link-color);
+ cursor: pointer;
+ text-decoration: underline;
+ }
+ .show-all-button iron-icon {
+ color: inherit;
+ --iron-icon-height: 18px;
+ --iron-icon-width: 18px;
+ }
+ .no-margins { margin: 0 0 0 0; }
+ .task-list-item {
+ display: flex;
+ align-items: center;
+ column-gap: 1em;
+ padding-top: 12px;
+ padding-left: 12px;
+ }
</style>
-<div id="tasks" hidden$="[[!_tasks.length]]">
- <div id="tasks_header" style="display: flex;">
- <iron-icon
- icon="gr-icons:expand-less"
- hidden$="[[!_expand_all]]"
- on-tap="_switch_expand"
- class="cursor"> </iron-icon>
- <iron-icon
- icon="gr-icons:expand-more"
- hidden$="[[_expand_all]]"
- on-tap="_switch_expand"
- class="cursor"> </iron-icon>
- <div style="display: flex; align-items: center; column-gap: 1em;">
- <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3>
+<div id="tasks" hidden$="[[_is_hidden(_isPending, _tasks)]]">
+ <paper-tabs id="secondaryTabs" selected="0">
+ <paper-tab
+ data-name$="Tasks"
+ class="Tasks"
+ >
+ Tasks
+ </paper-tab>
+ </paper-tabs>
+ <section class="TasksList">
+ <div hidden$="[[!_isPending]]" class="task-list-item">Loading...</div>
+ <div hidden$="[[_isPending]]" class="task-list-item">
<template is="dom-if" if="[[_is_show_all(_show_all)]]">
- <p class="no-margins" >All ([[_all_count]]) |
+ <p> All ([[_all_count]]) |
<span
- on-click="_needs_and_blocked_tap"
- class="links">Needs + Blocked ([[_ready_count]], [[_fail_count]])</span>
+ on-click="_needed_and_blocked_tap"
+ class="links">Needed + Blocked ([[_ready_count]], [[_fail_count]])</span>
<p>
</template>
<template is="dom-if" if="[[!_is_show_all(_show_all)]]">
- <p class="no-margins" > <span
+ <p> <span
class="links"
on-click="_show_all_tap">All ([[_all_count]])</span>
- | Needs + Blocked ([[_ready_count]], [[_fail_count]])</p>
+ | Needed + Blocked ([[_ready_count]], [[_fail_count]])</p>
</template>
+ <gr-button link="" class="show-all-button" on-click="_switch_expand"
+ >[[_computeShowAllLabelText(_expand_all)]]
+ <iron-icon
+ icon="gr-icons:expand-more"
+ hidden$="[[_expand_all]]"
+ ></iron-icon
+ ><iron-icon
+ icon="gr-icons:expand-less"
+ hidden$="[[!_expand_all]]"
+ ></iron-icon>
+ </gr-button>
</div>
- </div>
- <div hidden$="[[!_expand_all]]">
+ <div hidden$="[[!_expand_all]]" style="padding-bottom: 12px">
<ul style="list-style-type:none;">
<gr-task-plugin-tasks
tasks="[[_tasks]]"
show_all$="[[_show_all]]"> </gr-task-plugin-tasks>
</ul>
</div>
+ </section>
</div>`;
diff --git a/gr-task-plugin/gr-task-summary.js b/gr-task-plugin/gr-task-summary.js
new file mode 100644
index 0000000..49c858d
--- /dev/null
+++ b/gr-task-plugin/gr-task-summary.js
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * 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.
+ */
+
+import {htmlTemplate} from './gr-task-summary_html.js';
+import './gr-task-chip.js';
+
+class GrTaskSummary extends Polymer.Element {
+ static get is() {
+ return 'gr-task-summary';
+ }
+
+ static get template() {
+ return htmlTemplate;
+ }
+
+ static get properties() {
+ return {
+ ready_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ fail_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ invalid_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ waiting_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ duplicate_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ pass_count: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+
+ is_loading: {
+ type: Boolean,
+ value: true,
+ },
+ };
+ }
+
+ /** @override */
+ ready() {
+ super.ready();
+ document.addEventListener('tasks-loaded', e => {
+ this.ready_count = e.detail.ready_count;
+ this.fail_count = e.detail.fail_count;
+ this.invalid_count = e.detail.invalid_count;
+ this.waiting_count = e.detail.waiting_count;
+ this.duplicate_count = e.detail.duplicate_count;
+ this.pass_count = e.detail.pass_count;
+ this.is_loading = false;
+ });
+ }
+
+ _can_show_summary(is_loading, ready_count,
+ fail_count, invalid_count,
+ waiting_count, duplicate_count,
+ pass_count) {
+ if (is_loading || ready_count || fail_count || invalid_count ||
+ waiting_count || duplicate_count || pass_count) {
+ return true;
+ }
+ return false;
+ }
+}
+
+customElements.define(GrTaskSummary.is, GrTaskSummary);
\ No newline at end of file
diff --git a/gr-task-plugin/gr-task-summary_html.js b/gr-task-plugin/gr-task-summary_html.js
new file mode 100644
index 0000000..4667060
--- /dev/null
+++ b/gr-task-plugin/gr-task-summary_html.js
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * 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.
+ */
+
+export const htmlTemplate = Polymer.html`
+ <style include="gr-a11y-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
+ <style include="shared-styles">
+ :host {
+ display: block;
+ color: var(--deemphasized-text-color);
+ max-width: 625px;
+ margin-bottom: var(--spacing-m);
+ }
+ .zeroState {
+ color: var(--deemphasized-text-color);
+ }
+ .loading.zeroState {
+ margin-right: var(--spacing-m);
+ }
+ div.error,
+ .login {
+ display: flex;
+ color: var(--primary-text-color);
+ padding: 0 var(--spacing-s);
+ margin: var(--spacing-xs) 0;
+ width: 490px;
+ }
+ div.error {
+ background-color: var(--error-background);
+ }
+ div.error .right {
+ overflow: hidden;
+ }
+ div.error .right .message {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ td.key {
+ padding-right: var(--spacing-l);
+ padding-bottom: var(--spacing-s);
+ line-height: calc(var(--line-height-normal) + var(--spacing-s));
+ }
+ td.value {
+ padding-right: var(--spacing-l);
+ padding-bottom: var(--spacing-s);
+ line-height: calc(var(--line-height-normal) + var(--spacing-s));
+ display: flex;
+ }
+ div {
+ margin-left: var(--spacing-m);
+ }
+ </style>
+ <div class="task_summary">
+ <table>
+ <tr>
+ <template is="dom-if" if="[[_can_show_summary(is_loading, ready_count, fail_count, invalid_count, waiting_count, duplicate_count, pass_count)]]">
+ <td class="key">Tasks</td>
+ <td class="value">
+ <gr-task-chip chip_style="loading" hidden$="[[!is_loading]]">loading...</gr-task-chip>
+ <gr-task-chip chip_style="fail" hidden$="[[!fail_count]]">[[fail_count]] blocked</gr-task-chip>
+ <gr-task-chip chip_style="invalid" hidden$="[[!invalid_count]]">[[invalid_count]] invalid</gr-task-chip>
+ <gr-task-chip chip_style="waiting" hidden$="[[!waiting_count]]">[[waiting_count]] waiting</gr-task-chip>
+ <gr-task-chip chip_style="ready" hidden$="[[!ready_count]]">[[ready_count]] needed</gr-task-chip>
+ <gr-task-chip chip_style="success" hidden$="[[!pass_count]]">[[pass_count]] passed</gr-task-chip>
+ <gr-task-chip chip_style="duplicate" hidden$="[[!duplicate_count]]">[[duplicate_count]] duplicate</gr-task-chip>
+ </template>
+ </td>
+ </tr>
+ </table>
+ </div>
+`;
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js
index 59d05b0..2219f12 100644
--- a/gr-task-plugin/plugin.js
+++ b/gr-task-plugin/plugin.js
@@ -16,8 +16,11 @@
*/
import './gr-task-plugin.js';
+import './gr-task-summary.js';
Gerrit.install(plugin => {
plugin.registerCustomComponent(
'change-view-integration', 'gr-task-plugin');
+ plugin.registerCustomComponent(
+ 'commit-container', 'gr-task-summary');
});
diff --git a/package.json b/package.json
index fac70e5..82c0e23 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
"name": "task",
- "version": "3.2.6-SNAPSHOT",
+ "version": "3.5.0-SNAPSHOT",
"description": "Task Plugin",
"dependencies": {
- "@bazel/rollup": "^3.4.0",
- "@bazel/terser": "^3.4.0"
+ "@bazel/rollup": "~5.1.0",
+ "@bazel/terser": "~5.1.0"
},
"devDependencies": {
"eslint": "^7.24.0",
diff --git a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
new file mode 100644
index 0000000..3869643
--- /dev/null
+++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -0,0 +1,184 @@
+// 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.
+
+/**
+ *
+ * This file defines the grammar used for Task Reference
+ *
+ * TASK_REFERENCE = [
+ * [ // TASK_FILE_PATH ] |
+ * [ @USERNAME [ TASK_FILE_PATH ] ] |
+ * [ %GROUP_NAME [ TASK_FILE_PATH ] ] |
+ * [ %%GROUP_UUID [ TASK_FILE_PATH ] ] |
+ * [ TASK_FILE_PATH ]
+ * ] '^' TASK_NAME
+ *
+ * Examples:
+ *
+ * file: All-Projects:refs/meta/config:task.config
+ * reference: foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: /foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: sub-dir/foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/dir/sub-dir/foo.config task: sample
+ *
+ * file: All-Projects:refs/meta/config:task/dir/bar.config
+ * reference: ^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task.config task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: @jim^sample
+ * Implied task:
+ * file: All-Users:refs/users/<jim>:task.config task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: @jim/foo^simple
+ * Implied task:
+ * file: All-Users:refs/users/<jim>:task/foo^simple task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: //foo.config^sample
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task/foo task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: //^simple
+ * Implied task:
+ * file: All-Projects:refs/meta/config:task.config task: sample
+ *
+ * Suppose a8341ade45d83e867c24a2d37f47b410cfdbea6d is the UUID of 'CI System Owners' group.
+ * file: Any projects, ref, file
+ * reference: %CI System Owners^sample
+ * Implied task:
+ * file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config
+ * task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: %CI System Owners/foo^simple
+ * Implied task:
+ * file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task/foo^simple
+ * task: sample
+ *
+ * file: Any projects, ref, file
+ * reference: %%a8341ade45d83e867c24a2d37f47b410cfdbea6d^sample
+ * Implied task:
+ * file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config
+ * task: sample
+ *
+ */
+
+grammar TaskReference;
+
+options {
+ language = Java;
+}
+
+reference
+ : file_path? TASK
+ ;
+
+file_path
+ : ALL_PROJECTS_ROOT
+ | FWD_SLASH absolute TASK_DELIMETER
+ | user absolute? TASK_DELIMETER
+ | group_name absolute? TASK_DELIMETER
+ | group_uuid absolute? TASK_DELIMETER
+ | (absolute| relative)? TASK_DELIMETER
+ ;
+
+user
+ : '@' NAME
+ ;
+
+group_name
+ : '%' (NAME | NAME_WITH_SPACES)
+ ;
+
+group_uuid
+ : '%%' INTERNAL_GROUP_UUID
+ ;
+
+absolute
+ : FWD_SLASH relative
+ ;
+
+relative
+ : dir* NAME
+ ;
+
+dir
+ : (NAME FWD_SLASH)
+ ;
+
+TASK
+ : (~'^')+ EOF
+ ;
+
+INTERNAL_GROUP_UUID
+ : HEX_10 HEX_10 HEX_10 HEX_10
+ ;
+
+fragment HEX_10
+ : HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX
+ ;
+
+fragment HEX
+ : [0-9a-f]
+ ;
+
+NAME
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH*
+ ;
+
+NAME_WITH_SPACES
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE (URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH | SPACE)*
+ ;
+
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH
+ : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE
+ | '@' | '%'
+ ;
+
+fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE
+ : ':' | '?' | '#' | '[' | ']'
+ |'!' | '$' | '&' | '\'' | '(' | ')'
+ | '*' | '+' | ',' | ';' | '='
+ | 'A'..'Z' | 'a'..'z' | '0'..'9'
+ | '_' | '.' | '\\' | '-' | '~'
+ ;
+
+fragment SPACE
+ : ' '
+ ;
+
+TASK_DELIMETER
+ : '^'
+ ;
+
+ALL_PROJECTS_ROOT
+ : FWD_SLASH FWD_SLASH TASK_DELIMETER
+ ;
+
+FWD_SLASH
+ : '/'
+ ;
diff --git a/src/main/java/com/google/gerrit/common/BooleanTable.java b/src/main/java/com/google/gerrit/common/BooleanTable.java
new file mode 100644
index 0000000..9e89eb9
--- /dev/null
+++ b/src/main/java/com/google/gerrit/common/BooleanTable.java
@@ -0,0 +1,69 @@
+// 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.common;
+
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A space efficient Table for Booleans. This Table takes advantage of the fact that the values
+ * stored in it are all Booleans and uses BitSets to make this very space efficient.
+ */
+public class BooleanTable<R, C> {
+ protected class Row {
+ public final BitSet hasValues = new BitSet();
+ public final BitSet values = new BitSet();
+
+ public void setPosition(int position, Boolean value) {
+ if (value != null) {
+ values.set(position, value);
+ }
+ hasValues.set(position, value != null);
+ }
+
+ public Boolean getPosition(int position) {
+ if (hasValues.get(position)) {
+ return values.get(position);
+ }
+ return null;
+ }
+ }
+
+ protected Map<R, Row> rowByRow = new HashMap<>();
+ protected Map<C, Integer> positionByColumn = new HashMap<>();
+ protected int highestPosition = -1;
+
+ public void put(R r, C c, Boolean v) {
+ Row row = rowByRow.computeIfAbsent(r, k -> new Row());
+ Integer columnPosition = positionByColumn.computeIfAbsent(c, k -> nextPosition());
+ row.setPosition(columnPosition, v);
+ }
+
+ protected int nextPosition() {
+ return ++highestPosition;
+ }
+
+ public Boolean get(R r, C c) {
+ Row row = rowByRow.get(r);
+ if (row != null) {
+ Integer columnPosition = positionByColumn.get(c);
+ if (columnPosition != null) {
+ return row.getPosition(columnPosition);
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java b/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java
new file mode 100644
index 0000000..d5a8ad1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.task;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class ConfigSourcedValue {
+ public static ConfigSourcedValue create(FileKey sourceFile, String value) {
+ return new AutoValue_ConfigSourcedValue(sourceFile, value);
+ }
+
+ public static Class<? extends ConfigSourcedValue> getClassType() {
+ return AutoValue_ConfigSourcedValue.class;
+ }
+
+ public abstract FileKey sourceFile();
+
+ public abstract String value();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
new file mode 100644
index 0000000..9856075
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
@@ -0,0 +1,35 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+
+/** An immutable reference to a fully qualified file in gerrit repo. */
+@AutoValue
+public abstract class FileKey {
+ public static FileKey create(Project.NameKey project, String branch, String file) {
+ return new AutoValue_FileKey(BranchNameKey.create(project, branch), file);
+ }
+
+ public static FileKey create(BranchNameKey branch, String file) {
+ return new AutoValue_FileKey(branch, file);
+ }
+
+ public abstract BranchNameKey branch();
+
+ public abstract String file();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java b/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java
new file mode 100644
index 0000000..472b1e7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java
@@ -0,0 +1,61 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SubmitRequirementPredicate;
+import com.google.inject.AbstractModule;
+
+// TODO: Remove this class when up-merging to Gerrit v3.6+ as it supports
+// the 'is:true' submit requirement
+public class IsTrueOperator implements ChangeQueryBuilder.ChangeIsOperandFactory {
+
+ public static final String TRUE = "true";
+
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(ChangeQueryBuilder.ChangeIsOperandFactory.class)
+ .annotatedWith(Exports.named(TRUE))
+ .to(IsTrueOperator.class);
+ }
+ }
+
+ public class TruePredicate extends SubmitRequirementPredicate {
+
+ public TruePredicate() {
+ super("is", TRUE);
+ }
+
+ @Override
+ public boolean match(ChangeData data) {
+ return true;
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+ }
+
+ @Override
+ public Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException {
+ return new IsTrueOperator.TruePredicate();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
index 45fe46d..0be04ba 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
@@ -14,44 +14,54 @@
package com.googlesource.gerrit.plugins.task;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
-import java.util.HashMap;
-import java.util.Map;
+import com.googlesource.gerrit.plugins.task.statistics.HitBooleanTable;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
public class MatchCache {
+ protected final HitBooleanTable<String, Change.Id> resultByChangeByQuery =
+ new HitBooleanTable<>();
protected final PredicateCache predicateCache;
- protected final ChangeData changeData;
- protected final Map<String, Boolean> matchResultByQuery = new HashMap<>();
-
- public MatchCache(PredicateCache predicateCache, ChangeData changeData) {
+ public MatchCache(PredicateCache predicateCache) {
this.predicateCache = predicateCache;
- this.changeData = changeData;
}
- protected boolean match(String query) throws StorageException, QueryParseException {
- if (query == null) {
+ public Boolean matchOrNull(ChangeData changeData, String query, boolean isVisible) {
+ try {
+ return match(changeData, query, isVisible);
+ } catch (StorageException | QueryParseException e) {
+ }
+ return null;
+ }
+
+ @SuppressWarnings("try")
+ public boolean match(ChangeData changeData, String query, boolean isVisible)
+ throws StorageException, QueryParseException {
+ if (query == null || "true".equalsIgnoreCase(query)) {
return true;
}
- Boolean isMatched = matchResultByQuery.get(query);
+ Boolean isMatched = resultByChangeByQuery.get(query, changeData.getId());
if (isMatched == null) {
- isMatched = predicateCache.match(changeData, query);
- matchResultByQuery.put(query, isMatched);
+ Matchable<ChangeData> matchable = predicateCache.getPredicate(query, isVisible).asMatchable();
+ try (StopWatch stopWatch =
+ resultByChangeByQuery.createLoadingStopWatch(query, changeData.getId(), isVisible)) {
+ isMatched = matchable.match(changeData);
+ resultByChangeByQuery.put(query, changeData.getId(), isMatched);
+ }
}
return isMatched;
}
- protected Boolean matchOrNull(String query) {
- if (query == null) {
- return null;
- }
- Boolean isMatched = matchResultByQuery.get(query);
- if (isMatched == null) {
- isMatched = predicateCache.matchOrNull(changeData, query);
- matchResultByQuery.put(query, isMatched);
- }
- return isMatched;
+ public void initStatistics(int summaryCount) {
+ resultByChangeByQuery.initStatistics(summaryCount);
+ }
+
+ public Object getStatistics() {
+ return resultByChangeByQuery.getStatistics();
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
index 3a8d903..3451836 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -15,6 +15,8 @@
package com.googlesource.gerrit.plugins.task;
import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.webui.JavaScriptPlugin;
import com.google.gerrit.extensions.webui.WebUiPlugin;
@@ -23,7 +25,6 @@
import com.google.gerrit.server.restapi.change.GetChange;
import com.google.gerrit.server.restapi.change.QueryChanges;
import com.google.gerrit.sshd.commands.Query;
-import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
import java.util.ArrayList;
@@ -31,13 +32,24 @@
import org.kohsuke.args4j.Option;
public class Modules {
- public static class Module extends AbstractModule {
+ public static class Module extends FactoryModule {
@Override
protected void configure() {
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named(ViewPathsCapability.VIEW_PATHS))
+ .to(ViewPathsCapability.class);
+ factory(TaskPath.Factory.class);
+ factory(TaskReference.Factory.class);
+ factory(TaskExpression.Factory.class);
+ factory(TaskTree.Factory.class);
+ factory(Preloader.Factory.class);
+
bind(ChangePluginDefinedInfoFactory.class)
.annotatedWith(Exports.named("task"))
.to(TaskAttributeFactory.class);
+ install(new IsTrueOperator.Module());
+
bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class);
bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class);
bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class);
@@ -47,6 +59,13 @@
}
public static class MyOptions implements DynamicBean {
+ @Option(
+ name = "--only",
+ usage =
+ "Evaluate tasks under this task only. Only root task names are supported."
+ + " This option can be provided multiple times.")
+ public List<String> includedRoots = new ArrayList<>();
+
@Option(name = "--all", usage = "Include all visible tasks in the output")
public boolean all = false;
@@ -58,9 +77,18 @@
usage = "Include only invalid tasks and the tasks referencing them in the output")
public boolean onlyInvalid = false;
+ @Option(name = "--include-paths", usage = "Include absolute path to each task")
+ public boolean includePaths = false;
+
@Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task")
public boolean evaluationTime = false;
+ @Option(name = "--include-statistics", usage = "Include statistcs about the task evaluations")
+ public boolean includeStatistics = false;
+
+ @Option(name = "--summary-count", usage = "number of items to output in statistics summaries")
+ public int summaryCount = 5;
+
@Option(
name = "--preview",
metaVar = "{CHANGE,PATCHSET}",
@@ -78,5 +106,9 @@
public MyOptions(PatchSetArgument.Factory patchSetArgumentFactory) {
this.patchSetArgumentFactory = patchSetArgumentFactory;
}
+
+ public boolean shouldFilterRoot(String rootName) {
+ return !includedRoots.isEmpty() && !includedRoots.contains(rootName);
+ }
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
index 7896417..268b1a3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -14,63 +14,81 @@
package com.googlesource.gerrit.plugins.task;
-import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.query.AndPredicate;
+import com.google.gerrit.index.query.NotPredicate;
+import com.google.gerrit.index.query.OrPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeIndexPredicate;
+import com.google.gerrit.server.query.change.DestinationPredicate;
+import com.google.gerrit.server.query.change.RegexProjectPredicate;
+import com.google.gerrit.server.query.change.RegexRefPredicate;
+import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder;
import com.google.inject.Inject;
-import java.util.HashMap;
-import java.util.Map;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import com.googlesource.gerrit.plugins.task.util.ThrowingProvider;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
public class PredicateCache {
- protected final ChangeQueryBuilder cqb;
- protected final CurrentUser user;
+ public static class Statistics {
+ protected Object predicatesByQueryCache;
+ }
- protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
- predicatesByQuery = new HashMap<>();
+ protected final SubmitRequirementChangeQueryBuilder srcqb;
+ protected final Set<String> cacheableByBranchPredicateClassNames;
+ protected final CurrentUser user;
+ protected final HitHashMap<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>>
+ predicatesByQuery = new HitHashMap<>();
+
+ protected Statistics statistics;
@Inject
- public PredicateCache(CurrentUser user, ChangeQueryBuilder cqb) {
+ public PredicateCache(
+ @GerritServerConfig Config config,
+ @PluginName String pluginName,
+ CurrentUser user,
+ SubmitRequirementChangeQueryBuilder srcqb) {
this.user = user;
- this.cqb = cqb;
+ this.srcqb = srcqb;
+ cacheableByBranchPredicateClassNames =
+ new HashSet<>(
+ Arrays.asList(
+ config.getStringList(pluginName, "cacheable-predicates", "byBranch-className")));
}
- public boolean match(ChangeData c, String query) throws StorageException, QueryParseException {
- if (query == null) {
- return true;
+ public void initStatistics(int summaryCount) {
+ statistics = new Statistics();
+ predicatesByQuery.initStatistics(summaryCount);
+ }
+
+ public Object getStatistics() {
+ if (statistics != null) {
+ statistics.predicatesByQueryCache = predicatesByQuery.getStatistics();
}
- return matchWithExceptions(c, query);
+ return statistics;
}
- public Boolean matchOrNull(ChangeData c, String query) {
- if (query != null) {
- try {
- return matchWithExceptions(c, query);
- } catch (QueryParseException | RuntimeException e) {
- }
- }
- return null;
- }
-
- protected boolean matchWithExceptions(ChangeData c, String query)
- throws QueryParseException, StorageException {
- if ("true".equalsIgnoreCase(query)) {
- return true;
- }
- return getPredicate(query).asMatchable().match(c);
- }
-
- protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException {
+ @SuppressWarnings("try")
+ public Predicate<ChangeData> getPredicate(String query, boolean isVisible)
+ throws QueryParseException {
ThrowingProvider<Predicate<ChangeData>, QueryParseException> predProvider =
predicatesByQuery.get(query);
if (predProvider != null) {
return predProvider.get();
}
// never seen 'query' before
- try {
- Predicate<ChangeData> pred = cqb.parse(query);
+ try (StopWatch stopWatch = predicatesByQuery.createLoadingStopWatch(query, isVisible)) {
+ Predicate<ChangeData> pred = srcqb.parse(query);
predicatesByQuery.put(query, new ThrowingProvider.Entry<>(pred));
return pred;
} catch (QueryParseException e) {
@@ -78,4 +96,43 @@
throw e;
}
}
+
+ /**
+ * Can this query's output be assumed to be constant given any Change destined for the same
+ * Branch.NameKey?
+ */
+ public boolean isCacheableByBranch(String query, boolean isVisible) throws QueryParseException {
+ if (query == null
+ || "".equals(query)
+ || "false".equalsIgnoreCase(query)
+ || "true".equalsIgnoreCase(query)) {
+ return true;
+ }
+ return isCacheableByBranch(getPredicate(query, isVisible));
+ }
+
+ protected boolean isCacheableByBranch(Predicate<ChangeData> predicate) {
+ if (predicate instanceof AndPredicate
+ || predicate instanceof NotPredicate
+ || predicate instanceof OrPredicate) {
+ for (Predicate<ChangeData> subPred : predicate.getChildren()) {
+ if (!isCacheableByBranch(subPred)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (predicate instanceof DestinationPredicate
+ || predicate instanceof RegexProjectPredicate
+ || predicate instanceof RegexRefPredicate) {
+ return true;
+ }
+ if (predicate instanceof ChangeIndexPredicate) {
+ FieldDef<ChangeData, ?> field = ((ChangeIndexPredicate) predicate).getField();
+ if (field.equals(ChangeField.PROJECT) || field.equals(ChangeField.REF)) {
+ return true;
+ }
+ }
+ return cacheableByBranchPredicateClassNames.contains(predicate.getClass().getName());
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
index 8babe1c..7b325cf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -14,68 +14,181 @@
package com.googlesource.gerrit.plugins.task;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
+import com.googlesource.gerrit.plugins.task.statistics.StatisticsMap;
+import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
/** Use to pre-load a task definition with values from its preload-task definition. */
public class Preloader {
- public static void preload(Task definition) throws ConfigInvalidException {
- String name = definition.preloadTask;
- if (name != null) {
- Task task = definition.config.getTaskOptional(name);
- if (task != null) {
- preload(task);
- preloadFrom(definition, task);
- }
- }
+ public interface Factory {
+ Preloader create(@Assisted TaskConfigCache taskConfigCache);
}
- protected static void preloadFrom(Task definition, Task preloadFrom) {
+ public static class Statistics {
+ protected Object optionalTaskByExpressionCache;
+ protected long loaded;
+ protected long preloaded;
+ protected long preloadedFromDefinition;
+ }
+
+ protected final TaskConfigCache taskConfigCache;
+ protected final TaskExpression.Factory taskExpressionFactory;
+ protected final StatisticsMap<TaskExpressionKey, Optional<Task>> optionalTaskByExpression =
+ new HitHashMap<>();
+
+ protected Statistics statistics;
+
+ @Inject
+ public Preloader(
+ TaskExpression.Factory taskExpressionFactory, @Assisted TaskConfigCache taskConfigCache) {
+ this.taskConfigCache = taskConfigCache;
+ this.taskExpressionFactory = taskExpressionFactory;
+ }
+
+ public List<Task> getRootTasks() throws IOException, ConfigInvalidException {
+ return getTasks(taskConfigCache.getRootConfig(), TaskConfig.SECTION_ROOT);
+ }
+
+ public List<Task> getTasks(FileKey file) throws IOException, ConfigInvalidException {
+ return getTasks(taskConfigCache.getTaskConfig(file), TaskConfig.SECTION_TASK);
+ }
+
+ protected List<Task> getTasks(TaskConfig cfg, String type) throws IOException {
+ List<Task> preloaded = new ArrayList<>();
+ for (Task task : cfg.getTasks(type)) {
+ try {
+ preloaded.add(preload(task));
+ } catch (ConfigInvalidException e) {
+ preloaded.add(null);
+ }
+ }
+ return preloaded;
+ }
+
+ boolean inGetOptionalTask;
+
+ /**
+ * Get a preloaded Task for this TaskExpression.
+ *
+ * @param expression
+ * @return Optional<Task> which is empty if the expression is optional and no tasks are resolved
+ * @throws ConfigInvalidException if the expression requires a task and no tasks are resolved
+ */
+ public Optional<Task> getOptionalTask(TaskExpression expression)
+ throws ConfigInvalidException, IOException {
+ Optional<Task> task = optionalTaskByExpression.get(expression.key);
+ if (task == null) {
+ boolean firstInGetOptionalTask = !inGetOptionalTask;
+ inGetOptionalTask = true;
+ task = preloadOptionalTask(expression);
+ optionalTaskByExpression.put(expression.key, task);
+ if (firstInGetOptionalTask) {
+ inGetOptionalTask = false;
+ }
+ }
+ return task;
+ }
+
+ protected Optional<Task> preloadOptionalTask(TaskExpression expression)
+ throws ConfigInvalidException, IOException {
+ Optional<Task> definition = loadOptionalTask(expression);
+ return definition.isPresent() ? Optional.of(preload(definition.get())) : definition;
+ }
+
+ public Task preload(Task definition) throws ConfigInvalidException, IOException {
+ if (statistics != null && !inGetOptionalTask) {
+ statistics.preloadedFromDefinition++;
+ }
+ String expression = definition.preloadTask;
+ if (expression != null) {
+ if (statistics != null) {
+ statistics.preloaded++;
+ }
+ Optional<Task> preloadFrom =
+ getOptionalTask(taskExpressionFactory.create(definition.file(), expression));
+ if (preloadFrom.isPresent()) {
+ return preloadFrom(definition, preloadFrom.get());
+ }
+ }
+ return definition;
+ }
+
+ protected Optional<Task> loadOptionalTask(TaskExpression expression)
+ throws ConfigInvalidException, IOException {
+ if (statistics != null) {
+ statistics.loaded++;
+ }
+ try {
+ for (TaskKey key : expression) {
+ Optional<Task> task = getOptionalTask(key);
+ if (task.isPresent()) {
+ return task;
+ }
+ }
+ } catch (RuntimeConfigInvalidException e) {
+ throw e.checkedException;
+ } catch (NoSuchElementException e) {
+ // expression was not optional but we ran out of names to try
+ throw new ConfigInvalidException("task not defined");
+ }
+ return Optional.empty();
+ }
+
+ protected static Task preloadFrom(Task definition, Task preloadFrom) {
+ Task preloadTo = definition.config.new Task(definition.subSection);
for (Field field : definition.getClass().getFields()) {
String name = field.getName();
- if ("isVisible".equals(name) || "isTrusted".equals(name) || "config".equals(name)) {
+ if ("config".equals(name)) {
continue;
}
try {
field.setAccessible(true);
- preloadField(field.getType(), field, definition, preloadFrom);
+ preloadField(field, definition, preloadFrom, preloadTo);
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new RuntimeException();
}
}
+ return preloadTo;
}
- protected static <T, S, K, V> void preloadField(
- Class<T> clz, Field field, Task definition, Task preloadFrom)
+ protected Optional<Task> getOptionalTask(TaskKey key) throws IOException, ConfigInvalidException {
+ return taskConfigCache.getTaskConfig(key.subSection().file()).getOptionalTask(key.task());
+ }
+
+ protected static <S, K, V> void preloadField(
+ Field field, Task definition, Task preloadFrom, Task preloadTo)
throws IllegalArgumentException, IllegalAccessException {
- T pre = getField(clz, field, preloadFrom);
- if (pre != null) {
- T val = getField(clz, field, definition);
- if (val == null) {
- field.set(definition, pre);
- } else if (val instanceof List) {
- List<?> valList = List.class.cast(val);
- List<?> preList = List.class.cast(pre);
- field.set(definition, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
- } else if (val instanceof Map) {
- Map<?, ?> valMap = Map.class.cast(val);
- Map<?, ?> preMap = Map.class.cast(pre);
- field.set(definition, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
- } // nothing to do for overridden preloaded scalars
+ Object pre = field.get(preloadFrom);
+ Object val = field.get(definition);
+ if (val == null) {
+ field.set(preloadTo, pre);
+ } else if (pre == null) {
+ field.set(preloadTo, val);
+ } else if (val instanceof List) {
+ List<?> valList = List.class.cast(val);
+ List<?> preList = List.class.cast(pre);
+ field.set(preloadTo, preloadListFrom(castUnchecked(valList), castUnchecked(preList)));
+ } else if (val instanceof Map) {
+ Map<?, ?> valMap = Map.class.cast(val);
+ Map<?, ?> preMap = Map.class.cast(pre);
+ field.set(preloadTo, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap)));
+ } else {
+ field.set(preloadTo, val);
}
}
- protected static <T> T getField(Class<T> clz, Field field, Object obj)
- throws IllegalArgumentException, IllegalAccessException {
- return clz.cast(field.get(obj));
- }
-
@SuppressWarnings("unchecked")
protected static <S> List<S> castUnchecked(List<?> list) {
List<S> forceCheck = (List<S>) list;
@@ -89,28 +202,40 @@
}
protected static <T> List<T> preloadListFrom(List<T> list, List<T> preList) {
- List<T> extended = list;
- if (!preList.isEmpty()) {
- extended = preList;
- if (!list.isEmpty()) {
- extended = new ArrayList<>(list.size() + preList.size());
- extended.addAll(preList);
- extended.addAll(list);
- }
+ if (preList.isEmpty()) {
+ return list;
}
+ if (list.isEmpty()) {
+ return preList;
+ }
+
+ List<T> extended = new ArrayList<>(list.size() + preList.size());
+ extended.addAll(preList);
+ extended.addAll(list);
return extended;
}
protected static <K, V> Map<K, V> preloadMapFrom(Map<K, V> map, Map<K, V> preMap) {
- Map<K, V> extended = map;
- if (!preMap.isEmpty()) {
- extended = preMap;
- if (!map.isEmpty()) {
- extended = new HashMap<>(map.size() + preMap.size());
- extended.putAll(preMap);
- extended.putAll(map);
- }
+ if (preMap.isEmpty()) {
+ return map;
}
+ if (map.isEmpty()) {
+ return preMap;
+ }
+
+ Map<K, V> extended = new HashMap<>(map.size() + preMap.size());
+ extended.putAll(preMap);
+ extended.putAll(map);
return extended;
}
+
+ public void initStatistics(int summaryCount) {
+ statistics = new Statistics();
+ optionalTaskByExpression.initStatistics(summaryCount);
+ }
+
+ public Statistics getStatistics() {
+ statistics.optionalTaskByExpressionCache = optionalTaskByExpression.getStatistics();
+ return statistics;
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
deleted file mode 100644
index 028762c..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java
+++ /dev/null
@@ -1,215 +0,0 @@
-// Copyright (C) 2019 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.googlesource.gerrit.plugins.task;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
-import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/** Use to expand properties like ${_name} in the text of various definitions. */
-public class Properties {
- /** Use to expand properties specifically for Tasks. */
- public static class Task extends Expander {
- public static final Task EMPTY_PARENT = new Task();
-
- public Task() {
- super(Collections.emptyMap());
- }
-
- public Task(ChangeData changeData, TaskConfig.Task definition, Task parentProperties)
- throws StorageException {
- super(parentProperties.forDescendants());
- valueByName.putAll(getInternalProperties(definition, changeData));
- new RecursiveExpander(valueByName).expand(definition.getAllProperties());
-
- definition.setExpandedProperties(valueByName);
-
- expandFieldValues(definition, Collections.emptySet());
- }
-
- protected Map<String, String> forDescendants() {
- return new HashMap<>(valueByName);
- }
- }
-
- /** Use to expand properties specifically for NamesFactories. */
- public static class NamesFactory extends Expander {
- public NamesFactory(TaskConfig.NamesFactory namesFactory, Task properties) {
- super(properties.valueByName);
- expandFieldValues(namesFactory, Sets.newHashSet(TaskConfig.KEY_TYPE));
- }
- }
-
- protected static Map<String, String> getInternalProperties(
- TaskConfig.Task definition, ChangeData changeData) throws StorageException {
- Map<String, String> properties = new HashMap<>();
-
- properties.put("_name", definition.name);
-
- Change c = changeData.change();
- properties.put("_change_number", String.valueOf(c.getId().get()));
- properties.put("_change_id", c.getKey().get());
- properties.put("_change_project", c.getProject().get());
- properties.put("_change_branch", c.getDest().branch());
- properties.put("_change_status", c.getStatus().toString());
- properties.put("_change_topic", c.getTopic());
-
- return properties;
- }
-
- /**
- * Use to expand properties whose values may contain other references to properties.
- *
- * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there
- * are no looping definitions.
- *
- * <p>Given some property name/value asssociations defined like this:
- *
- * <p><code>
- * valueByName.put("obstacle", "fence");
- * valueByName.put("action", "jumped over the ${obstacle}");
- * </code>
- *
- * <p>a String like: <code>"The brown fox ${action}."</code>
- *
- * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
- */
- protected static class RecursiveExpander {
- protected final Expander expander;
- protected Map<String, String> unexpandedByName;
- protected Set<String> expanding;
-
- public RecursiveExpander(Map<String, String> valueByName) {
- expander =
- new Expander(valueByName) {
- @Override
- protected String getValueForName(String name) {
- expandUnexpanded(name); // recursive call
- return super.getValueForName(name);
- }
- };
- }
-
- public void expand(Map<String, String> unexpandedByName) {
- this.unexpandedByName = unexpandedByName;
-
- // Copy keys to allow out of order removals during iteration
- for (String unexpanedName : new ArrayList<>(unexpandedByName.keySet())) {
- expanding = new HashSet<>();
- expandUnexpanded(unexpanedName);
- }
- }
-
- protected void expandUnexpanded(String name) {
- if (!expanding.add(name)) {
- throw new RuntimeException("Looping property definitions.");
- }
- String value = unexpandedByName.remove(name);
- if (value != null) {
- expander.valueByName.put(name, expander.expandText(value));
- }
- }
- }
-
- /**
- * Use to expand properties like ${property} in Strings into their values.
- *
- * <p>Given some property name/value asssociations defined like this:
- *
- * <p><code>
- * valueByName.put("animal", "fox");
- * valueByName.put("bar", "foo");
- * valueByName.put("obstacle", "fence");
- * </code>
- *
- * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code>
- *
- * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
- */
- protected static class Expander {
- // "${_name}" -> group(1) = "_name"
- protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
-
- public final Map<String, String> valueByName;
-
- public Expander(Map<String, String> valueByName) {
- this.valueByName = valueByName;
- }
-
- /** Expand all properties in the Strings in the object's Fields (except the exclude ones) */
- protected void expandFieldValues(Object object, Set<String> excludedFieldNames) {
- for (Field field : object.getClass().getFields()) {
- try {
- if (!excludedFieldNames.contains(field.getName())) {
- field.setAccessible(true);
- Object o = field.get(object);
- if (o instanceof String) {
- field.set(object, expandText((String) o));
- } else if (o instanceof List) {
- @SuppressWarnings("unchecked")
- List<String> forceCheck = List.class.cast(o);
- expandElements(forceCheck);
- }
- }
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e);
- }
- }
- }
-
- /** Expand all properties in the Strings in the List */
- public void expandElements(List<String> list) {
- if (list != null) {
- for (ListIterator<String> it = list.listIterator(); it.hasNext(); ) {
- it.set(expandText(it.next()));
- }
- }
- }
-
- /** Expand all properties (${property_name} -> property_value) in the given text */
- public String expandText(String text) {
- if (text == null) {
- return null;
- }
- StringBuffer out = new StringBuffer();
- Matcher m = PATTERN.matcher(text);
- while (m.find()) {
- m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1))));
- }
- m.appendTail(out);
- return out.toString();
- }
-
- /** Get the replacement value for the property identified by name */
- protected String getValueForName(String name) {
- String value = valueByName.get(name);
- return value == null ? "" : value;
- }
- }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
new file mode 100644
index 0000000..cc0ed94
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
@@ -0,0 +1,27 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class RuntimeConfigInvalidException extends RuntimeException {
+ protected static final long serialVersionUID = 1L;
+ protected ConfigInvalidException checkedException;
+
+ public RuntimeConfigInvalidException(ConfigInvalidException e) {
+ super(e);
+ this.checkedException = e;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
new file mode 100644
index 0000000..54db1e4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
@@ -0,0 +1,31 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.auto.value.AutoValue;
+
+/** An immutable reference to a SubSection in fully qualified task config file. */
+@AutoValue
+public abstract class SubSectionKey {
+ public static SubSectionKey create(FileKey file, String section, String subSection) {
+ return new AutoValue_SubSectionKey(file, section, subSection == null ? "" : subSection);
+ }
+
+ public abstract FileKey file();
+
+ public abstract String section();
+
+ public abstract String subSection();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
index 1e19f7b..ca1ae2c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -14,13 +14,14 @@
package com.googlesource.gerrit.plugins.task;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Change;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.common.PluginDefinedInfo;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.DynamicOptions.BeanProvider;
import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
@@ -29,33 +30,64 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory {
- private static final FluentLogger log = FluentLogger.forEnclosingClass();
+ public static final TaskPath MISSING_VIEW_PATH_CAPABILITY =
+ new TaskPath(
+ String.format(
+ "Can't perform operation, need %s capability", ViewPathsCapability.VIEW_PATHS));
+
public enum Status {
INVALID,
UNKNOWN,
+ DUPLICATE,
WAITING,
READY,
PASS,
FAIL;
}
+ public static class Statistics {
+ public long numberOfChanges;
+ public long numberOfChangeNodes;
+ public long numberOfDuplicates;
+ public long numberOfNodes;
+ public long numberOfTaskPluginAttributes;
+ public Object predicateCache;
+ public Object matchCache;
+ public Preloader.Statistics preloader;
+ public TaskTree.Statistics treeCaches;
+ }
+
public static class TaskAttribute {
+ public static class Statistics {
+ public boolean isApplicableRefreshRequired;
+ public boolean isSubNodeReloadRequired;
+ public boolean isTaskRefreshNeeded;
+ public Boolean hasUnfilterableSubNodes;
+ public Object nodesByBranchCache;
+ public Object properties;
+ }
+
public Boolean applicable;
public Map<String, String> exported;
public Boolean hasPass;
public String hint;
public Boolean inProgress;
+ public TaskPath path;
public String name;
+ public Integer change;
public Status status;
public List<TaskAttribute> subTasks;
public Long evaluationMilliSeconds;
+ public Statistics statistics;
public TaskAttribute(String name) {
this.name = name;
@@ -64,29 +96,53 @@
public static class TaskPluginAttribute extends PluginDefinedInfo {
public List<TaskAttribute> roots = new ArrayList<>();
+ public Statistics queryStatistics;
}
+ protected final String pluginName;
protected final TaskTree definitions;
protected final PredicateCache predicateCache;
+ protected final boolean hasViewPathsCapability;
+ protected final TaskPath.Factory taskPathFactory;
+ protected final TaskConfigCache taskConfigCache;
protected Modules.MyOptions options;
+ protected TaskPluginAttribute lastTaskPluginAttribute;
+ protected Statistics statistics;
@Inject
- public TaskAttributeFactory(TaskTree definitions, PredicateCache predicateCache) {
- this.definitions = definitions;
+ public TaskAttributeFactory(
+ String pluginName,
+ TaskTree.Factory taskTreeFactory,
+ PredicateCache predicateCache,
+ PermissionBackend permissionBackend,
+ TaskPath.Factory taskPathFactory,
+ TaskConfigCache taskConfigCache) {
+ this.pluginName = pluginName;
+ this.definitions = taskTreeFactory.create(taskConfigCache);
this.predicateCache = predicateCache;
+ this.hasViewPathsCapability =
+ permissionBackend
+ .currentUser()
+ .testOrFalse(new PluginPermission(this.pluginName, ViewPathsCapability.VIEW_PATHS));
+ this.taskPathFactory = taskPathFactory;
+ this.taskConfigCache = taskConfigCache;
}
@Override
public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
- Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
+ Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) {
Map<Change.Id, PluginDefinedInfo> pluginInfosByChange = new HashMap<>();
options = (Modules.MyOptions) beanProvider.getDynamicBean(plugin);
if (options.all || options.onlyApplicable || options.onlyInvalid) {
+ initStatistics();
for (PatchSetArgument psa : options.patchSetArguments) {
- definitions.masquerade(psa);
+ taskConfigCache.masquerade(psa);
}
cds.forEach(cd -> pluginInfosByChange.put(cd.getId(), createWithExceptions(cd)));
+ if (lastTaskPluginAttribute != null) {
+ lastTaskPluginAttribute.queryStatistics = getStatistics(pluginInfosByChange);
+ }
}
return pluginInfosByChange;
}
@@ -94,38 +150,47 @@
protected PluginDefinedInfo createWithExceptions(ChangeData c) {
TaskPluginAttribute a = new TaskPluginAttribute();
try {
- for (Node node : definitions.getRootNodes(c)) {
- if (node == null) {
+ for (Node root : definitions.getRootNodes(c)) {
+ if (root instanceof Node.Invalid) {
a.roots.add(invalid());
} else {
- new AttributeFactory(node).create().ifPresent(t -> a.roots.add(t));
+ if (options.shouldFilterRoot(root.task.name())) {
+ continue;
+ }
+ new AttributeFactory(root).create().ifPresent(t -> a.roots.add(t));
}
}
- } catch (ConfigInvalidException | IOException e) {
+ } catch (ConfigInvalidException | IOException | StorageException e) {
a.roots.add(invalid());
}
if (a.roots.isEmpty()) {
return null;
}
+ lastTaskPluginAttribute = a;
return a;
}
protected class AttributeFactory {
public Node node;
- public MatchCache matchCache;
protected Task task;
protected TaskAttribute attribute;
protected AttributeFactory(Node node) {
- this(node, new MatchCache(predicateCache, node.getChangeData()));
- }
-
- protected AttributeFactory(Node node, MatchCache matchCache) {
this.node = node;
- this.matchCache = matchCache;
this.task = node.task;
- this.attribute = new TaskAttribute(task.name);
+ attribute = new TaskAttribute(task.name());
+ if (options.includeStatistics) {
+ statistics.numberOfNodes++;
+ if (node.isChange()) {
+ statistics.numberOfChangeNodes++;
+ }
+ if (node.isDuplicate) {
+ statistics.numberOfDuplicates++;
+ }
+ attribute.statistics = new TaskAttribute.Statistics();
+ attribute.statistics.properties = node.propertiesStatistics;
+ }
}
public Optional<TaskAttribute> create() {
@@ -134,20 +199,37 @@
attribute.evaluationMilliSeconds = millis();
}
- boolean applicable = matchCache.match(task.applicable);
+ boolean applicable;
+ try {
+ applicable = node.match(task.applicable);
+ } catch (QueryParseException e) {
+ return Optional.of(invalid());
+ }
if (!task.isVisible) {
- if (!task.isTrusted || (!applicable && !options.onlyApplicable)) {
+ if (!node.isTrusted() || (!applicable && !options.onlyApplicable)) {
return Optional.of(unknown());
}
}
if (applicable || !options.onlyApplicable) {
- attribute.hasPass = task.pass != null || task.fail != null;
- attribute.subTasks = getSubTasks();
+ if (node.isChange()) {
+ attribute.change = node.getChangeData().getId().get();
+ }
+ attribute.hasPass = !node.isDuplicate && (task.pass != null || task.fail != null);
+ if (!node.isDuplicate) {
+ attribute.subTasks = getSubTasks();
+ }
attribute.status = getStatus();
if (options.onlyInvalid && !isValidQueries()) {
attribute.status = Status.INVALID;
}
+ if (options.includePaths) {
+ if (hasViewPathsCapability) {
+ attribute.path = taskPathFactory.create(node.taskKey);
+ } else {
+ attribute.path = MISSING_VIEW_PATH_CAPABILITY;
+ }
+ }
boolean groupApplicable = attribute.status != null;
if (groupApplicable || !options.onlyApplicable) {
@@ -157,26 +239,54 @@
if (!options.onlyApplicable) {
attribute.applicable = applicable;
}
- if (task.inProgress != null) {
- attribute.inProgress = matchCache.matchOrNull(task.inProgress);
+ if (!node.isDuplicate) {
+ if (task.inProgress != null) {
+ attribute.inProgress = node.matchOrNull(task.inProgress);
+ }
+ attribute.exported = task.exported.isEmpty() ? null : task.exported;
}
attribute.hint = getHint(attribute.status, task);
- attribute.exported = task.exported.isEmpty() ? null : task.exported;
if (options.evaluationTime) {
attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds;
}
+ addStatistics(attribute.statistics);
return Optional.of(attribute);
}
}
}
- } catch (QueryParseException | RuntimeException e) {
+ } catch (IOException | RuntimeException | ConfigInvalidException e) {
return Optional.of(invalid()); // bad applicability query
}
return Optional.empty();
}
+ protected TaskAttribute invalid() {
+ TaskAttribute invalid = TaskAttributeFactory.invalid();
+ if (task.isVisible) {
+ invalid.name = task.name();
+ }
+ return invalid;
+ }
+
+ public void addStatistics(TaskAttribute.Statistics statistics) {
+ if (statistics != null) {
+ statistics.isApplicableRefreshRequired = node.properties.isApplicableRefreshRequired();
+ statistics.isSubNodeReloadRequired = node.properties.isSubNodeReloadRequired();
+ statistics.isTaskRefreshNeeded = node.properties.isTaskRefreshRequired();
+ if (!statistics.isSubNodeReloadRequired) {
+ statistics.hasUnfilterableSubNodes = node.hasUnfilterableSubNodes;
+ }
+ if (node.nodesByBranch != null) {
+ statistics.nodesByBranchCache = node.nodesByBranch.getStatistics();
+ }
+ }
+ }
+
protected Status getStatusWithExceptions() throws StorageException, QueryParseException {
+ if (node.isDuplicate) {
+ return Status.DUPLICATE;
+ }
if (isAllNull(task.pass, task.fail, attribute.subTasks)) {
// A leaf def has no defined subdefs.
boolean hasDefinedSubtasks =
@@ -198,7 +308,7 @@
}
if (task.fail != null) {
- if (matchCache.match(task.fail)) {
+ if (node.match(task.fail)) {
// A FAIL definition is meant to be a hard blocking criteria
// (like a CodeReview -2). Thus, if hard blocked, it is
// irrelevant what the subtask states, or the PASS criteria are.
@@ -212,7 +322,8 @@
}
}
- if (attribute.subTasks != null && !isAll(attribute.subTasks, Status.PASS)) {
+ if (attribute.subTasks != null
+ && !isAll(attribute.subTasks, EnumSet.of(Status.PASS, Status.DUPLICATE))) {
// It is possible for a subtask's PASS criteria to change while
// a parent task is executing, or even after the parent task
// completes. This can result in the parent PASS criteria being
@@ -224,7 +335,7 @@
return Status.WAITING;
}
- if (task.pass != null && !matchCache.match(task.pass)) {
+ if (task.pass != null && !node.match(task.pass)) {
// Non-leaf tasks with no PASS criteria are supported in order
// to support "grouping tasks" (tasks with no function aside from
// organizing tasks). A task without a PASS criteria, cannot ever
@@ -245,13 +356,15 @@
}
}
- protected List<TaskAttribute> getSubTasks() throws StorageException {
+ protected List<TaskAttribute> getSubTasks()
+ throws IOException, StorageException, ConfigInvalidException {
List<TaskAttribute> subTasks = new ArrayList<>();
- for (Node subNode : node.getSubNodes()) {
- if (subNode == null) {
- subTasks.add(invalid());
+ for (Node subNode :
+ options.onlyApplicable ? node.getApplicableSubNodes() : node.getSubNodes()) {
+ if (subNode instanceof Node.Invalid) {
+ subTasks.add(TaskAttributeFactory.invalid());
} else {
- new AttributeFactory(subNode, matchCache).create().ifPresent(t -> subTasks.add(t));
+ new AttributeFactory(subNode).create().ifPresent(t -> subTasks.add(t));
}
}
if (subTasks.isEmpty()) {
@@ -262,9 +375,9 @@
protected boolean isValidQueries() {
try {
- matchCache.match(task.inProgress);
- matchCache.match(task.fail);
- matchCache.match(task.pass);
+ node.match(task.inProgress);
+ node.match(task.fail);
+ node.match(task.pass);
return true;
} catch (QueryParseException | RuntimeException e) {
return false;
@@ -276,7 +389,30 @@
return System.nanoTime() / 1000000;
}
- protected TaskAttribute invalid() {
+ public void initStatistics() {
+ if (options.includeStatistics) {
+ statistics = new Statistics();
+ definitions.predicateCache.initStatistics(options.summaryCount);
+ definitions.matchCache.initStatistics(options.summaryCount);
+ definitions.preloader.initStatistics(options.summaryCount);
+ definitions.initStatistics(options.summaryCount);
+ }
+ }
+
+ public Statistics getStatistics(Map<Change.Id, PluginDefinedInfo> pluginInfosByChange) {
+ if (statistics != null) {
+ statistics.numberOfChanges = pluginInfosByChange.size();
+ statistics.numberOfTaskPluginAttributes =
+ pluginInfosByChange.values().stream().filter(tpa -> tpa != null).count();
+ statistics.predicateCache = definitions.predicateCache.getStatistics();
+ statistics.matchCache = definitions.matchCache.getStatistics();
+ statistics.preloader = definitions.preloader.getStatistics();
+ statistics.treeCaches = definitions.getStatistics();
+ }
+ return statistics;
+ }
+
+ protected static TaskAttribute invalid() {
// For security reasons, do not expose the task name without knowing
// the visibility which is derived from its applicability.
TaskAttribute a = unknown();
@@ -284,17 +420,23 @@
return a;
}
- protected TaskAttribute unknown() {
+ protected static TaskAttribute unknown() {
TaskAttribute a = new TaskAttribute("UNKNOWN");
a.status = Status.UNKNOWN;
return a;
}
- protected String getHint(Status status, Task def) {
- if (status == Status.READY) {
- return def.readyHint;
- } else if (status == Status.FAIL) {
- return def.failHint;
+ protected static String getHint(Status status, Task def) {
+ if (status != null) {
+ switch (status) {
+ case READY:
+ return def.readyHint;
+ case FAIL:
+ return def.failHint;
+ case DUPLICATE:
+ return "Duplicate task is non blocking and empty to break the loop";
+ default:
+ }
}
return null;
}
@@ -308,9 +450,9 @@
return true;
}
- protected static boolean isAll(Iterable<TaskAttribute> atts, Status state) {
+ protected static boolean isAll(Iterable<TaskAttribute> atts, Set<Status> states) {
for (TaskAttribute att : atts) {
- if (att.status != state) {
+ if (!states.contains(att.status)) {
return false;
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
index 5e987ca..53897b5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -14,21 +14,20 @@
package com.googlesource.gerrit.plugins.task;
-import com.google.common.primitives.Primitives;
import com.google.gerrit.common.Container;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.server.git.meta.AbstractVersionedMetaData;
-import java.lang.reflect.Field;
+import com.googlesource.gerrit.plugins.task.util.Copier;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.eclipse.jgit.errors.ConfigInvalidException;
+import java.util.stream.Collectors;
/** Task Configuration file living in git */
public class TaskConfig extends AbstractVersionedMetaData {
@@ -44,16 +43,19 @@
}
}
- protected class Section extends Container {
+ protected class SubSection extends Container {
public TaskConfig config;
+ public final SubSectionKey subSection;
- public Section() {
+ public SubSection(SubSectionKey s) {
this.config = TaskConfig.this;
+ this.subSection = s;
}
}
- public class TaskBase extends Section {
+ public class TaskBase extends SubSection {
public String applicable;
+ public String duplicateKey;
public Map<String, String> exported;
public String fail;
public String failHint;
@@ -62,18 +64,20 @@
public String preloadTask;
public Map<String, String> properties;
public String readyHint;
- public List<String> subTasks;
- public List<String> subTasksExternals;
- public List<String> subTasksFactories;
- public List<String> subTasksFiles;
+ public List<ConfigSourcedValue> subTasks;
+ public List<ConfigSourcedValue> subTasksExternals;
+ public List<ConfigSourcedValue> subTasksFactories;
+ public List<ConfigSourcedValue> subTasksFiles;
public boolean isVisible;
- public boolean isTrusted;
+ public boolean isMasqueraded;
- public TaskBase(SubSection s, boolean isVisible, boolean isTrusted) {
+ public TaskBase(SubSectionKey s, boolean isVisible, boolean isMasqueraded) {
+ super(s);
this.isVisible = isVisible;
- this.isTrusted = isTrusted;
+ this.isMasqueraded = isMasqueraded;
applicable = getString(s, KEY_APPLICABLE, null);
+ duplicateKey = getString(s, KEY_DUPLICATE_KEY, null);
exported = getProperties(s, KEY_EXPORT_PREFIX);
fail = getString(s, KEY_FAIL, null);
failHint = getString(s, KEY_FAIL_HINT, null);
@@ -82,60 +86,50 @@
preloadTask = getString(s, KEY_PRELOAD_TASK, null);
properties = getProperties(s, KEY_PROPERTIES_PREFIX);
readyHint = getString(s, KEY_READY_HINT, null);
- subTasks = getStringList(s, KEY_SUBTASK);
- subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL);
- subTasksFactories = getStringList(s, KEY_SUBTASKS_FACTORY);
- subTasksFiles = getStringList(s, KEY_SUBTASKS_FILE);
+ subTasks =
+ getStringList(s, KEY_SUBTASK).stream()
+ .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+ .collect(Collectors.toList());
+ subTasksExternals =
+ getStringList(s, KEY_SUBTASKS_EXTERNAL).stream()
+ .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+ .collect(Collectors.toList());
+ subTasksFactories =
+ getStringList(s, KEY_SUBTASKS_FACTORY).stream()
+ .map(subTask -> ConfigSourcedValue.create(s.file(), subTask))
+ .collect(Collectors.toList());
+ subTasksFiles =
+ getStringList(s, KEY_SUBTASKS_FILE).stream()
+ .map(fileName -> ConfigSourcedValue.create(s.file(), fileName))
+ .collect(Collectors.toList());
}
protected TaskBase(TaskBase base) {
- copyDeclaredFields(TaskBase.class, base);
+ this(base.subSection);
+ Copier.shallowCopyDeclaredFields(TaskBase.class, base, this, false);
}
- protected <T> void copyDeclaredFields(Class<T> cls, T from) {
- for (Field field : cls.getDeclaredFields()) {
- try {
- field.setAccessible(true);
- Class<?> fieldCls = field.getType();
- Object val = field.get(from);
- if (field.getType().isPrimitive()
- || Primitives.isWrapperType(fieldCls)
- || (val instanceof String)
- || val == null) {
- field.set(this, val);
- } else if (val instanceof List) {
- List<?> list = List.class.cast(val);
- field.set(this, new ArrayList<>(list));
- } else if (val instanceof Map) {
- Map<?, ?> map = Map.class.cast(val);
- field.set(this, new HashMap<>(map));
- } else if (field.getName().equals("this$0")) { // Don't copy internal final field
- } else {
- throw new RuntimeException(
- "Don't know how to deep copy " + fieldValueToString(field, val));
- }
- } catch (IllegalAccessException e) {
- throw new RuntimeException(
- "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
- }
- }
- }
-
- protected String fieldValueToString(Field field, Object val) {
- return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+ protected TaskBase(SubSectionKey s) {
+ super(s);
}
}
- public class Task extends TaskBase {
- public String name;
+ public class Task extends TaskBase implements Cloneable {
+ public final TaskKey key;
- public Task(SubSection s, boolean isVisible, boolean isTrusted) {
- super(s, isVisible, isTrusted);
- name = s.subSection;
+ public Task(SubSectionKey s, boolean isVisible, boolean isMasqueraded) {
+ super(s, isVisible, isMasqueraded);
+ key = TaskKey.create(s);
}
- protected Task(TaskBase base) {
- super(base);
+ public Task(TasksFactory tasks, String name) {
+ super(tasks);
+ key = TaskKey.create(tasks.subSection, name);
+ }
+
+ public Task(SubSectionKey s) {
+ super(s);
+ key = TaskKey.create(s);
}
protected Map<String, String> getAllProperties() {
@@ -144,104 +138,103 @@
return all;
}
- protected void setExpandedProperties(Map<String, String> expanded) {
- properties.clear();
- properties.putAll(expanded);
- for (String property : exported.keySet()) {
- exported.put(property, properties.get(property));
- }
+ public String name() {
+ return key.task();
+ }
+
+ public FileKey file() {
+ return key.subSection().file();
+ }
+
+ public TaskKey key() {
+ return key;
}
}
public class TasksFactory extends TaskBase {
public String namesFactory;
- public TasksFactory(SubSection s, boolean isVisible, boolean isTrusted) {
- super(s, isVisible, isTrusted);
+ public TasksFactory(SubSectionKey s, boolean isVisible, boolean isMasqueraded) {
+ super(s, isVisible, isMasqueraded);
namesFactory = getString(s, KEY_NAMES_FACTORY, null);
}
}
- public class NamesFactory extends Section {
+ public class NamesFactory extends SubSection implements Cloneable {
public String changes;
public List<String> names;
public String type;
- public NamesFactory(SubSection s) {
+ public NamesFactory(SubSectionKey s) {
+ super(s);
changes = getString(s, KEY_CHANGES, null);
names = getStringList(s, KEY_NAME);
type = getString(s, KEY_TYPE, null);
}
}
- public class External extends Section {
+ public class External extends SubSection {
public String name;
public String file;
public String user;
- public External(SubSection s) {
- name = s.subSection;
+ public External(SubSectionKey s) {
+ super(s);
+ name = s.subSection();
file = getString(s, KEY_FILE, null);
user = getString(s, KEY_USER, null);
}
}
- protected static final Pattern OPTIONAL_TASK_PATTERN =
- Pattern.compile("([^ |]*( *[^ |])*) *\\| *");
+ public static final String SEP = "\0";
- protected static final String SECTION_EXTERNAL = "external";
- protected static final String SECTION_NAMES_FACTORY = "names-factory";
- protected static final String SECTION_ROOT = "root";
- protected static final String SECTION_TASK = "task";
- protected static final String SECTION_TASKS_FACTORY = "tasks-factory";
- protected static final String KEY_APPLICABLE = "applicable";
- protected static final String KEY_CHANGES = "changes";
- protected static final String KEY_EXPORT_PREFIX = "export-";
- protected static final String KEY_FAIL = "fail";
- protected static final String KEY_FAIL_HINT = "fail-hint";
- protected static final String KEY_FILE = "file";
- protected static final String KEY_IN_PROGRESS = "in-progress";
- protected static final String KEY_NAME = "name";
- protected static final String KEY_NAMES_FACTORY = "names-factory";
- protected static final String KEY_PASS = "pass";
- protected static final String KEY_PRELOAD_TASK = "preload-task";
- protected static final String KEY_PROPERTIES_PREFIX = "set-";
- protected static final String KEY_READY_HINT = "ready-hint";
- protected static final String KEY_SUBTASK = "subtask";
- protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
- protected static final String KEY_SUBTASKS_FACTORY = "subtasks-factory";
- protected static final String KEY_SUBTASKS_FILE = "subtasks-file";
- protected static final String KEY_TYPE = "type";
- protected static final String KEY_USER = "user";
+ public static final String SECTION_EXTERNAL = "external";
+ public static final String SECTION_NAMES_FACTORY = "names-factory";
+ public static final String SECTION_ROOT = "root";
+ public static final String SECTION_TASK = TaskKey.CONFIG_SECTION;
+ public static final String SECTION_TASKS_FACTORY = TaskKey.CONFIG_TASKS_FACTORY;
+ public static final String KEY_APPLICABLE = "applicable";
+ public static final String KEY_CHANGES = "changes";
+ public static final String KEY_DUPLICATE_KEY = "duplicate-key";
+ public static final String KEY_EXPORT_PREFIX = "export-";
+ public static final String KEY_FAIL = "fail";
+ public static final String KEY_FAIL_HINT = "fail-hint";
+ public static final String KEY_FILE = "file";
+ public static final String KEY_IN_PROGRESS = "in-progress";
+ public static final String KEY_NAME = "name";
+ public static final String KEY_NAMES_FACTORY = "names-factory";
+ public static final String KEY_PASS = "pass";
+ public static final String KEY_PRELOAD_TASK = "preload-task";
+ public static final String KEY_PROPERTIES_PREFIX = "set-";
+ public static final String KEY_READY_HINT = "ready-hint";
+ public static final String KEY_SUBTASK = "subtask";
+ public static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external";
+ public static final String KEY_SUBTASKS_FACTORY = "subtasks-factory";
+ public static final String KEY_SUBTASKS_FILE = "subtasks-file";
+ public static final String KEY_TYPE = "type";
+ public static final String KEY_USER = "user";
+ protected final FileKey file;
public boolean isVisible;
- public boolean isTrusted;
+ public boolean isMasqueraded;
- public Task createTask(TasksFactory tasks, String name) {
- Task task = new Task(tasks);
- task.name = name;
- return task;
+ public TaskConfig(FileKey file, boolean isVisible, boolean isMasqueraded) {
+ this(file.branch(), file, isVisible, isMasqueraded);
}
- public TaskConfig(BranchNameKey branch, String fileName, boolean isVisible, boolean isTrusted) {
- super(branch, fileName);
+ public TaskConfig(
+ BranchNameKey masqueraded, FileKey file, boolean isVisible, boolean isMasqueraded) {
+ super(masqueraded, file.file());
+ this.file = file;
this.isVisible = isVisible;
- this.isTrusted = isTrusted;
- }
-
- public List<Task> getRootTasks() {
- return getTasks(SECTION_ROOT);
- }
-
- public List<Task> getTasks() {
- return getTasks(SECTION_TASK);
+ this.isMasqueraded = isMasqueraded;
}
protected List<Task> getTasks(String type) {
List<Task> tasks = new ArrayList<>();
// No need to get a task with no name (what would we call it?)
for (String task : cfg.getSubsections(type)) {
- tasks.add(new Task(new SubSection(type, task), isVisible, isTrusted));
+ tasks.add(new Task(subSectionKey(type, task), isVisible, isMasqueraded));
}
return tasks;
}
@@ -255,103 +248,77 @@
return externals;
}
- /* returs null only if optional and not found */
- public Task getTaskOptional(String name) throws ConfigInvalidException {
- int end = 0;
- Matcher m = OPTIONAL_TASK_PATTERN.matcher(name);
- while (m.find()) {
- end = m.end();
- Task task = getTaskOrNull(m.group(1));
- if (task != null) {
- return task;
- }
- }
-
- String last = name.substring(end);
- if (!"".equals(last)) { // Last entry was not optional
- Task task = getTaskOrNull(last);
- if (task != null) {
- return task;
- }
- throw new ConfigInvalidException("task not defined");
- }
- return null;
- }
-
- /* returns null if not found */
- protected Task getTaskOrNull(String name) {
- SubSection subSection = new SubSection(SECTION_TASK, name);
- return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted);
+ protected Optional<Task> getOptionalTask(String name) {
+ SubSectionKey subSection = subSectionKey(SECTION_TASK, name);
+ return getNames(subSection).isEmpty()
+ ? Optional.empty()
+ : Optional.of(new Task(subSection, isVisible, isMasqueraded));
}
public TasksFactory getTasksFactory(String name) {
- return new TasksFactory(new SubSection(SECTION_TASKS_FACTORY, name), isVisible, isTrusted);
+ return new TasksFactory(subSectionKey(SECTION_TASKS_FACTORY, name), isVisible, isMasqueraded);
}
public NamesFactory getNamesFactory(String name) {
- return new NamesFactory(new SubSection(SECTION_NAMES_FACTORY, name));
+ return new NamesFactory(subSectionKey(SECTION_NAMES_FACTORY, name));
}
public External getExternal(String name) {
- return getExternal(new SubSection(SECTION_EXTERNAL, name));
+ return getExternal(subSectionKey(SECTION_EXTERNAL, name));
}
- protected External getExternal(SubSection s) {
+ protected External getExternal(SubSectionKey s) {
return new External(s);
}
- protected Map<String, String> getProperties(SubSection s, String prefix) {
+ protected Map<String, String> getProperties(SubSectionKey s, String prefix) {
Map<String, String> valueByName = new HashMap<>();
for (Map.Entry<String, String> e :
getStringByName(s, getMatchingNames(s, prefix + ".+")).entrySet()) {
String name = e.getKey();
valueByName.put(name.substring(prefix.length()), e.getValue());
}
- return valueByName;
+ return Collections.unmodifiableMap(valueByName);
}
- protected Map<String, String> getStringByName(SubSection s, Iterable<String> names) {
+ protected Map<String, String> getStringByName(SubSectionKey s, Iterable<String> names) {
Map<String, String> valueByName = new HashMap<>();
for (String name : names) {
valueByName.put(name, getString(s, name));
}
- return valueByName;
+ return Collections.unmodifiableMap(valueByName);
}
- protected Set<String> getMatchingNames(SubSection s, String match) {
+ protected Set<String> getMatchingNames(SubSectionKey s, String match) {
Set<String> matched = new HashSet<>();
for (String name : getNames(s)) {
if (name.matches(match)) {
matched.add(name);
}
}
- return matched;
+ return Collections.unmodifiableSet(matched);
}
- protected Set<String> getNames(SubSection s) {
- return cfg.getNames(s.section, s.subSection);
+ protected Set<String> getNames(SubSectionKey s) {
+ return cfg.getNames(s.section(), s.subSection());
}
- protected String getString(SubSection s, String key, String def) {
+ protected String getString(SubSectionKey s, String key, String def) {
String v = getString(s, key);
return v != null ? v : def;
}
- protected String getString(SubSection s, String key) {
- return cfg.getString(s.section, s.subSection, key);
+ protected String getString(SubSectionKey s, String key) {
+ return cfg.getString(s.section(), s.subSection(), key);
}
- protected List<String> getStringList(SubSection s, String key) {
- return Arrays.asList(cfg.getStringList(s.section, s.subSection, key));
+ protected List<String> getStringList(SubSectionKey s, String key) {
+ List<String> stringList = Arrays.asList(cfg.getStringList(s.section(), s.subSection(), key));
+ stringList.replaceAll(str -> str == null ? "" : str);
+ return Collections.unmodifiableList(stringList);
}
- protected static class SubSection {
- public final String section;
- public final String subSection;
-
- protected SubSection(String section, String subSection) {
- this.section = section;
- this.subSection = subSection;
- }
+ protected SubSectionKey subSectionKey(String section, String subSection) {
+ return SubSectionKey.create(file, section, subSection);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
similarity index 70%
rename from src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
rename to src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
index f790ccc..648c729 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
@@ -17,9 +17,11 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -32,12 +34,9 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
-public class TaskConfigFactory {
+public class TaskConfigCache {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
- protected static final String EXTENSION = ".config";
- protected static final String DEFAULT = "task" + EXTENSION;
-
protected final GitRepositoryManager gitMgr;
protected final PermissionBackend permissionBackend;
@@ -45,21 +44,22 @@
protected final AllProjectsName allProjects;
protected final Map<BranchNameKey, PatchSetArgument> psaMasquerades = new HashMap<>();
+ protected final Map<FileKey, TaskConfig> taskCfgByFile = new HashMap<>();
@Inject
- protected TaskConfigFactory(
- AllProjectsName allProjects,
+ protected TaskConfigCache(
+ AllProjectsNameProvider allProjectsNameProvider,
GitRepositoryManager gitMgr,
PermissionBackend permissionBackend,
CurrentUser user) {
- this.allProjects = allProjects;
+ this.allProjects = allProjectsNameProvider.get();
this.gitMgr = gitMgr;
this.permissionBackend = permissionBackend;
this.user = user;
}
public TaskConfig getRootConfig() throws ConfigInvalidException, IOException {
- return getTaskConfig(getRootBranch(), DEFAULT, true);
+ return getTaskConfig(FileKey.create(getRootBranch(), TaskFileConstants.TASK_CFG));
}
public void masquerade(PatchSetArgument psa) {
@@ -67,26 +67,39 @@
}
protected BranchNameKey getRootBranch() {
- return BranchNameKey.create(allProjects, "refs/meta/config");
+ return BranchNameKey.create(allProjects, RefNames.REFS_CONFIG);
}
- public TaskConfig getTaskConfig(BranchNameKey branch, String fileName, boolean isTrusted)
- throws ConfigInvalidException, IOException {
+ public TaskConfig getTaskConfig(FileKey key) throws ConfigInvalidException, IOException {
+ TaskConfig cfg = taskCfgByFile.get(key);
+ if (cfg == null) {
+ cfg = loadTaskConfig(key);
+ taskCfgByFile.put(key, cfg);
+ }
+ return cfg;
+ }
+
+ private TaskConfig loadTaskConfig(FileKey file) throws ConfigInvalidException, IOException {
+ BranchNameKey branch = file.branch();
PatchSetArgument psa = psaMasquerades.get(branch);
boolean visible = true; // invisible psas are filtered out by commandline
+ boolean isMasqueraded = false;
if (psa == null) {
- visible = canRead(branch);
+ visible = isVisible(branch);
} else {
- isTrusted = false;
+ isMasqueraded = true;
branch = BranchNameKey.create(psa.change.getProject(), psa.patchSet.refName());
}
- Project.NameKey project = branch.project();
- TaskConfig cfg = new TaskConfig(branch, fileName, visible, isTrusted);
+ Project.NameKey project = file.branch().project();
+ TaskConfig cfg =
+ isMasqueraded
+ ? new TaskConfig(branch, file, visible, isMasqueraded)
+ : new TaskConfig(file, visible, isMasqueraded);
try (Repository git = gitMgr.openRepository(project)) {
cfg.load(project, git);
} catch (IOException e) {
- log.atWarning().withCause(e).log("Failed to load %s for %s", fileName, project);
+ log.atWarning().withCause(e).log("Failed to load %s for %s", file.file(), project);
throw e;
} catch (ConfigInvalidException e) {
throw e;
@@ -94,7 +107,7 @@
return cfg;
}
- public boolean canRead(BranchNameKey branch) {
+ public boolean isVisible(BranchNameKey branch) {
try {
PermissionBackend.ForProject permissions =
permissionBackend.user(user).project(branch.project());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
new file mode 100644
index 0000000..5b9bef0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
@@ -0,0 +1,106 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * A TaskExpression represents a config string pointing to an expression which includes zero or more
+ * task references separated by a '|', and potentially termintated by a '|'. If the expression is
+ * not terminated by a '|' it indicates that task resolution of at least one task is required. Task
+ * selection priority is from left to right. This can be expressed as:
+ *
+ * <pre>
+ * TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
+ * </pre>
+ *
+ * <a href="file:../../../../../../antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4">See
+ * this for Task Reference</a>
+ *
+ * <p>Example expressions to prioritized names and requirements:
+ *
+ * <ul>
+ * <li>
+ * <pre> "simple" -> ("simple") required</pre>
+ * <li>
+ * <pre> "world | peace" -> ("world", "peace") required</pre>
+ * <li>
+ * <pre> "shadenfreud |" -> ("shadenfreud") optional</pre>
+ * <li>
+ * <pre> "foo | bar |" -> ("foo", "bar") optional</pre>
+ * </ul>
+ */
+public class TaskExpression implements Iterable<TaskKey> {
+ public interface Factory {
+ TaskExpression create(FileKey key, String expression);
+ }
+
+ protected static final Pattern EXPRESSION_PATTERN = Pattern.compile("([^ |]+[^|]*)(\\|)?");
+ protected final TaskExpressionKey key;
+ protected final TaskReference.Factory taskReferenceFactory;
+
+ @Inject
+ public TaskExpression(
+ TaskReference.Factory taskReferenceFactory,
+ @Assisted FileKey key,
+ @Assisted String expression) {
+ this.key = TaskExpressionKey.create(key, expression);
+ this.taskReferenceFactory = taskReferenceFactory;
+ }
+
+ @Override
+ public Iterator<TaskKey> iterator() {
+ return new Iterator<TaskKey>() {
+ Matcher m = EXPRESSION_PATTERN.matcher(key.expression());
+ Boolean hasNext;
+ boolean optional;
+
+ @Override
+ public boolean hasNext() {
+ if (hasNext == null) {
+ hasNext = m.find();
+ if (hasNext) {
+ optional = m.group(2) != null;
+ }
+ }
+ if (!hasNext && !optional) {
+ return true; // fake it so next() throws an Exception
+ }
+ return hasNext;
+ }
+
+ @Override
+ public TaskKey next() {
+ // Can't use @SuppressWarnings("ReturnValueIgnored") on method call
+ boolean ignored = hasNext(); // in case next() was (re)called w/o calling hasNext()
+ if (!hasNext) {
+ throw new NoSuchElementException("No more names, yet expression was not optional");
+ }
+ hasNext = null;
+ try {
+ return taskReferenceFactory.create(key.file(), m.group(1)).getTaskKey();
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
new file mode 100644
index 0000000..6fcd30d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
@@ -0,0 +1,34 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.BranchNameKey;
+
+/** A key for TaskExpression. */
+@AutoValue
+public abstract class TaskExpressionKey {
+ public static TaskExpressionKey create(FileKey file, String expression) {
+ return new AutoValue_TaskExpressionKey(file, expression);
+ }
+
+ public BranchNameKey branch() {
+ return file().branch();
+ }
+
+ public abstract FileKey file();
+
+ public abstract String expression();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java
new file mode 100644
index 0000000..904c933
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java
@@ -0,0 +1,20 @@
+// 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.googlesource.gerrit.plugins.task;
+
+public final class TaskFileConstants {
+ public static final String TASK_DIR = "task";
+ public static final String TASK_CFG = "task.config";
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
new file mode 100644
index 0000000..bd0b683
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -0,0 +1,204 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** An immutable reference to a task in task config file. */
+@AutoValue
+public abstract class TaskKey {
+ protected static final String CONFIG_SECTION = "task";
+ protected static final String CONFIG_TASKS_FACTORY = "tasks-factory";
+
+ /** Creates a TaskKey with task name as the name of sub section. */
+ public static TaskKey create(SubSectionKey section) {
+ return create(section, section.subSection());
+ }
+
+ /** Creates a TaskKey with given FileKey and task name and sub section's name as 'task'. */
+ public static TaskKey create(FileKey file, String task) {
+ return create(SubSectionKey.create(file, CONFIG_SECTION, task));
+ }
+
+ /** Creates a TaskKey from a sub section and task name, generally used by TasksFactory. */
+ public static TaskKey create(SubSectionKey section, String task) {
+ return new AutoValue_TaskKey(section, task);
+ }
+
+ public BranchNameKey branch() {
+ return subSection().file().branch();
+ }
+
+ public abstract SubSectionKey subSection();
+
+ public abstract String task();
+
+ public boolean isTasksFactoryGenerated() {
+ return subSection().section().equals(CONFIG_TASKS_FACTORY);
+ }
+
+ public static class Builder {
+ protected final AccountCache accountCache;
+ protected final AllProjectsName allProjectsName;
+ protected final AllUsersName allUsersName;
+ protected final FileKey relativeTo;
+ protected BranchNameKey branch;
+ protected String file;
+ protected String task;
+ protected GroupCache groupCache;
+
+ Builder(
+ FileKey relativeTo,
+ AllProjectsName allProjectsName,
+ AllUsersName allUsersName,
+ AccountCache accountCache,
+ GroupCache groupCache) {
+ this.relativeTo = relativeTo;
+ this.allProjectsName = allProjectsName;
+ this.allUsersName = allUsersName;
+ this.accountCache = accountCache;
+ this.groupCache = groupCache;
+ }
+
+ public TaskKey buildTaskKey() {
+ return isReferencingAnotherRef() ? getAnotherRefTask() : getSameRefTask();
+ }
+
+ protected TaskKey getAnotherRefTask() {
+ return TaskKey.create(
+ isReferencingRootFile()
+ ? FileKey.create(branch, TaskFileConstants.TASK_CFG)
+ : FileKey.create(branch, file),
+ task);
+ }
+
+ protected TaskKey getSameRefTask() {
+ return TaskKey.create(
+ isRelativePath() ? relativeTo : FileKey.create(relativeTo.branch(), file), task);
+ }
+
+ public void setAbsolute() {
+ file = TaskFileConstants.TASK_DIR;
+ }
+
+ public void setPath(Path path) throws ConfigInvalidException {
+ Path parentDir = Paths.get(relativeTo.file()).getParent();
+ if (parentDir == null) {
+ parentDir = Paths.get(TaskFileConstants.TASK_DIR);
+ }
+
+ file =
+ isRelativePath()
+ ? parentDir.resolve(path).toString()
+ : Paths.get(file).resolve(path).toString();
+ throwIfInvalidPath();
+ }
+
+ public void setRefRootFile() throws ConfigInvalidException {
+ Preconditions.checkState(!isFileAlreadySet());
+ file = TaskFileConstants.TASK_CFG;
+ }
+
+ public void setTaskName(String task) {
+ this.task = task;
+ }
+
+ public void setUsername(String username) throws ConfigInvalidException {
+ branch =
+ BranchNameKey.create(
+ allUsersName,
+ RefNames.refsUsers(
+ accountCache
+ .getByUsername(username)
+ .orElseThrow(
+ () -> new ConfigInvalidException("Cannot resolve username: " + username))
+ .account()
+ .id()));
+ }
+
+ public void setGroupName(String groupName) throws ConfigInvalidException {
+ branch =
+ BranchNameKey.create(
+ allUsersName,
+ RefNames.refsGroups(
+ groupCache
+ .get(AccountGroup.nameKey(groupName))
+ .orElseThrow(
+ () ->
+ new ConfigInvalidException(
+ String.format("Cannot resolve group name: %s", groupName)))
+ .getGroupUUID()));
+ }
+
+ public void setGroupUUID(String uuid) throws ConfigInvalidException {
+ branch =
+ BranchNameKey.create(
+ allUsersName,
+ RefNames.refsGroups(
+ groupCache
+ .get(AccountGroup.uuid(uuid))
+ .orElseThrow(
+ () ->
+ new ConfigInvalidException(
+ String.format("Cannot resolve group uuid: %s", uuid)))
+ .getGroupUUID()));
+ }
+
+ public void setReferringAllProjectsTask() {
+ branch = BranchNameKey.create(allProjectsName, RefNames.REFS_CONFIG);
+ }
+
+ protected void throwIfInvalidPath() throws ConfigInvalidException {
+ Path path = Paths.get(file);
+ if (!path.startsWith(TaskFileConstants.TASK_DIR)
+ && !path.equals(Paths.get(TaskFileConstants.TASK_CFG))) {
+ throw new ConfigInvalidException(
+ "Invalid config location, path should be "
+ + TaskFileConstants.TASK_CFG
+ + " or under "
+ + TaskFileConstants.TASK_DIR
+ + " directory");
+ }
+ }
+
+ /** Returns true when the path implies relative or same file. */
+ protected boolean isRelativePath() {
+ return file == null;
+ }
+
+ protected boolean isFileAlreadySet() {
+ return file != null;
+ }
+
+ protected boolean isReferencingRootFile() {
+ return file == null;
+ }
+
+ protected boolean isReferencingAnotherRef() {
+ return branch != null;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
new file mode 100644
index 0000000..c0c5d8c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
@@ -0,0 +1,76 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+public class TaskPath {
+ public interface Factory {
+ TaskPath create(TaskKey key);
+ }
+
+ protected String name;
+ protected String type;
+ protected String tasksFactory;
+ protected String user;
+ protected String project;
+ protected String ref;
+ protected String file;
+ protected String error;
+
+ @Inject
+ public TaskPath(AllUsersNameProvider allUsers, Accounts accounts, @Assisted TaskKey key) {
+ name = key.task();
+ type = key.subSection().section();
+ tasksFactory = key.isTasksFactoryGenerated() ? key.subSection().subSection() : null;
+ user = getUserOrNull(accounts, allUsers.get(), key);
+ project = key.branch().project().get();
+ ref = key.branch().branch();
+ file = key.subSection().file().file();
+ }
+
+ public TaskPath(String error) {
+ this.error = error;
+ }
+
+ private String getUserOrNull(Accounts accounts, Project.NameKey allUsers, TaskKey key) {
+ try {
+ if (allUsers.get().equals(key.branch().project().get())) {
+ String ref = key.branch().branch();
+ Account.Id id = Account.Id.fromRef(ref);
+ if (id != null) {
+ Optional<AccountState> state = accounts.get(id);
+ if (state.isPresent()) {
+ Optional<String> userName = state.get().userName();
+ if (userName.isPresent()) {
+ return userName.get();
+ }
+ }
+ }
+ }
+ } catch (ConfigInvalidException | IOException e) {
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
new file mode 100644
index 0000000..a7824ac
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -0,0 +1,180 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.nio.file.Paths;
+import java.util.NoSuchElementException;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.Lexer;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** This class is used by TaskExpression to decode the task from task reference. */
+public class TaskReference {
+ protected final String reference;
+ protected final TaskKey.Builder taskKeyBuilder;
+
+ interface Factory {
+ TaskReference create(FileKey relativeTo, String reference);
+ }
+
+ @Inject
+ public TaskReference(
+ AllProjectsNameProvider allProjectsNameProvider,
+ AllUsersNameProvider allUsersNameProvider,
+ AccountCache accountCache,
+ GroupCache groupCache,
+ @Assisted FileKey relativeTo,
+ @Assisted String reference) {
+ this(
+ new TaskKey.Builder(
+ relativeTo,
+ allProjectsNameProvider.get(),
+ allUsersNameProvider.get(),
+ accountCache,
+ groupCache),
+ reference);
+ }
+
+ @VisibleForTesting
+ public TaskReference(TaskKey.Builder taskKeyBuilder, String reference) {
+ this.taskKeyBuilder = taskKeyBuilder;
+ this.reference = reference.trim();
+ if (reference.isEmpty()) {
+ throw new NoSuchElementException();
+ }
+ }
+
+ public TaskKey getTaskKey() throws ConfigInvalidException {
+ ParseTreeWalker walker = new ParseTreeWalker();
+ try {
+ walker.walk(new TaskReferenceListener(taskKeyBuilder), parse());
+ } catch (RuntimeConfigInvalidException e) {
+ throw e.checkedException;
+ }
+ return taskKeyBuilder.buildTaskKey();
+ }
+
+ protected ParseTree parse() {
+ Lexer lexer = new TaskReferenceLexer(CharStreams.fromString(reference));
+ lexer.removeErrorListeners();
+ lexer.addErrorListener(TaskReferenceErrorListener.INSTANCE);
+ return new TaskReferenceParser(new CommonTokenStream(lexer)).reference();
+ }
+
+ protected static class TaskReferenceErrorListener extends BaseErrorListener {
+ protected static final TaskReferenceErrorListener INSTANCE = new TaskReferenceErrorListener();
+
+ @Override
+ public void syntaxError(
+ Recognizer<?, ?> recognizer,
+ Object offendingSymbol,
+ int line,
+ int charPositionInLine,
+ String msg,
+ RecognitionException e) {
+ throw new NoSuchElementException();
+ }
+ }
+
+ protected class TaskReferenceListener extends TaskReferenceBaseListener {
+ TaskKey.Builder builder;
+
+ TaskReferenceListener(TaskKey.Builder builder) {
+ this.builder = builder;
+ }
+
+ @Override
+ public void enterAbsolute(TaskReferenceParser.AbsoluteContext ctx) {
+ builder.setAbsolute();
+ }
+
+ @Override
+ public void enterRelative(TaskReferenceParser.RelativeContext ctx) {
+ try {
+ builder.setPath(
+ ctx.dir().stream()
+ .map(dir -> Paths.get(dir.NAME().getText()))
+ .reduce(Paths.get(""), (a, b) -> a.resolve(b))
+ .resolve(ctx.NAME().getText()));
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+
+ @Override
+ public void enterReference(TaskReferenceParser.ReferenceContext ctx) {
+ builder.setTaskName(ctx.TASK().getText());
+ }
+
+ @Override
+ public void enterFile_path(TaskReferenceParser.File_pathContext ctx) {
+ if (ctx.ALL_PROJECTS_ROOT() != null || (ctx.FWD_SLASH() != null && ctx.absolute() != null)) {
+ builder.setReferringAllProjectsTask();
+ }
+
+ if (ctx.absolute() == null && ctx.relative() == null) {
+ try {
+ builder.setRefRootFile();
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+ }
+
+ @Override
+ public void enterUser(TaskReferenceParser.UserContext ctx) {
+ try {
+ builder.setUsername(ctx.NAME().getText());
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+
+ @Override
+ public void enterGroup_name(TaskReferenceParser.Group_nameContext ctx) {
+ try {
+ String groupName =
+ ctx.NAME() == null
+ ? (ctx.NAME_WITH_SPACES() == null ? "" : ctx.NAME_WITH_SPACES().getText())
+ : ctx.NAME().getText();
+ builder.setGroupName(groupName);
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+
+ @Override
+ public void enterGroup_uuid(TaskReferenceParser.Group_uuidContext ctx) {
+ try {
+ builder.setGroupUUID(ctx.INTERNAL_GROUP_UUID().getText());
+ } catch (ConfigInvalidException e) {
+ throw new RuntimeConfigInvalidException(e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
index 7079918..5586774 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -14,6 +14,8 @@
package com.googlesource.gerrit.plugins.task;
+import static java.util.stream.Collectors.toList;
+
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
@@ -30,22 +32,33 @@
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
import com.googlesource.gerrit.plugins.task.TaskConfig.External;
import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactoryType;
import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory;
-import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument;
+import com.googlesource.gerrit.plugins.task.properties.Properties;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMap;
+import com.googlesource.gerrit.plugins.task.statistics.HitHashMapOfCollection;
+import com.googlesource.gerrit.plugins.task.statistics.StatisticsMap;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import com.googlesource.gerrit.plugins.task.statistics.TracksStatistics;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.Set;
-import java.util.function.BiFunction;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.util.StringUtils;
/**
* Add structure to access the task definitions from the config as a tree.
@@ -55,17 +68,46 @@
*/
public class TaskTree {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ TaskTree create(@Assisted TaskConfigCache taskConfigCache);
+ }
+
+ @FunctionalInterface
+ public interface NodeFactory {
+ Node create(NodeList parent, Task definition) throws Exception;
+ }
+
+ public static class Statistics {
+ public Object definitionsPerSubSectionCache;
+ public Object definitionsByBranchBySubSectionCache;
+ public Object changesByNamesFactoryQueryCache;
+ public Properties.Statistics properties;
+ public transient int summaryCount;
+ }
+
protected static final String TASK_DIR = "task";
protected final AccountResolver accountResolver;
protected final AllUsersNameProvider allUsers;
protected final CurrentUser user;
- protected final TaskConfigFactory taskFactory;
- protected final Root root = new Root();
+ protected final PredicateCache predicateCache;
+ protected final MatchCache matchCache;
+ protected final Preloader preloader;
+ protected final TaskConfigCache taskConfigCache;
+ protected final TaskExpression.Factory taskExpressionFactory;
+ protected final NodeList root = new NodeList();
protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
+ protected final StatisticsMap<String, List<ChangeData>> changesByNamesFactoryQuery =
+ new HitHashMap<>();
+ protected final StatisticsMap<SubSectionKey, List<Task>> definitionsBySubSection =
+ new HitHashMapOfCollection<>();
+ protected final StatisticsMap<SubSectionKey, Map<BranchNameKey, List<Task>>>
+ definitionsByBranchBySubSection = new HitHashMap<>();
protected ChangeData changeData;
+ protected Statistics statistics;
@Inject
public TaskTree(
@@ -73,266 +115,562 @@
AllUsersNameProvider allUsers,
AnonymousUser anonymousUser,
CurrentUser user,
- TaskConfigFactory taskFactory,
Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
- Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
+ Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
+ PredicateCache predicateCache,
+ TaskExpression.Factory taskExpressionFactory,
+ Preloader.Factory preloaderFactory,
+ @Assisted TaskConfigCache taskConfigCache) {
this.accountResolver = accountResolver;
this.allUsers = allUsers;
this.user = user != null ? user : anonymousUser;
- this.taskFactory = taskFactory;
this.changeQueryProcessorProvider = changeQueryProcessorProvider;
this.changeQueryBuilderProvider = changeQueryBuilderProvider;
+ this.predicateCache = predicateCache;
+ this.matchCache = new MatchCache(predicateCache);
+ this.taskConfigCache = taskConfigCache;
+ this.taskExpressionFactory = taskExpressionFactory;
+ this.preloader = preloaderFactory.create(taskConfigCache);
}
- public void masquerade(PatchSetArgument psa) {
- taskFactory.masquerade(psa);
- }
-
- public List<Node> getRootNodes(ChangeData changeData) throws ConfigInvalidException, IOException {
+ public List<Node> getRootNodes(ChangeData changeData)
+ throws ConfigInvalidException, IOException, StorageException {
this.changeData = changeData;
- return root.getRootNodes();
- }
-
- public Node createNodeOrNull(NodeList parent, Task definition) {
- try {
- return new Node(parent, definition);
- } catch (Exception e) {
- return null;
- }
+ root.path = Collections.emptyList();
+ root.duplicateKeys = Collections.emptyList();
+ return root.getSubNodes();
}
protected class NodeList {
protected NodeList parent = null;
- protected LinkedList<String> path = new LinkedList<>();
- protected List<Node> nodes;
- protected Set<String> names = new HashSet<>();
+ protected Collection<String> path;
+ protected Collection<String> duplicateKeys;
+ protected Map<TaskKey, Node> cachedNodeByTask = new HashMap<>();
+ protected List<Node> cachedNodes;
- protected void addSubDefinitions(List<Task> defs) {
- for (Task def : defs) {
- addSubDefinition(def);
+ protected List<Node> getSubNodes()
+ throws ConfigInvalidException, IOException, StorageException {
+ if (cachedNodes != null) {
+ return refresh(cachedNodes);
}
+ return cachedNodes = loadSubNodes();
}
- protected void addSubDefinition(Task def) {
- addSubDefinition(def, (d, c) -> createNodeOrNull(d, c));
- }
-
- protected void addSubDefinition(Task def, BiFunction<NodeList, Task, Node> nodeConstructor) {
- Node node = null;
- if (def != null && !path.contains(def.name) && names.add(def.name)) {
- // path check above detects looping definitions
- // names check above detects duplicate subtasks
- node = nodeConstructor.apply(this, def);
- }
- nodes.add(node);
+ protected List<Node> loadSubNodes()
+ throws ConfigInvalidException, IOException, StorageException {
+ return new SubNodeFactory().createFromPreloaded(preloader.getRootTasks());
}
public ChangeData getChangeData() {
- return parent == null ? TaskTree.this.changeData : parent.getChangeData();
+ return TaskTree.this.changeData;
}
- protected Properties.Task getProperties() {
- return Properties.Task.EMPTY_PARENT;
+ protected boolean isTrusted() {
+ return true;
}
- }
- protected class Root extends NodeList {
- public List<Node> getRootNodes() throws ConfigInvalidException, IOException {
- if (nodes == null) {
- nodes = new ArrayList<>();
- addSubDefinitions(getRootDefinitions());
+ protected class SubNodeFactory {
+ protected Set<String> names = new HashSet<>();
+
+ public List<Node> createFromPreloaded(List<Task> defs) {
+ List<Node> nodes = new ArrayList<>();
+ for (Task def : defs) {
+ nodes.add(createFromPreloaded(def));
+ }
+ return nodes;
}
- return nodes;
- }
- protected List<Task> getRootDefinitions() throws ConfigInvalidException, IOException {
- return taskFactory.getRootConfig().getRootTasks();
+ public Node createFromPreloaded(Task def) {
+ return createFromPreloaded(def, (parent, definition) -> new Node(parent, definition));
+ }
+
+ public Node createFromPreloaded(Task def, ChangeData changeData) {
+ return createFromPreloaded(
+ def,
+ (parent, definition) ->
+ new Node(parent, definition) {
+ @Override
+ public ChangeData getChangeData() {
+ return changeData;
+ }
+
+ @Override
+ public boolean isChange() {
+ return true;
+ }
+ });
+ }
+
+ protected Node createFromPreloaded(Task def, NodeFactory nodeFactory) {
+ if (def != null) {
+ try {
+ Node node = cachedNodeByTask.get(def.key());
+ boolean isRefreshNeeded = node != null;
+ if (node == null) {
+ node = nodeFactory.create(NodeList.this, def);
+ }
+
+ if (names.add(def.name())) {
+ // names check above detects duplicate subtasks
+ if (isRefreshNeeded) {
+ node.refreshTask();
+ }
+ return node;
+ }
+ } catch (Exception e) {
+ }
+ }
+ return createInvalid();
+ }
+
+ protected Node createInvalid() {
+ return new Node().new Invalid();
+ }
}
}
public class Node extends NodeList {
- public final Task task;
- protected final Properties.Task properties;
+ public class Invalid extends Node {
+ @Override
+ public void refreshTask() {}
- public Node(NodeList parent, Task definition) throws ConfigInvalidException, StorageException {
- this.parent = parent;
- this.task = definition;
- this.path.addAll(parent.path);
- this.path.add(definition.name);
- Preloader.preload(definition);
- properties = new Properties.Task(getChangeData(), definition, parent.getProperties());
+ @Override
+ public Task getDefinition() {
+ return null;
+ }
}
- public List<Node> getSubNodes() {
- if (nodes == null) {
- nodes = new ArrayList<>();
- addSubDefinitions();
+ public Task task;
+ public boolean isDuplicate;
+
+ protected Properties.Statistics propertiesStatistics;
+ protected final Properties properties;
+ protected final TaskKey taskKey;
+ protected StatisticsMap<BranchNameKey, List<Node>> nodesByBranch;
+ protected boolean hasUnfilterableSubNodes = false;
+
+ protected Node() { // Only for Invalid
+ taskKey = null;
+ properties = null;
+ }
+
+ public Node(NodeList parent, Task task) {
+ this.parent = parent;
+ taskKey = task.key();
+ properties = new Properties(this, task);
+ refreshTask();
+ }
+
+ public String key() {
+ return String.valueOf(getChangeData().getId().get()) + TaskConfig.SEP + taskKey;
+ }
+
+ public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
+ if (cachedNodes != null) {
+ return refresh(cachedNodes);
+ }
+ List<Node> nodes = loadSubNodes();
+ if (!properties.isSubNodeReloadRequired()) {
+ if (!isChange()) {
+ return cachedNodes = nodes;
+ }
+ definitionsBySubSection.computeIfAbsentTimed(
+ task.key().subSection(),
+ k -> nodes.stream().map(n -> n.getDefinition()).collect(toList()),
+ task.isVisible);
+ } else {
+ hasUnfilterableSubNodes = true;
+ cachedNodeByTask.clear();
+ nodes.stream()
+ .filter(n -> !(n instanceof Invalid) && !n.isChange())
+ .forEach(n -> cachedNodeByTask.put(n.task.key(), n));
}
return nodes;
}
- protected void addSubDefinitions() throws StorageException {
- addSubTaskDefinitions();
- addSubTasksFactoryDefinitions();
- addSubFileDefinitions();
- addExternalDefinitions();
- }
-
- protected void addSubTaskDefinitions() {
- for (String name : task.subTasks) {
- try {
- Task def = task.config.getTaskOptional(name);
- if (def != null) {
- addSubDefinition(def);
- }
- } catch (ConfigInvalidException e) {
- addSubDefinition(null);
- }
- }
- }
-
- protected void addSubFileDefinitions() {
- for (String file : task.subTasksFiles) {
- try {
- addSubDefinitions(getTaskDefinitions(task.config.getBranch(), file));
- } catch (ConfigInvalidException | IOException e) {
- addSubDefinition(null);
- }
- }
- }
-
- protected void addExternalDefinitions() throws StorageException {
- for (String external : task.subTasksExternals) {
- try {
- External ext = task.config.getExternal(external);
- if (ext == null) {
- addSubDefinition(null);
- } else {
- addSubDefinitions(getTaskDefinitions(ext));
- }
- } catch (ConfigInvalidException | IOException e) {
- addSubDefinition(null);
- }
- }
- }
-
- protected void addSubTasksFactoryDefinitions() throws StorageException {
- for (String taskFactoryName : task.subTasksFactories) {
- TasksFactory tasksFactory = task.config.getTasksFactory(taskFactoryName);
- if (tasksFactory != null) {
- NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory);
- if (namesFactory != null && namesFactory.type != null) {
- new Properties.NamesFactory(namesFactory, getProperties());
- switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
- case STATIC:
- addStaticTypeTasksDefinitions(tasksFactory, namesFactory);
- continue;
- case CHANGE:
- addChangesTypeTaskDefinitions(tasksFactory, namesFactory);
- continue;
- }
- }
- }
- addSubDefinition(null);
- }
- }
-
- protected void addStaticTypeTasksDefinitions(
- TasksFactory tasksFactory, NamesFactory namesFactory) {
- for (String name : namesFactory.names) {
- addSubDefinition(task.config.createTask(tasksFactory, name));
- }
- }
-
- protected void addChangesTypeTaskDefinitions(
- TasksFactory tasksFactory, NamesFactory namesFactory) {
- try {
- if (namesFactory.changes != null) {
- List<ChangeData> changeDataList =
- changeQueryProcessorProvider
- .get()
- .query(changeQueryBuilderProvider.get().parse(namesFactory.changes))
- .entities();
- for (ChangeData changeData : changeDataList) {
- addSubDefinition(
- task.config.createTask(tasksFactory, changeData.getId().toString()),
- new ChangeNodeFactory(changeData)::createChangeNodeOrNull);
- }
- return;
- }
- } catch (StorageException e) {
- log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes);
- } catch (QueryParseException e) {
- }
- addSubDefinition(null);
- }
-
- protected List<Task> getTaskDefinitions(External external)
- throws ConfigInvalidException, IOException, StorageException {
- return getTaskDefinitions(resolveUserBranch(external.user), external.file);
- }
-
- protected List<Task> getTaskDefinitions(BranchNameKey branch, String file)
- throws ConfigInvalidException, IOException {
- return taskFactory
- .getTaskConfig(branch, resolveTaskFileName(file), task.isTrusted)
- .getTasks();
+ public List<Node> getApplicableSubNodes()
+ throws IOException, StorageException, ConfigInvalidException {
+ return hasUnfilterableSubNodes ? getSubNodes() : new ApplicableNodeFilter().getSubNodes();
}
@Override
- protected Properties.Task getProperties() {
- return properties;
+ protected List<Node> loadSubNodes()
+ throws IOException, StorageException, ConfigInvalidException {
+ List<Task> cachedDefinitions = definitionsBySubSection.get(task.key().subSection());
+ if (cachedDefinitions != null) {
+ return new SubNodeFactory().createFromPreloaded(cachedDefinitions);
+ }
+ List<Node> nodes = new SubNodeAdder().getSubNodes();
+ properties.expansionComplete();
+ return nodes;
}
- protected String resolveTaskFileName(String file) throws ConfigInvalidException {
- if (file == null) {
- throw new ConfigInvalidException("External file not defined");
+ /* The task needs to be refreshed before a node is used, however
+ subNode refreshing can wait until they are fetched since they may
+ not be needed. */
+ public void refreshTask() {
+ this.path = new LinkedList<>(parent.path);
+ String key = key();
+ isDuplicate = path.contains(key);
+ path.add(key);
+
+ if (statistics != null) {
+ properties.setStatisticsConsumer(
+ s -> statistics.properties = (propertiesStatistics = s).sum(statistics.properties));
}
- Path p = Paths.get(TASK_DIR, file);
- if (!p.startsWith(TASK_DIR)) {
- throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
+ this.task = properties.getTask(getChangeData());
+
+ this.duplicateKeys = new LinkedList<>(parent.duplicateKeys);
+ if (task.duplicateKey != null) {
+ isDuplicate |= duplicateKeys.contains(task.duplicateKey);
+ duplicateKeys.add(task.duplicateKey);
}
- return p.toString();
}
- protected BranchNameKey resolveUserBranch(String user)
- throws ConfigInvalidException, IOException, StorageException {
- if (user == null) {
- throw new ConfigInvalidException("External user not defined");
+ public Properties getParentProperties() {
+ return (parent instanceof Node) ? ((Node) parent).properties : Properties.EMPTY;
+ }
+
+ @Override
+ protected boolean isTrusted() {
+ return parent.isTrusted() && !task.isMasqueraded;
+ }
+
+ @Override
+ public ChangeData getChangeData() {
+ return parent.getChangeData();
+ }
+
+ public Task getDefinition() {
+ return properties.isTaskRefreshRequired() ? properties.origTask : task;
+ }
+
+ public boolean isChange() {
+ return false;
+ }
+
+ public boolean match(String query) throws StorageException, QueryParseException {
+ return matchCache.match(getChangeData(), query, task.isVisible);
+ }
+
+ public Boolean matchOrNull(String query) {
+ return matchCache.matchOrNull(getChangeData(), query, task.isVisible);
+ }
+
+ protected class SubNodeAdder {
+ protected List<Node> nodes = new ArrayList<>();
+ protected SubNodeFactory factory = new SubNodeFactory();
+
+ public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
+ addSubTasks();
+ addSubTasksFactoryTasks();
+ addSubTasksFiles();
+ addSubTasksExternals();
+ return nodes;
}
- Account.Id acct;
- try {
- acct = accountResolver.resolve(user).asUnique().account().id();
- } catch (UnprocessableEntityException e) {
- throw new ConfigInvalidException("Cannot resolve user: " + user);
+
+ protected void addSubTasks() throws IOException, StorageException {
+ for (ConfigSourcedValue configSourcedValue : task.subTasks) {
+ try {
+ Optional<Task> def =
+ preloader.getOptionalTask(
+ taskExpressionFactory.create(
+ configSourcedValue.sourceFile(), configSourcedValue.value()));
+ if (def.isPresent()) {
+ addPreloaded(def.get());
+ }
+ } catch (ConfigInvalidException e) {
+ addInvalidNode();
+ }
+ }
}
- return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct));
+
+ protected void addSubTasksFiles() {
+ for (ConfigSourcedValue configSourcedValue : task.subTasksFiles) {
+ try {
+ addPreloaded(
+ preloader.getTasks(
+ FileKey.create(
+ configSourcedValue.sourceFile().branch(),
+ resolveTaskFileName(configSourcedValue.value()))));
+ } catch (ConfigInvalidException | IOException e) {
+ addInvalidNode();
+ }
+ }
+ }
+
+ protected void addSubTasksExternals() throws StorageException {
+ for (ConfigSourcedValue configSourcedValue : task.subTasksExternals) {
+ try {
+ External ext =
+ taskConfigCache
+ .getTaskConfig(configSourcedValue.sourceFile())
+ .getExternal(configSourcedValue.value());
+ if (ext == null) {
+ addInvalidNode();
+ } else {
+ addPreloaded(getPreloadedTasks(ext));
+ }
+ } catch (ConfigInvalidException | IOException e) {
+ addInvalidNode();
+ }
+ }
+ }
+
+ protected void addSubTasksFactoryTasks()
+ throws IOException, StorageException, ConfigInvalidException {
+ for (ConfigSourcedValue configSourcedValue : task.subTasksFactories) {
+ TasksFactory tasksFactory =
+ taskConfigCache
+ .getTaskConfig(configSourcedValue.sourceFile())
+ .getTasksFactory(configSourcedValue.value());
+ if (tasksFactory != null) {
+ NamesFactory namesFactory =
+ taskConfigCache
+ .getTaskConfig(configSourcedValue.sourceFile())
+ .getNamesFactory(tasksFactory.namesFactory);
+ if (namesFactory != null && namesFactory.type != null) {
+ namesFactory = properties.getNamesFactory(namesFactory);
+ switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) {
+ case STATIC:
+ addStaticTypeTasks(tasksFactory, namesFactory);
+ continue;
+ case CHANGE:
+ addChangeTypeTasks(tasksFactory, namesFactory);
+ continue;
+ }
+ }
+ }
+ addInvalidNode();
+ }
+ }
+
+ protected void addStaticTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+ throws IOException, StorageException {
+ for (String name : namesFactory.names) {
+ if (StringUtils.isEmptyOrNull(name)) {
+ addInvalidNode();
+ } else {
+ try {
+ addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name)));
+ } catch (ConfigInvalidException e) {
+ addInvalidNode();
+ }
+ }
+ }
+ }
+
+ protected void addChangeTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory)
+ throws IOException {
+ try {
+ if (namesFactory.changes != null) {
+ for (ChangeData changeData : query(namesFactory.changes, task.isVisible)) {
+ addPreloaded(
+ preloader.preload(
+ task.config.new Task(tasksFactory, changeData.getId().toString())),
+ changeData);
+ }
+ return;
+ }
+ } catch (StorageException e) {
+ log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes);
+ } catch (QueryParseException | ConfigInvalidException e) {
+ }
+ addInvalidNode();
+ }
+
+ public void addPreloaded(List<Task> defs) {
+ nodes.addAll(factory.createFromPreloaded(defs));
+ }
+
+ public void addPreloaded(Task def, ChangeData changeData) {
+ nodes.add(factory.createFromPreloaded(def, changeData));
+ }
+
+ public void addPreloaded(Task def) {
+ nodes.add(factory.createFromPreloaded(def));
+ }
+
+ public void addInvalidNode() {
+ nodes.add(factory.createInvalid());
+ }
+
+ protected List<Task> getPreloadedTasks(External external)
+ throws ConfigInvalidException, IOException, StorageException {
+ return preloader.getTasks(
+ FileKey.create(resolveUserBranch(external.user), resolveTaskFileName(external.file)));
+ }
+ }
+
+ public class ApplicableNodeFilter {
+ protected BranchNameKey branch = getChangeData().change().getDest();
+ protected SubSectionKey subSection = task.key.subSection();
+ protected Map<BranchNameKey, List<Task>> definitionsByBranch =
+ definitionsByBranchBySubSection.get(subSection);
+
+ public ApplicableNodeFilter() throws StorageException {}
+
+ public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException {
+ if (nodesByBranch != null) {
+ List<Node> nodes = nodesByBranch.get(branch);
+ if (nodes != null) {
+ return refresh(nodes);
+ }
+ }
+ if (definitionsByBranch != null) {
+ List<Task> branchDefinitions = definitionsByBranch.get(branch);
+ if (branchDefinitions != null) {
+ return new SubNodeFactory().createFromPreloaded(branchDefinitions);
+ }
+ }
+ List<Node> nodes = Node.this.getSubNodes();
+ if (isChange()
+ && definitionsByBranch == null
+ && definitionsByBranchBySubSection.containsKey(subSection)) {
+ hasUnfilterableSubNodes = true;
+ }
+
+ if (!hasUnfilterableSubNodes && !nodes.isEmpty()) {
+ Optional<List<Node>> filterable = getOptionalApplicableForBranch(nodes);
+ if (filterable.isPresent()) {
+ if (!isChange()) {
+ if (nodesByBranch == null) {
+ nodesByBranch = initStatistics(new HitHashMapOfCollection<>());
+ }
+ nodesByBranch.put(branch, filterable.get());
+ } else {
+ if (definitionsByBranch == null) {
+ definitionsByBranch = initStatistics(new HitHashMap<>());
+ definitionsByBranchBySubSection.put(subSection, definitionsByBranch);
+ }
+ definitionsByBranch.put(
+ branch,
+ filterable.get().stream().map(node -> node.getDefinition()).collect(toList()));
+ }
+ return filterable.get();
+ }
+ hasUnfilterableSubNodes = true;
+ if (isChange()) {
+ definitionsByBranchBySubSection.put(subSection, null);
+ }
+ }
+ return nodes;
+ }
+
+ protected Optional<List<Node>> getOptionalApplicableForBranch(List<Node> nodes)
+ throws StorageException {
+ int filterable = 0;
+ List<Node> applicableNodes = new ArrayList<>();
+ for (Node node : nodes) {
+ if (node instanceof Invalid) {
+ filterable++;
+ } else if (isApplicableCacheableByBranch(node)) {
+ filterable++;
+ try {
+ if (!node.match(node.task.applicable)) {
+ // Correctness will not be affected if more nodes are added than necessary
+ // (i.e. if isApplicableCacheableByBranch() does not realize a Node is cacheable
+ // based on its Branch), but it is incorrect to filter out a Node now that could
+ // later be applicable when a property, other than its Change's destination, is
+ // altered.
+ continue;
+ }
+ } catch (QueryParseException e) {
+ }
+ }
+ applicableNodes.add(node);
+ }
+ // Simple heuristic to determine whether storing the filtered nodes is worth it. There
+ // is minor evidence to suggest that storing a large list actually hurts performance.
+ return (filterable > nodes.size() / 2) ? Optional.of(applicableNodes) : Optional.empty();
+ }
+
+ protected boolean isApplicableCacheableByBranch(Node node) {
+ String applicable = node.task.applicable;
+ if (node.properties.isApplicableRefreshRequired()) {
+ return false;
+ }
+ try {
+ return predicateCache.isCacheableByBranch(applicable, task.isVisible);
+ } catch (QueryParseException e) {
+ return false;
+ }
+ }
}
}
- public class ChangeNodeFactory {
- public class ChangeNode extends Node {
- public ChangeNode(NodeList parent, Task definition) throws ConfigInvalidException {
- super(parent, definition);
- }
-
- public ChangeData getChangeData() {
- return ChangeNodeFactory.this.changeData;
- }
+ protected String resolveTaskFileName(String file) throws ConfigInvalidException {
+ if (file == null) {
+ throw new ConfigInvalidException("External file not defined");
}
-
- protected ChangeData changeData;
-
- public ChangeNodeFactory(ChangeData changeData) {
- this.changeData = changeData;
+ Path p = Paths.get(TASK_DIR, file);
+ if (!p.startsWith(TASK_DIR)) {
+ throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file);
}
+ return p.toString();
+ }
- public ChangeNode createChangeNodeOrNull(NodeList parent, Task definition) {
- try {
- return new ChangeNode(parent, definition);
- } catch (Exception e) {
- return null;
+ protected BranchNameKey resolveUserBranch(String user)
+ throws ConfigInvalidException, IOException, StorageException {
+ if (user == null) {
+ throw new ConfigInvalidException("External user not defined");
+ }
+ Account.Id acct;
+ try {
+ acct = accountResolver.resolve(user).asUnique().account().id();
+ } catch (UnprocessableEntityException e) {
+ throw new ConfigInvalidException("Cannot resolve user: " + user);
+ }
+ return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct));
+ }
+
+ @SuppressWarnings("try")
+ public List<ChangeData> query(String query, boolean isVisible)
+ throws StorageException, QueryParseException {
+ List<ChangeData> changeDataList = changesByNamesFactoryQuery.get(query);
+ if (changeDataList == null) {
+ try (StopWatch stopWatch =
+ changesByNamesFactoryQuery.createLoadingStopWatch(query, isVisible)) {
+ changeDataList =
+ changeQueryProcessorProvider
+ .get()
+ .query(changeQueryBuilderProvider.get().parse(query))
+ .entities();
}
+ changesByNamesFactoryQuery.put(query, changeDataList);
}
+ return changeDataList;
+ }
+
+ public void initStatistics(int summaryCount) {
+ statistics = new Statistics();
+ statistics.summaryCount = summaryCount;
+ definitionsBySubSection.initStatistics(summaryCount);
+ definitionsByBranchBySubSection.initStatistics(summaryCount);
+ changesByNamesFactoryQuery.initStatistics(summaryCount);
+ }
+
+ protected <T extends TracksStatistics> T initStatistics(T tracker) {
+ if (statistics != null) {
+ tracker.initStatistics(statistics.summaryCount);
+ }
+ return tracker;
+ }
+
+ public Statistics getStatistics() {
+ if (statistics != null) {
+ statistics.definitionsPerSubSectionCache = definitionsBySubSection.getStatistics();
+ statistics.definitionsByBranchBySubSectionCache =
+ definitionsByBranchBySubSection.getStatistics();
+ statistics.changesByNamesFactoryQueryCache = changesByNamesFactoryQuery.getStatistics();
+ }
+ return statistics;
+ }
+
+ protected static List<Node> refresh(List<Node> nodes) {
+ for (Node node : nodes) {
+ node.refreshTask();
+ }
+ return nodes;
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
new file mode 100644
index 0000000..1fd1e3a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
@@ -0,0 +1,26 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class ViewPathsCapability extends CapabilityDefinition {
+ public static final String VIEW_PATHS = "viewTaskPaths";
+
+ @Override
+ public String getDescription() {
+ return "View Task Paths";
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java
new file mode 100644
index 0000000..ce781d9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java
@@ -0,0 +1,189 @@
+// 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.googlesource.gerrit.plugins.task.properties;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Use to expand properties like ${property} in Strings into their values.
+ *
+ * <p>Given some property name/value associations like this:
+ *
+ * <p><code>
+ * "animal" -> "fox"
+ * "bar" -> "foo"
+ * "obstacle" -> "fence"
+ * </code>
+ *
+ * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code>
+ *
+ * <p>will expand to: <code>"The brown fox jumped over the fence."</code> This class is meant to be
+ * used as a building block for other full featured expanders and thus must be overriden to provide
+ * the name/value associations via the getValueForName() method.
+ */
+public abstract class AbstractExpander {
+ protected Consumer<Matcher.Statistics> statisticsConsumer;
+
+ protected final Map<Class<?>, Function<?, ?>> expanderByClass = new HashMap<>();
+
+ protected AbstractExpander() {
+ registerClassExpander(String.class, this::expandText);
+ }
+
+ public <T> void registerClassExpander(Class<? extends T> classType, Function<T, T> expander) {
+ expanderByClass.put(classType, expander);
+ }
+
+ public void setStatisticsConsumer(Consumer<Matcher.Statistics> statisticsConsumer) {
+ this.statisticsConsumer = statisticsConsumer;
+ }
+
+ /**
+ * Returns expanded object if property found in the Strings in the object's Fields (except the
+ * excluded ones). Returns same object if no expansions occurred.
+ */
+ public <C extends Cloneable> C expand(C object, Set<String> excludedFieldNames) {
+ return expand(new CopyOnWrite.CloneOnWrite<>(object), excludedFieldNames);
+ }
+
+ /**
+ * Returns expanded object if property found in the Strings in the object's Fields (except the
+ * excluded ones). Returns same object if no expansions occurred.
+ */
+ public <T> T expand(T object, Function<T, T> copier, Set<String> excludedFieldNames) {
+ return expand(new CopyOnWrite<>(object, copier), excludedFieldNames);
+ }
+
+ /**
+ * Returns expanded object if property found in the Strings in the object's Fields (except the
+ * excluded ones). Returns same object if no expansions occurred.
+ */
+ public <T> T expand(CopyOnWrite<T> cow, Set<String> excludedFieldNames) {
+ for (Field field : cow.getOriginal().getClass().getFields()) {
+ if (!excludedFieldNames.contains(field.getName())) {
+ expand(cow, field);
+ }
+ }
+ return cow.getForRead();
+ }
+
+ /**
+ * Returns expanded object if property found in the fieldName Field if it is a String, or in the
+ * List's Strings if it is a List. Returns same object if no expansions occurred.
+ */
+ public <T> T expand(CopyOnWrite<T> cow, String fieldName) {
+ try {
+ return expand(cow, cow.getOriginal().getClass().getField(fieldName));
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns expanded object if property found in the Field if it is a String, or in the List's
+ * Strings if it is a List. Returns same object if no expansions occurred.
+ */
+ public <T> T expand(CopyOnWrite<T> cow, Field field) {
+ try {
+ field.setAccessible(true);
+ Object o = field.get(cow.getOriginal());
+ if (o instanceof String) {
+ String expanded = expandText((String) o);
+ if (expanded != o) {
+ field.set(cow.getForWrite(), expanded);
+ }
+ } else if (o instanceof List) {
+ List<?> expanded = expand((List<?>) o);
+ if (expanded != o) {
+ field.set(cow.getForWrite(), expanded);
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ return cow.getForRead();
+ }
+
+ /**
+ * Returns expanded unmodifiable List if property found. Returns same object if no expansions
+ * occurred.
+ */
+ public <T> List<T> expand(List<T> list) {
+ if (list != null) {
+ boolean hasProperty = false;
+ List<T> expandedList = new ArrayList<>(list.size());
+ for (T value : list) {
+ T expanded = expand(value);
+ hasProperty = hasProperty || value != expanded;
+ expandedList.add(expanded);
+ }
+ return hasProperty ? Collections.unmodifiableList(expandedList) : list;
+ }
+ return null;
+ }
+
+ /**
+ * Expand all properties (${property_name} -> property_value) in the given generic value. Returns
+ * same object if no expansions occurred.
+ */
+ public <T> T expand(T value) {
+ if (value == null) {
+ return null;
+ }
+ @SuppressWarnings("unchecked")
+ Function<T, T> expander =
+ (Function<T, T>) expanderByClass.getOrDefault(value.getClass(), Function.identity());
+ return expander.apply(value);
+ }
+
+ /**
+ * Expand all properties (${property_name} -> property_value) in the given text. Returns same
+ * object if no expansions occurred.
+ */
+ public String expandText(String text) {
+ if (text == null) {
+ return null;
+ }
+ Matcher m = new Matcher(text);
+ m.setStatisticsConsumer(statisticsConsumer);
+ if (!m.find()) {
+ return text;
+ }
+ StringBuffer out = new StringBuffer();
+ do {
+ m.appendValue(out, getValueForName(m.getName()));
+ } while (m.find());
+ m.appendTail(out);
+ return out.toString();
+ }
+
+ /**
+ * Get the replacement value for the property identified by name
+ *
+ * @param name of the property to get the replacement value for
+ * @return the replacement value. Since the expandText() method alwyas needs a String to replace
+ * '${property-name}' reference with, even when the property does not exist, this will never
+ * return null, instead it will returns the empty string if the property is not found.
+ */
+ protected abstract String getValueForName(String name);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java
new file mode 100644
index 0000000..d1510fa
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java
@@ -0,0 +1,103 @@
+// 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.googlesource.gerrit.plugins.task.properties;
+
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.LongConsumer;
+
+public class CopyOnWrite<T> {
+ public static class CloneOnWrite<C extends Cloneable> extends CopyOnWrite<C> {
+ public CloneOnWrite(C cloneable) {
+ super(cloneable, copier(cloneable));
+ }
+ }
+
+ public static <C extends Cloneable> Function<C, C> copier(C cloneable) {
+ return c -> clone(c);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <C extends Cloneable> C clone(C cloneable) {
+ try {
+ for (Class<?> cls = cloneable.getClass(); cls != null; cls = cls.getSuperclass()) {
+ Optional<Method> optional = getOptionalDeclaredMethod(cls, "clone");
+ if (optional.isPresent()) {
+ Method clone = optional.get();
+ clone.setAccessible(true);
+ return (C) cloneable.getClass().cast(clone.invoke(cloneable));
+ }
+ }
+ throw new RuntimeException("Cannot find clone() method");
+ } catch (SecurityException
+ | IllegalAccessException
+ | IllegalArgumentException
+ | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * A faster getDeclaredMethod() without exceptions. The original apparently does a linear search
+ * anyway, and it is significantly slower when it throws NoSuchMethodExceptions.
+ */
+ public static Optional<Method> getOptionalDeclaredMethod(
+ Class<?> cls, String name, Class<?>... parameterTypes) {
+ for (Method method : cls.getDeclaredMethods()) {
+ if (method.getName().equals(name)
+ && Arrays.equals(method.getParameterTypes(), parameterTypes)) {
+ return Optional.of(method);
+ }
+ }
+ return Optional.empty();
+ }
+
+ protected Function<T, T> copier;
+ protected StopWatch.Runner stopWatch = StopWatch.Runner.DISABLED;
+ protected T original;
+ protected T copy;
+
+ public CopyOnWrite(T original, Function<T, T> copier) {
+ this.original = original;
+ this.copier = copier;
+ }
+
+ protected void setNanosecondsConsumer(LongConsumer nanosConsumer) {
+ stopWatch = new StopWatch.Runner.Enabled().setNanosConsumer(nanosConsumer);
+ }
+
+ public T getOriginal() {
+ return original;
+ }
+
+ public T getForRead() {
+ return isCopy() ? copy : original;
+ }
+
+ public T getForWrite() {
+ if (!isCopy()) {
+ stopWatch.run(() -> copy = copier.apply(original));
+ }
+ return copy;
+ }
+
+ public boolean isCopy() {
+ return copy != null;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java
new file mode 100644
index 0000000..df817a1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java
@@ -0,0 +1,89 @@
+// 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.googlesource.gerrit.plugins.task.properties;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Use to expand properties whose values may contain other references to properties.
+ *
+ * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there
+ * are no looping definitions.
+ *
+ * <p>Given some property name/value asssociations defined like this:
+ *
+ * <p><code>
+ * valueByName.put("obstacle", "fence");
+ * valueByName.put("action", "jumped over the ${obstacle}");
+ * </code>
+ *
+ * <p>a String like: <code>"The brown fox ${action}."</code>
+ *
+ * <p>will expand to: <code>"The brown fox jumped over the fence."</code>
+ */
+public class Expander extends AbstractExpander {
+ protected final Function<String, String> loadingFunction;
+ protected final Map<String, String> valueByName = new HashMap<>();
+ protected final Set<String> expanding = new HashSet<>();
+
+ public Expander(Function<String, String> loadingFunction) {
+ this.loadingFunction = loadingFunction;
+ }
+
+ /**
+ * Expand all properties (${property_name} -> property_value) in the given text. Returns same
+ * object if no expansions occurred.
+ */
+ public Map<String, String> expand(Map<String, String> map) {
+ if (map != null) {
+ boolean hasProperty = false;
+ Map<String, String> expandedMap = new HashMap<>(map.size());
+ for (Map.Entry<String, String> e : map.entrySet()) {
+ String name = e.getKey();
+ String value = e.getValue();
+ String expanded = getValueForName(name);
+ hasProperty = hasProperty || value != expanded;
+ expandedMap.put(name, expanded);
+ }
+ return hasProperty ? Collections.unmodifiableMap(expandedMap) : map;
+ }
+ return null;
+ }
+
+ @Override
+ public String getValueForName(String name) {
+ String value = valueByName.get(name);
+ if (value != null) {
+ return value;
+ }
+ value = loadingFunction.apply(name);
+ if (value == null) {
+ value = "";
+ } else if (!value.isEmpty()) {
+ if (!expanding.add(name)) {
+ throw new RuntimeException("Looping property definitions.");
+ }
+ value = expandText(value);
+ expanding.remove(name);
+ }
+ valueByName.put(name, value);
+ return value;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java
new file mode 100644
index 0000000..05120b3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java
@@ -0,0 +1,93 @@
+// 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.googlesource.gerrit.plugins.task.properties;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import java.util.function.Function;
+
+public class Loader {
+ protected final Task task;
+ protected final ChangeData changeData;
+ protected final Function<String, String> inherritedMapper;
+ protected Change change;
+ protected boolean isInheritedPropertyLoaded;
+
+ public Loader(Task task, ChangeData changeData, Function<String, String> inherritedMapper) {
+ this.task = task;
+ this.changeData = changeData;
+ this.inherritedMapper = inherritedMapper;
+ }
+
+ public boolean isNonTaskDefinedPropertyLoaded() {
+ return change != null || isInheritedPropertyLoaded;
+ }
+
+ public String load(String name) throws StorageException {
+ if (name.startsWith("_")) {
+ return internal(name);
+ }
+ String value = task.exported.get(name);
+ if (value == null) {
+ value = task.properties.get(name);
+ if (value == null) {
+ value = inherritedMapper.apply(name);
+ if (!value.isEmpty()) {
+ isInheritedPropertyLoaded = true;
+ }
+ }
+ }
+ return value;
+ }
+
+ protected String internal(String name) throws StorageException {
+ if ("_name".equals(name)) {
+ return task.name();
+ }
+ String changeProp = name.replace("_change_", "");
+ if (changeProp != name) {
+ return change(changeProp);
+ }
+ return "";
+ }
+
+ protected String change(String changeProp) throws StorageException {
+ switch (changeProp) {
+ case "number":
+ return String.valueOf(change().getId().get());
+ case "id":
+ return change().getKey().get();
+ case "project":
+ return change().getProject().get();
+ case "branch":
+ return change().getDest().branch();
+ case "status":
+ return change().getStatus().toString();
+ case "topic":
+ return change().getTopic();
+ default:
+ return "";
+ }
+ }
+
+ protected Change change() {
+ if (change == null) {
+ change = changeData.change();
+ }
+ return change;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java
new file mode 100644
index 0000000..68151fb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java
@@ -0,0 +1,103 @@
+// 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.googlesource.gerrit.plugins.task.properties;
+
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import java.util.function.Consumer;
+
+/** A handcrafted properties Matcher which has an API similar to an RE Matcher, but is faster. */
+public class Matcher {
+ public static class Statistics {
+ public long appendNanoseconds;
+ public long findNanoseconds;
+
+ public Statistics sum(Statistics other) {
+ if (other == null) {
+ return this;
+ }
+ Statistics statistics = new Statistics();
+ statistics.appendNanoseconds = appendNanoseconds + other.appendNanoseconds;
+ statistics.findNanoseconds = findNanoseconds + other.findNanoseconds;
+ return statistics;
+ }
+ }
+
+ protected String text;
+ protected int start;
+ protected int nameStart;
+ protected int end;
+ protected int cursor;
+
+ protected Statistics statistics;
+ protected StopWatch.Runner appendNanoseconds = StopWatch.Runner.DISABLED;
+ protected StopWatch.Runner findNanoseconds = StopWatch.Runner.DISABLED;
+
+ public Matcher(String text) {
+ this.text = text;
+ }
+
+ protected void setStatisticsConsumer(Consumer<Statistics> statisticsConsumer) {
+ if (statisticsConsumer != null) {
+ statistics = new Statistics();
+ statisticsConsumer.accept(statistics);
+ appendNanoseconds =
+ new StopWatch.Runner.Enabled().setNanosConsumer(ns -> statistics.appendNanoseconds = ns);
+ findNanoseconds =
+ new StopWatch.Runner.Enabled().setNanosConsumer(ns -> statistics.findNanoseconds = ns);
+ }
+ }
+
+ public boolean find() {
+ return findNanoseconds.get(() -> findUntimed());
+ }
+
+ protected boolean findUntimed() {
+ start = text.indexOf("${", cursor);
+ nameStart = start + 2;
+ if (start < 0 || text.length() < nameStart + 1) {
+ return false;
+ }
+ end = text.indexOf('}', nameStart);
+ boolean found = end >= 0;
+ return found;
+ }
+
+ public String getName() {
+ return text.substring(nameStart, end);
+ }
+
+ public void appendValue(StringBuffer buffer, String value) {
+ appendNanoseconds.accept((b, v) -> appendValueUntimed(b, v), buffer, value);
+ }
+
+ protected void appendValueUntimed(StringBuffer buffer, String value) {
+ if (start > cursor) {
+ buffer.append(text.substring(cursor, start));
+ }
+ buffer.append(value);
+ cursor = end + 1;
+ }
+
+ public void appendTail(StringBuffer buffer) {
+ appendNanoseconds.accept(b -> appendTailUntimed(b), buffer);
+ }
+
+ protected void appendTailUntimed(StringBuffer buffer) {
+ if (cursor < text.length()) {
+ buffer.append(text.substring(cursor));
+ cursor = text.length();
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java
new file mode 100644
index 0000000..6177f2b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java
@@ -0,0 +1,170 @@
+// 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.googlesource.gerrit.plugins.task.properties;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlesource.gerrit.plugins.task.ConfigSourcedValue;
+import com.googlesource.gerrit.plugins.task.TaskConfig;
+import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory;
+import com.googlesource.gerrit.plugins.task.TaskConfig.Task;
+import com.googlesource.gerrit.plugins.task.TaskTree;
+import com.googlesource.gerrit.plugins.task.statistics.StopWatch;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/** Use to expand properties like ${_name} in the text of various definitions. */
+public class Properties {
+ public static class Statistics {
+ public long getTaskNanoseconds;
+ public long copierNanoseconds;
+ public Matcher.Statistics matcher;
+
+ public static void setNanoseconds(Statistics stats, long nanos) {
+ if (stats != null) {
+ stats.getTaskNanoseconds = nanos;
+ }
+ }
+
+ public Statistics sum(Statistics other) {
+ if (other == null) {
+ return this;
+ }
+ Statistics statistics = new Statistics();
+ statistics.getTaskNanoseconds = getTaskNanoseconds + other.getTaskNanoseconds;
+ statistics.copierNanoseconds = copierNanoseconds + other.copierNanoseconds;
+ statistics.matcher = matcher == null ? other.matcher : matcher.sum(other.matcher);
+ return statistics;
+ }
+ }
+
+ public static final Properties EMPTY =
+ new Properties() {
+ @Override
+ protected Function<String, String> getParentMapper() {
+ return n -> "";
+ }
+ };
+
+ public final Task origTask;
+ protected final TaskTree.Node node;
+ protected final CopyOnWrite<Task> task;
+ protected Statistics statistics;
+ protected Consumer<Statistics> statisticsConsumer;
+ protected Consumer<Matcher.Statistics> matcherStatisticsConsumer;
+ protected Expander expander;
+ protected Loader loader;
+ protected boolean init = true;
+ protected boolean isTaskRefreshRequired;
+ protected boolean isApplicableRefreshRequired;
+ protected boolean isSubNodeReloadRequired;
+
+ public Properties() {
+ this(null, null);
+ expander = new Expander(n -> "");
+ }
+
+ public Properties(TaskTree.Node node, Task origTask) {
+ this.node = node;
+ this.origTask = origTask;
+ task = new CopyOnWrite.CloneOnWrite<>(origTask);
+ }
+
+ /** Use to expand properties specifically for Tasks. */
+ @SuppressWarnings("try")
+ public Task getTask(ChangeData changeData) {
+ try (StopWatch stopWatch =
+ StopWatch.builder()
+ .enabled(statistics != null)
+ .build()
+ .setNanosConsumer(l -> Statistics.setNanoseconds(statistics, l))) {
+ loader = new Loader(origTask, changeData, getParentMapper());
+ expander = new Expander(n -> loader.load(n));
+ expander.registerClassExpander(
+ ConfigSourcedValue.getClassType(), getConfigSourcedValueExpander(expander));
+ expander.setStatisticsConsumer(matcherStatisticsConsumer);
+ if (isTaskRefreshRequired || init) {
+ expander.expand(task, TaskConfig.KEY_APPLICABLE);
+ isApplicableRefreshRequired = loader.isNonTaskDefinedPropertyLoaded();
+
+ expander.expand(task, ImmutableSet.of(TaskConfig.KEY_APPLICABLE, TaskConfig.KEY_NAME));
+
+ Map<String, String> exported = expander.expand(origTask.exported);
+ if (exported != origTask.exported) {
+ task.getForWrite().exported = exported;
+ }
+
+ if (init) {
+ init = false;
+ isTaskRefreshRequired = loader.isNonTaskDefinedPropertyLoaded();
+ }
+ }
+ }
+ if (statisticsConsumer != null) {
+ statisticsConsumer.accept(statistics);
+ }
+ return task.getForRead();
+ }
+
+ protected Function<ConfigSourcedValue, ConfigSourcedValue> getConfigSourcedValueExpander(
+ Expander expander) {
+ return t -> {
+ String toExpand = t.value();
+ String expanded = expander.expandText(toExpand);
+ if (toExpand != expanded) {
+ return ConfigSourcedValue.create(t.sourceFile(), expanded);
+ }
+ return t;
+ };
+ }
+
+ public void setStatisticsConsumer(Consumer<Statistics> statisticsConsumer) {
+ if (statisticsConsumer != null) {
+ this.statisticsConsumer = statisticsConsumer;
+ statistics = new Statistics();
+ matcherStatisticsConsumer = s -> statistics.matcher = s;
+ task.setNanosecondsConsumer(ns -> statistics.copierNanoseconds = ns);
+ }
+ }
+
+ // To detect NamesFactories dependent on non task defined properties, the checking must be
+ // done after subnodes are fully loaded, which unfortunately happens after getTask() is
+ // called, therefore this must be called after all subnodes have been loaded.
+ public void expansionComplete() {
+ isSubNodeReloadRequired = loader.isNonTaskDefinedPropertyLoaded();
+ }
+
+ public boolean isApplicableRefreshRequired() {
+ return isApplicableRefreshRequired;
+ }
+
+ public boolean isTaskRefreshRequired() {
+ return isTaskRefreshRequired;
+ }
+
+ public boolean isSubNodeReloadRequired() {
+ return isSubNodeReloadRequired;
+ }
+
+ /** Use to expand properties specifically for NamesFactories. */
+ public NamesFactory getNamesFactory(NamesFactory namesFactory) {
+ return expander.expand(namesFactory, ImmutableSet.of(TaskConfig.KEY_TYPE));
+ }
+
+ protected Function<String, String> getParentMapper() {
+ return n -> node.getParentProperties().expander.getValueForName(n);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java
new file mode 100644
index 0000000..2f35084
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java
@@ -0,0 +1,94 @@
+// 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.googlesource.gerrit.plugins.task.statistics;
+
+import com.google.gerrit.common.BooleanTable;
+import com.googlesource.gerrit.plugins.task.util.TopKeyMap;
+
+/**
+ * A space efficient Table for Booleans. This Table takes advantage of the fact that the values
+ * stored in it are all Booleans and uses BitSets to make this very space efficient.
+ */
+public class HitBooleanTable<R, C> extends BooleanTable<R, C> implements TracksStatistics {
+ public static class Statistics<V> {
+ public long hits;
+ public long misses;
+ public long size;
+ public int numberOfRows;
+ public int numberOfColumns;
+ public Long sumNanosecondsLoading;
+ public TopKeyMap<V> topNanosecondsLoadingKeys;
+ }
+
+ protected Statistics<TopKeyMap.TableKeyValue<R, C>> statistics;
+
+ @Override
+ public Boolean get(R r, C c) {
+ Boolean value = super.get(r, c);
+ if (statistics != null) {
+ if (value != null) {
+ statistics.hits++;
+ } else {
+ statistics.misses++;
+ }
+ }
+ return value;
+ }
+
+ public StopWatch createLoadingStopWatch(R row, C column, boolean isVisible) {
+ if (statistics == null) {
+ return StopWatch.DISABLED;
+ }
+ if (statistics.sumNanosecondsLoading == null) {
+ statistics.sumNanosecondsLoading = 0L;
+ }
+ return new StopWatch.Enabled()
+ .setNanosConsumer(
+ ns ->
+ statistics.sumNanosecondsLoading +=
+ updateTopLoadingTimes(ns, row, column, isVisible));
+ }
+
+ public long updateTopLoadingTimes(long nanos, R row, C column, boolean isVisible) {
+ statistics.topNanosecondsLoadingKeys.addIfTop(
+ nanos, isVisible ? new TopKeyMap.TableKeyValue<R, C>(row, column) : null);
+ return nanos;
+ }
+
+ @Override
+ public void initStatistics(int summaryCount) {
+ statistics = new Statistics<>();
+ statistics.topNanosecondsLoadingKeys = new TopKeyMap<>(summaryCount);
+ }
+
+ @Override
+ public void ensureStatistics(int summaryCount) {
+ if (statistics == null) {
+ initStatistics(summaryCount);
+ }
+ }
+
+ @Override
+ public Object getStatistics() {
+ statistics.numberOfRows = rowByRow.size();
+ statistics.numberOfColumns = positionByColumn.size();
+ statistics.size =
+ rowByRow.values().stream()
+ .map(r -> (long) r.hasValues.size() + (long) r.values.size())
+ .mapToLong(Long::longValue)
+ .sum();
+ return statistics;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java
new file mode 100644
index 0000000..ccb12a9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java
@@ -0,0 +1,184 @@
+// 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.googlesource.gerrit.plugins.task.statistics;
+
+import static java.util.stream.Collectors.toList;
+
+import com.googlesource.gerrit.plugins.task.util.TopKeyMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public class HitHashMap<K, V> extends HashMap<K, V> implements StatisticsMap<K, V> {
+ public static class Statistics<K> {
+ public long hits;
+ public int size;
+ public Long sumNanosecondsLoading;
+ public TopKeyMap<K> topNanosecondsLoadingKeys;
+ public List<Object> elements;
+ }
+
+ public static final long serialVersionUID = 1;
+
+ protected Statistics<K> statistics;
+
+ public HitHashMap() {}
+
+ @Override
+ public V get(Object key) {
+ V v = super.get(key);
+ if (statistics != null && v != null) {
+ statistics.hits++;
+ }
+ return v;
+ }
+
+ @Override
+ public V getOrDefault(Object key, V dv) {
+ V v = get(key);
+ if (v == null) {
+ return dv;
+ }
+ return v;
+ }
+
+ @Override
+ @SuppressWarnings("try")
+ public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+ V v = get(key);
+ if (v == null) {
+ v = mappingFunction.apply(key);
+ if (v != null) {
+ put(key, v);
+ }
+ }
+ return v;
+ }
+
+ @Override
+ @SuppressWarnings("try")
+ public V computeIfAbsentTimed(
+ K key, Function<? super K, ? extends V> mappingFunction, boolean isVisible) {
+ V v = get(key);
+ if (v == null) {
+ try (StopWatch stopWatch = createLoadingStopWatch(key, isVisible)) {
+ v = mappingFunction.apply(key);
+ }
+ if (v != null) {
+ put(key, v);
+ }
+ }
+ return v;
+ }
+
+ @Override
+ public V put(K key, V value) {
+ if (statistics != null && value instanceof TracksStatistics) {
+ ((TracksStatistics) value).ensureStatistics(statistics.topNanosecondsLoadingKeys.size());
+ }
+ return super.put(key, value);
+ }
+
+ @Override
+ public void putAll(Map<? extends K, ? extends V> m) {
+ m.entrySet().stream().forEach(e -> put(e.getKey(), e.getValue()));
+ }
+
+ @Override
+ public V putIfAbsent(K key, V value) {
+ if (!containsKey(key)) {
+ put(key, value);
+ return null;
+ }
+ return get(key);
+ }
+
+ @Override
+ public V computeIfPresent(
+ K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+ throw new UnsupportedOperationException(); // Todo if needed
+ }
+
+ @Override
+ public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+ throw new UnsupportedOperationException(); // Todo if needed
+ }
+
+ @Override
+ public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+ throw new UnsupportedOperationException(); // Todo if needed
+ }
+
+ @Override
+ public V replace(K key, V value) {
+ throw new UnsupportedOperationException(); // Todo if needed
+ }
+
+ @Override
+ public boolean replace(K key, V oldValue, V newValue) {
+ throw new UnsupportedOperationException(); // Todo if needed
+ }
+
+ @Override
+ public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
+ throw new UnsupportedOperationException(); // Todo if needed
+ }
+
+ @Override
+ public void initStatistics(int summaryCount) {
+ statistics = new Statistics<>();
+ statistics.topNanosecondsLoadingKeys = new TopKeyMap<>(summaryCount);
+ }
+
+ @Override
+ public void ensureStatistics(int summaryCount) {
+ if (statistics == null) {
+ initStatistics(summaryCount);
+ }
+ }
+
+ public StopWatch createLoadingStopWatch(K key, boolean isVisible) {
+ if (statistics == null) {
+ return StopWatch.DISABLED;
+ }
+ if (statistics.sumNanosecondsLoading == null) {
+ statistics.sumNanosecondsLoading = 0L;
+ }
+ return new StopWatch.Enabled()
+ .setNanosConsumer(
+ ns -> statistics.sumNanosecondsLoading += updateTopLoadingTimes(ns, key, isVisible));
+ }
+
+ public long updateTopLoadingTimes(long nanos, K key, boolean isVisible) {
+ statistics.topNanosecondsLoadingKeys.addIfTop(nanos, isVisible ? key : null);
+ return nanos;
+ }
+
+ @Override
+ public Object getStatistics() {
+ statistics.size = size();
+ List<Object> elementStatistics =
+ values().stream()
+ .filter(e -> e instanceof TracksStatistics)
+ .map(e -> ((TracksStatistics) e).getStatistics())
+ .collect(toList());
+ if (!elementStatistics.isEmpty()) {
+ statistics.elements = elementStatistics;
+ }
+ return statistics;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java
new file mode 100644
index 0000000..31a9d61
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java
@@ -0,0 +1,65 @@
+// 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.googlesource.gerrit.plugins.task.statistics;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+public class HitHashMapOfCollection<K, V extends Collection<?>> extends HitHashMap<K, V> {
+ public static class Statistics<K> extends HitHashMap.Statistics<K> {
+ public List<Integer> top5CollectionSizes;
+ public List<Integer> bottom5CollectionSizes;
+ }
+
+ public static final long serialVersionUID = 1;
+
+ protected Statistics<K> statistics;
+
+ public HitHashMapOfCollection() {}
+
+ @Override
+ public void initStatistics(int summaryCount) {
+ super.initStatistics(summaryCount);
+ statistics = new Statistics<>();
+ }
+
+ @Override
+ public Object getStatistics() {
+ super.getStatistics();
+ statistics.hits = super.statistics.hits;
+ statistics.size = super.statistics.size;
+
+ List<Integer> collectionSizes =
+ values().stream().map(l -> l.size()).sorted(Comparator.reverseOrder()).collect(toList());
+ statistics.top5CollectionSizes = new ArrayList<>(5);
+ statistics.bottom5CollectionSizes = new ArrayList<>(5);
+ for (int i = 0; i < 5 && i < collectionSizes.size(); i++) {
+ statistics.top5CollectionSizes.add(collectionSizes.get(i));
+ int bottom = collectionSizes.size() - 6 + i;
+ if (bottom > 4 && bottom < collectionSizes.size()) {
+ // The > 4 ensures that there are no entries also in the top list
+ statistics.bottom5CollectionSizes.add(collectionSizes.get(bottom));
+ }
+ }
+ if (statistics.bottom5CollectionSizes.isEmpty()) {
+ statistics.bottom5CollectionSizes = null;
+ }
+ return statistics;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java
new file mode 100644
index 0000000..5bca24d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java
@@ -0,0 +1,25 @@
+// 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.googlesource.gerrit.plugins.task.statistics;
+
+import java.util.Map;
+import java.util.function.Function;
+
+public interface StatisticsMap<K, V> extends Map<K, V>, TracksStatistics {
+ V computeIfAbsentTimed(
+ K key, Function<? super K, ? extends V> mappingFunction, boolean isVisible);
+
+ StopWatch createLoadingStopWatch(K key, boolean isVisible);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java
new file mode 100644
index 0000000..05f942b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java
@@ -0,0 +1,140 @@
+// 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.googlesource.gerrit.plugins.task.statistics;
+
+import com.google.common.base.Stopwatch;
+import com.googlesource.gerrit.plugins.task.util.SamTryWrapper;
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongConsumer;
+
+/**
+ * StopWatches with APIs designed to make it easy to disable, and to make robust in the face of
+ * exceptions.
+ *
+ * <p>The Stopwatch class from google commons is used by placing start() and stop() calls around
+ * code sections which need to be timed. This approach can be problematic since the code being timed
+ * could throw an exception and if the stop() is not in a finally clause, then it will likely never
+ * get called, potentially causing bad timings, or worse programatic issues elsewhere due to double
+ * calls to start(). The need for a finally clause to make things safe is an obvious hint that using
+ * an AutoCloseable approach is likely going to be safer. With that in mind, the two API approaches
+ * provided by these StopWatch classes are:
+ *
+ * <ol>
+ * <li>The timed try-with-resource API. Use the StopWatch.Enabled class for this API.
+ * <li>The timed SAM evaluation API. Use the StopWatch.Runner.Enabled class for these APIs.
+ * </ol>
+ *
+ * <p>Finally, the commons stopwatch API also does not provide an easy way to disable timings at
+ * runtime when they are not desired, so the DISABLED classes can be used for this with both
+ * approaches above, and thus provide low cost runtime substitutes for either the StopWatch.Enabled
+ * or StopWatch.Runner.Enabled classes.
+ */
+public interface StopWatch extends AutoCloseable {
+ /** Designed for the greatest simplicity to time SAM executions. */
+ public abstract static class Runner extends SamTryWrapper<AutoCloseable> {
+ public static class Enabled extends Runner {
+ protected LongConsumer nanosConsumer = EMPTY_LONG_CONSUMER;
+
+ @Override
+ protected AutoCloseable getAutoCloseable() {
+ return new StopWatch.Enabled().setNanosConsumer(nanosConsumer);
+ }
+
+ @Override
+ public Runner setNanosConsumer(LongConsumer nanosConsumer) {
+ this.nanosConsumer = nanosConsumer;
+ return this;
+ }
+ }
+
+ /** May be used anywhere that Enabled can be used */
+ public static final Runner DISABLED =
+ new Runner() {
+ @Override
+ protected AutoCloseable getAutoCloseable() {
+ return () -> {};
+ }
+ };
+
+ public Runner setNanosConsumer(LongConsumer nanosConsumer) {
+ return this;
+ }
+ }
+
+ /** Should be created and used only within a try-with-resource */
+ public static class Enabled implements StopWatch {
+ protected LongConsumer nanosConsumer = EMPTY_LONG_CONSUMER;
+ protected Stopwatch stopwatch = Stopwatch.createStarted();
+
+ @Override
+ public StopWatch setNanosConsumer(LongConsumer nanosConsumer) {
+ this.nanosConsumer = nanosConsumer;
+ return this;
+ }
+
+ @Override
+ public void close() {
+ stopwatch.stop();
+ nanosConsumer.accept(stopwatch.elapsed(TimeUnit.NANOSECONDS));
+ }
+ }
+
+ /**
+ * A easy way to build a timer which needes to be enabled/disabled based on a runtime boolean.
+ *
+ * <p>Example Usage:
+ *
+ * <p><code>
+ * try (StopWatch stopWatch =
+ * StopWatch.builder().enabled(myBoolean).build().setNanosConsumer(myConsumer)) {
+ * // Code to be timed here...
+ * }
+ * </code>
+ */
+ public static class Builder {
+ protected static class Enabled extends Builder {
+ @Override
+ public StopWatch build() {
+ return new StopWatch.Enabled();
+ }
+ }
+
+ protected static final Builder ENABLED = new Enabled();
+ protected static final Builder DISABLED = new Builder();
+
+ public Builder enabled(boolean enabled) {
+ return enabled ? ENABLED : this;
+ }
+
+ public StopWatch build() {
+ return StopWatch.DISABLED;
+ }
+ }
+
+ /** May be used anywhere that Enabled can be used */
+ public static final StopWatch DISABLED = new StopWatch() {};
+
+ public static final LongConsumer EMPTY_LONG_CONSUMER = l -> {};
+
+ public static Builder builder() {
+ return Builder.DISABLED;
+ }
+
+ default StopWatch setNanosConsumer(LongConsumer nanosConsumer) {
+ return this;
+ }
+
+ default void close() {}
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java
new file mode 100644
index 0000000..a150600
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java
@@ -0,0 +1,23 @@
+// 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.googlesource.gerrit.plugins.task.statistics;
+
+public interface TracksStatistics {
+ void initStatistics(int summaryCount);
+
+ void ensureStatistics(int summaryCount);
+
+ Object getStatistics();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java
new file mode 100644
index 0000000..434a4de
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java
@@ -0,0 +1,43 @@
+// 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.googlesource.gerrit.plugins.task.util;
+
+import java.lang.reflect.Field;
+
+public class Copier {
+ public static <T> void shallowCopyDeclaredFields(
+ Class<T> cls, T from, T to, boolean includeInaccessible) {
+ for (Field field : cls.getDeclaredFields()) {
+ try {
+ if (includeInaccessible) {
+ field.setAccessible(true);
+ }
+ Object val = field.get(from);
+ if (!field.getName().equals("this$0")) { // Can't copy internal final field
+ field.set(to, val);
+ }
+ } catch (IllegalAccessException e) {
+ if (includeInaccessible) {
+ throw new RuntimeException(
+ "Cannot access field to copy it " + fieldValueToString(field, "unknown"));
+ }
+ }
+ }
+ }
+
+ protected static String fieldValueToString(Field field, Object val) {
+ return "field:" + field.getName() + " value:" + val + " type:" + field.getType();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java
new file mode 100644
index 0000000..ef42e5a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java
@@ -0,0 +1,84 @@
+// 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.googlesource.gerrit.plugins.task.util;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Class designed to make SAM calls wrapped by an AutoCloseable. This is usefull with AutoCloseables
+ * which do not provide resources which are directly needed during the SAM call, but rather with
+ * AutoCloseables which likely manage external resources or state such as a locks or timers.
+ */
+public abstract class SamTryWrapper<A extends AutoCloseable> {
+ protected abstract A getAutoCloseable();
+
+ @SuppressWarnings("try")
+ public void run(Runnable runnable) {
+ try (A autoCloseable = getAutoCloseable()) {
+ runnable.run();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("try")
+ public <T> T get(Supplier<T> supplier) {
+ try (A autoCloseable = getAutoCloseable()) {
+ return supplier.get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("try")
+ public <T> void accept(Consumer<T> consumer, T t) {
+ try (A autoCloseable = getAutoCloseable()) {
+ consumer.accept(t);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("try")
+ public <T, U> void accept(BiConsumer<T, U> consumer, T t, U u) {
+ try (A autoCloseable = getAutoCloseable()) {
+ consumer.accept(t, u);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("try")
+ public <T, R> R apply(Function<T, R> func, T t) {
+ try (A autoCloseable = getAutoCloseable()) {
+ return func.apply(t);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("try")
+ public <T, U, R> R apply(BiFunction<T, U, R> func, T t, U u) {
+ try (A autoCloseable = getAutoCloseable()) {
+ return func.apply(t, u);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
similarity index 95%
rename from src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java
rename to src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
index 7644143..3d5197e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.googlesource.gerrit.plugins.task;
+package com.googlesource.gerrit.plugins.task.util;
public interface ThrowingProvider<V, E extends Exception> {
public V get() throws E;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
new file mode 100644
index 0000000..a6627fb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
@@ -0,0 +1,94 @@
+// 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.googlesource.gerrit.plugins.task.util;
+
+/**
+ * A TopKeyMap is a lightweight limited size (default 5) map with 'long' keys designed to store only
+ * the elements with the top five largest keys.
+ *
+ * <p>A TopKeyMap is array based and has O(n) insertion time. Despite not having O(1) insertion
+ * times, it should likely be much faster than a hash based map for small n sizes. It also is more
+ * memory efficient than a hash based map, although both are likely O(n) in space usage. The
+ * TopKeyMap allocates all of its entries up front so it does not change its memory utilization at
+ * all, and it does not have to create or free any Objects during its post constructor lifespan.
+ *
+ * <p>While a TopKeyMap currently only uses 'long's as keys, it is possible to easiy upgrade this
+ * collection to use any type of Comparable key.
+ *
+ * <p>Although not currently thread safe, due to the simplicity of the data structures used, and the
+ * insertion approach, it is easy to make a TopKeyMap efficiently thread safe.
+ */
+public class TopKeyMap<V> {
+ /**
+ * A TableKeyValue is a helper class for TopKeyMap use cases, such as a table with with row and
+ * column keys, which involve two values.
+ */
+ public static class TableKeyValue<R, C> {
+ public final R row;
+ public final C column;
+
+ public TableKeyValue(R row, C column) {
+ this.row = row;
+ this.column = column;
+ }
+ }
+
+ protected class Entry {
+ public long key;
+ public V value;
+
+ protected void set(long key, V value) {
+ this.key = key;
+ this.value = value;
+ }
+ }
+
+ protected Entry[] entries;
+
+ public TopKeyMap() {
+ this(5);
+ }
+
+ @SuppressWarnings("unchecked")
+ public TopKeyMap(int length) {
+ entries = (Entry[]) new Object[length];
+ for (int i = 0; i < entries.length; i++) {
+ entries[i] = new Entry();
+ }
+ }
+
+ public void addIfTop(long key, V value) {
+ addIfTop(0, key, value);
+ }
+
+ protected void addIfTop(int i, long key, V value) {
+ if (entries[entries.length - 1].key < key) {
+ for (; i < entries.length; i++) {
+ Entry e = entries[i];
+ if (e.key < key) {
+ long eKValue = e.key;
+ V eValue = e.value;
+ e.set(key, value);
+ addIfTop(i + 1, eKValue, eValue);
+ return;
+ }
+ }
+ }
+ }
+
+ public int size() {
+ return entries.length;
+ }
+}
diff --git a/src/main/resources/Documentation/config-gerrit.md b/src/main/resources/Documentation/config-gerrit.md
new file mode 100644
index 0000000..6f0b0a5
--- /dev/null
+++ b/src/main/resources/Documentation/config-gerrit.md
@@ -0,0 +1,25 @@
+# Admin User Guide - Configuration
+
+## File `etc/gerrit.config`
+
+The file `'$site_path'/etc/gerrit.config` is a Git-style config file
+that controls many host specific settings for Gerrit.
+
+### Section @PLUGIN@ "cacheable-predicates"
+
+The @PLUGIN@.cacheable-predicates section configures Change Predicate
+optimizations which the @PLUGIN@ plugin may use when evaluating tasks.
+
+#### @PLUGIN@.cacheable-predicates.byBranch-className
+
+The value set with this key specifies a fully qualified class name
+of a Predicate which can be assumed to always return the same match
+result to all Changes destined for the same project/branch
+combinations. This key may be specified more than once.
+
+Example:
+
+```
+[@PLUGIN@ "cacheable-predicates"]
+ byBranch-className = com.google.gerrit.server.query.change.BranchSetPredicate
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md
index fa5e834..75ee189 100644
--- a/src/main/resources/Documentation/task.md
+++ b/src/main/resources/Documentation/task.md
@@ -51,11 +51,17 @@
completes.
A task with a `WAITING` status is not yet ready to execute. A task in this
-state is blocked by its subtasks which are not yet in the `PASS` state.
+state is blocked by its subtasks which are not yet in the `PASS` or `DUPLICATE`
+state.
A task with a `READY` status is ready to be executed. All of its subtasks are
in the `PASS` state.
+A task with a `DUPLICATE` status has the same task key as one of its ancestors.
+Task keys are generally made up of the canonical task name and the change to
+which it applies. To avoid infinite loops, subtasks are ignored on duplicate
+tasks.
+
A task with a `PASS` status meets all the criteria for `READY`, and has
executed and was successful.
@@ -160,8 +166,8 @@
from the current task if they redefined in the current task. Attributes
which are lists (such as subtasks) or maps (such as properties), will be
preloaded by the preload-task and then extended with the attributes from the
-current task. See [Optional Tasks](#optional_tasks) for how to define optional
-preload-tasks.
+current task. See [Task Expression](task_expression.html) for how to define
+optional preload-tasks.
Example:
```
@@ -172,8 +178,8 @@
: This key lists the name of a subtask of the current task. This key may be
used several times in a task section to define more than one subtask for a
-particular task. See [Optional Tasks](#optional_tasks) for how to define
-optional subtasks.
+particular task. See [Task Expression](task_expression.html) for how to define
+subtasks.
Example:
@@ -231,6 +237,48 @@
subtasks-file = common.config # references the file named task/common.config
```
+`duplicate-key`
+
+: This key defines an identifier to help identify tasks which should be
+considered duplicates even if they are not exact duplicates. When the task
+plugin encounters a task with the same duplicate-key as one of its
+ancestors, it will be considered a duplicate of that ancestor. Tasks such as
+a starting task and a looping tasks-factory that preload the same base task
+are not exact duplicates, yet they may logically represent duplicates. In
+this case, defining a `duplicate-key` on the base task which is preloaded
+from two different places (usually a root and a change tasks-factory), will
+ensure that any loops are halted once the original change is reached. Without
+a duplicate-key, the walking would generally walk one task further than
+desired.
+
+Outlined below is a simple way to walk a change's git dependencies in the
+task plugin. While Git does not allow loops in commit histories, sometimes
+in Gerrit when changes get rebased, it can cause loops (because Gerrit
+sometimes tracks outdated dependencies). The use of the duplicate-key
+below results in the loop being detected when you would expect it to be.
+
+Example:
+
+```
+[root "git dependencies"]
+ applicable = status:new
+ preload-task = git dependencies
+
+[task "git dependencies"]
+ fail = -status:new
+ fail-hint = [${_change_status}] dependency needs to be OPEN
+ subtasks-factory = git dependencies
+ duplicate-key = git dependencies ${_change_number}
+
+[tasks-factory "git dependencies"]
+ names-factory = git dependencies
+ preload-task = git dependencies
+
+[names-factory "git dependencies"]
+ type = change
+ changes = -status:merged parentof:${_change_number} project:${_change_project} branch:${_change_branch}
+```
+
Root Tasks
----------
Root tasks typically define the "final verification" tasks for changes. Each
@@ -275,26 +323,7 @@
fail = label:code-review-2
```
-<a id="optional_tasks"/>
-Optional Tasks
---------------
-To define a task that may not exist and that will not cause the task referencing
-it to be INVALID, follow the task name with pipe (`|`) character. This feature
-is particularly useful when a property is used in the task name.
-
-```
- preload-task = Optional Subtask {$_name} |
-```
-
-To define an alternate task to load when an optional task does not exist,
-list the alterante task name after the pipe (`|`) character. This feature
-may be chained together as many times as needed.
-
-```
- subtask = Optional Subtask {$_name} |
- Backup Optional Subtask {$_name} Backup |
- Default Subtask # Must exist if the above two don't!
-```
+<a id="tasks_factory"/>
Tasks-Factory
-------------
A tasks-factory section supports all the keys supported by task sections. In
@@ -419,9 +448,9 @@
Examples:
```
- fail-hint = {$_name} needs to be fixed
- fail-hint = {$_change_number} with {$_change_status} needs to be fixed
- fail-hint = {$_change_id} on {$_change_project} and {$_change_branch} needs to be fixed
+ fail-hint = ${_name} needs to be fixed
+ fail-hint = ${_change_number} with ${_change_status} needs to be fixed
+ fail-hint = ${_change_id} on ${_change_project} and ${_change_branch} needs to be fixed
changes = parentof:${_change_number} project:${_change_project} branch:${_change_branch}
```
@@ -509,7 +538,33 @@
not output anything. This switch is particularly useful in combination
with the **\-\-@PLUGIN@\-\-preview** switch.
-**\-\-@PLUGIN@\-\-task\-\-evaluation-time**
+**\-\-@PLUGIN@\-\-include-paths**
+
+This switch will show the absolute path of each task. This is meant for
+debugging when tasks are spread out in different files. A task path includes
+task name, type (indicating one of root, task, tasks-factory),
+[task factory](#tasks_factory) name (if it is generated by one), file name,
+project, and branch the file belongs to. Additionally, if a task is on a user
+ref, it also shows the identity of that user. Only users with `viewTaskPaths`
+capability on the server can view absolute task paths with this switch.
+
+```
+ $ ssh -x -p 29418 example.com gerrit query change:123 \-\-@PLUGIN@\-\-include-paths
+ ...
+ plugins:
+ name: task
+ roots:
+ name: Jenkins Build and Test
+ inProgress: false
+ status: READY
+ path:
+ name: Jenkins Build and Test
+ project: All-Projects.git
+ branch: refs/meta/config
+ file: task.config
+```
+
+**\-\-@PLUGIN@\-\-evaluation-time**
This switch is meant as a debug switch to evaluate task performance. This
switch outputs an elapsed time value on every task indicating how much time
@@ -533,6 +588,13 @@
status: PASS
```
+**\-\-@PLUGIN@\-\-only**
+
+This switch can be used to only evaluate tasks under a certain root when tasks
+from other roots are unwanted. For example, a CI system may not be interested
+in evaluating tasks for another CI system. The switch can be provided multiple
+times.
+
Examples
--------
See [task_states](test/task_states.html) for a comprehensive list of examples
diff --git a/src/main/resources/Documentation/task_expression.md b/src/main/resources/Documentation/task_expression.md
new file mode 100644
index 0000000..c084a39
--- /dev/null
+++ b/src/main/resources/Documentation/task_expression.md
@@ -0,0 +1,220 @@
+<a id="task_expression"/>
+Task Expression
+--------------
+
+The tasks in subtask and preload-task can be defined using a Task Expression.
+Each task expression can contain multiple tasks (all can be optional). Tasks
+from other files and refs can be referenced using [Task Reference](#task_reference).
+
+```
+TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ]
+```
+
+To define a task that may not exist and that will not cause the task referencing
+it to be INVALID, follow the task name with pipe (`|`) character. This feature
+is particularly useful when a property is used in the task name.
+
+```
+ preload-task = Optional task ${_name} |
+```
+
+To define an alternate task to load when an optional task does not exist,
+list the alternate task name after the pipe (`|`) character. This feature
+may be chained together as many times as needed.
+
+```
+ subtask = Optional Subtask ${_name} |
+ Backup Optional Subtask ${_name} Backup |
+ Default Subtask # Must exist if the above two don't!
+```
+
+<a id="task_reference"/>
+Task Reference
+---------
+
+Tasks reference can be a simple task name when the defined task is intended to be in
+the same file, tasks from other files and refs can also be referenced by syntax explained
+below.
+
+```
+ TASK_REFERENCE = [
+ [ // TASK_FILE_PATH ]
+ [ @USERNAME [ TASK_FILE_PATH ] ] |
+ [ %GROUP_NAME [ TASK_FILE_PATH ] ] |
+ [ %%GROUP_UUID [ TASK_FILE_PATH ] ] |
+ [ TASK_FILE_PATH ]
+ ] '^' TASK_NAME
+```
+
+To reference a task from root task.config (top level task.config file of a repository)
+on the current ref, prefix the task name with `^`.
+
+Example:
+
+task/.../<any>.config
+```
+ ...
+ preload-task = ^Task in root task config
+ ...
+```
+
+task.config
+```
+ ...
+ [task "Task in root task config"]
+ ...
+```
+
+To provide an absolute reference to a task under the `task` folder, provide the subpath starting
+from `task` directory with a leading `/` followed by a `^` and then task name.
+
+Example:
+
+task.config
+```
+ ...
+ subtask = /foo/bar/baz.config^Absolute Example Task
+ ...
+```
+
+task/foo/bar/baz.config
+```
+ ...
+ [task "Absolute Example Task"]
+ ...
+```
+
+Similarly, to provide reference to tasks which are in a subdirectory of the file containing the
+current task avoid the leading `/`.
+
+Example:
+
+task/foo/file.config
+```
+ ...
+ subtask = bar/baz.config^Relative Example Task
+ ...
+```
+
+task/foo/bar/baz.config
+```
+ ...
+ [task "Relative Example Task"]
+ ...
+```
+
+Relative tasks specified in a root task.config would look for a file path under the task directory.
+
+Example:
+
+task.config
+```
+ ...
+ subtask = foo/bar.config^Relative from Root Example Task
+ ...
+```
+
+task/foo/bar.config
+```
+ ...
+ [task "Relative from Root Example Task"]
+ ...
+```
+
+To reference a task from a specific user ref (All-Users.git:refs/users/<user>), specify the
+username with `@`.
+
+when referencing from user refs, to get task from top level task.config on a user ref use
+`@<username>^<task_name>` and to get any task under the task directory use the relative
+path, like: `@<username>/<relative path from task dir>^<task_name>`. It doesn't matter which
+project, ref and file one is referencing from while using this syntax.
+
+Example:
+Assumption: Account id of user_a is 1000000
+
+All-Users:refs/users/00/1000000:task.config
+```
+ ...
+ [task "top level task"]
+ ...
+```
+
+All-Users:refs/users/00/1000000:/task/dir/common.config
+```
+ ...
+ [task "common task"]
+ ...
+```
+
+All-Projects:refs/meta/config:/task.config
+```
+ ...
+ preload-task = @user_a_username^top level task
+ preload-task = @user_a_username/dir/common.config^common task
+ ...
+```
+
+To reference a task from root task.config on the All-Projects.git, prefix the task name with `//^`
+and to reference a task from task dir on the All-Projects.git, use
+`//<relative path from task dir>^<task_name>`. It doesn't matter which project, ref and file one
+is referencing from while using this syntax.
+
+Example:
+
+All-Projects:refs/meta/config:task.config
+```
+ ...
+ [task "root task"]
+ ...
+```
+
+All-Projects:refs/meta/config:/task/dir/sample.config
+
+```
+ ...
+ [task "sample task"]
+ ...
+```
+
+All-Users:refs/users/00/1000000:task.config
+```
+ ...
+ preload-task = //dir/sample.config^sample task
+ preload-task = //^root task
+ ...
+```
+
+To reference a task from a specific group ref (All-Users.git:refs/groups/<sharded-group-uuid>),
+specify the group name with `%` or group uuid with `%%`.
+
+When referencing from group refs, to get task from top level task.config on a group ref use
+`%<group_name>^<task_name>` or `%%<group_uuid>^<task_name>` and to get any task under the
+task directory use the relative path,
+like: `%<group_name>/<relative path from task dir>^<task_name>` or
+`%%<group_uuid>/<relative path from task dir>^<task_name>`.
+It doesn't matter which project, ref and file one is referencing from while using this syntax.
+
+Example:
+Assumption: Group uuid of group_a is 720269095421a08a24889e29d092df1839a7a706
+
+All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:task.config
+```
+ ...
+ [task "top level task"]
+ ...
+```
+
+All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:/task/dir/common.config
+```
+ ...
+ [task "common task"]
+ ...
+```
+
+All-Projects:refs/meta/config:/task.config
+```
+ ...
+ preload-task = %group_a^top level task
+ preload-task = %%720269095421a08a24889e29d092df1839a7a706/dir/common.config^common task
+ ...
+```
diff --git a/src/main/resources/Documentation/test/paths.md b/src/main/resources/Documentation/test/paths.md
new file mode 100644
index 0000000..8f43cb0
--- /dev/null
+++ b/src/main/resources/Documentation/test/paths.md
@@ -0,0 +1,208 @@
+`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[root "Root Task PATHS"]
+ subtask = subtask pass
+
+[task "subtask pass"]
+ applicable = is:open
+ pass = is:open
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Task PATHS",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "Root Task PATHS",
+ "project" : "All-Projects",
+ "type" : "root"
+ },
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "subtask pass",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "subtask pass",
+ "project" : "All-Projects",
+ "type" : "task"
+ },
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root other FILE"]
+ applicable = is:open
+ subtasks-file = common.config
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root other FILE",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "Root other FILE",
+ "project" : "All-Projects",
+ "type" : "root"
+ },
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config PASS",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task/common.config",
+ "name" : "file task/common.config PASS",
+ "project" : "All-Projects",
+ "type" : "task"
+ },
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config FAIL",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task/common.config",
+ "name" : "file task/common.config FAIL",
+ "project" : "All-Projects",
+ "type" : "task"
+ },
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root tasks-factory"]
+ subtasks-factory = tasks-factory example
+
+[tasks-factory "tasks-factory example"]
+ names-factory = names-factory example list
+
+[names-factory "names-factory example list"]
+ type = static
+ name = my a task
+ name = my b task
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root tasks-factory",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "Root tasks-factory",
+ "project" : "All-Projects",
+ "type" : "root"
+ },
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "my a task",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "my a task",
+ "project" : "All-Projects",
+ "tasksFactory" : "tasks-factory example",
+ "type" : "tasks-factory"
+ },
+ "status" : "INVALID"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "my b task",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "my b task",
+ "project" : "All-Projects",
+ "tasksFactory" : "tasks-factory example",
+ "type" : "tasks-factory"
+ },
+ "status" : "INVALID"
+ }
+ ]
+}
+
+[root "Root other PROJECT"]
+ subtasks-external = user ref
+
+[external "user ref"]
+ user = testuser
+ file = common.config
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root other PROJECT",
+ "path" : {
+ "ref" : "refs/meta/config",
+ "file" : "task.config",
+ "name" : "Root other PROJECT",
+ "project" : "All-Projects",
+ "type" : "root"
+ },
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config PASS",
+ "path" : {
+ "ref" : "{testuser_user_ref}",
+ "file" : "task/common.config",
+ "name" : "file task/common.config PASS",
+ "project" : "All-Users",
+ "user" : "testuser",
+ "type" : "task"
+ },
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config FAIL",
+ "path" : {
+ "ref" : "{testuser_user_ref}",
+ "file" : "task/common.config",
+ "name" : "file task/common.config FAIL",
+ "project" : "All-Users",
+ "user" : "testuser",
+ "type" : "task"
+ },
+ "status" : "FAIL"
+ }
+ ]
+}
+```
+`task.config` file in project `All-Projects` on ref `refs/meta/config`.
+
+```
+[root "Root Capability Error"]
+ applicable = is:open
+ pass = true
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Capability Error",
+ "path" : {
+ "error" : "Can't perform operation, need viewTaskPaths capability"
+ },
+ "status" : "PASS"
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/preview.md b/src/main/resources/Documentation/test/preview.md
index e53b7d8..df25a6e 100644
--- a/src/main/resources/Documentation/test/preview.md
+++ b/src/main/resources/Documentation/test/preview.md
@@ -1,3 +1,4 @@
+```
[root "INVALIDS Preview"]
subtasks-file = invalids.config
@@ -102,6 +103,22 @@
]
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Blank",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "Bad APPLICABLE query",
+ "status" : "INVALID"
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -122,9 +139,13 @@
"status" : "INVALID" # Only Test Suite: invalid
},
{
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
"applicable" : true,
"hasPass" : false,
- "name" : "Looping",
+ "name" : "task (tasks-factory missing)",
"status" : "WAITING",
"subTasks" : [
{
@@ -134,13 +155,21 @@
]
},
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory static INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
},
{
"applicable" : true,
"hasPass" : false,
- "name" : "task (tasks-factory missing)",
+ "name" : "task (tasks-factory change INVALID)",
"status" : "WAITING",
"subTasks" : [
{
@@ -176,6 +205,18 @@
{
"applicable" : true,
"hasPass" : false,
+ "name" : "task (names-factory name Blank)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
"name" : "task (names-factory duplicate)",
"status" : "WAITING",
"subTasks" : [
@@ -226,38 +267,6 @@
"status" : "INVALID"
}
]
- },
- {
- "applicable" : true,
- "hasPass" : false,
- "name" : "task (tasks-factory changes loop)",
- "status" : "WAITING",
- "subTasks" : [
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change1_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change2_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- }
- ]
}
]
}
@@ -312,16 +321,28 @@
"status" : "WAITING",
"subTasks" : [
{
- "applicable" : true,
- "hasPass" : true,
- "name" : "userfile task/special.config PASS",
- "status" : "PASS"
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "userfile task/special.config PASS", # Only Test Suite: secret
+ "status" : "PASS" # Only Test Suite: secret
+ "name" : "UNKNOWN", # Only Test Suite: !secret
+ "status" : "UNKNOWN" # Only Test Suite: !secret
},
{
- "applicable" : true,
- "hasPass" : true,
- "name" : "userfile task/special.config FAIL",
- "status" : "FAIL"
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "userfile task/special.config FAIL", # Only Test Suite: secret
+ "status" : "FAIL" # Only Test Suite: secret
+ "name" : "UNKNOWN", # Only Test Suite: !secret
+ "status" : "UNKNOWN" # Only Test Suite: !secret
+ },
+ {
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "file task/common.config Preload PASS", # Only Test Suite: secret
+ "status" : "PASS" # Only Test Suite: secret
+ "name" : "UNKNOWN", # Only Test Suite: !secret
+ "status" : "UNKNOWN" # Only Test Suite: !secret
}
]
}
@@ -442,6 +463,22 @@
]
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Blank",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "Bad APPLICABLE query",
+ "status" : "INVALID"
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -462,9 +499,13 @@
"status" : "INVALID" # Only Test Suite: invalid
},
{
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
"applicable" : true,
"hasPass" : false,
- "name" : "Looping",
+ "name" : "task (tasks-factory missing)",
"status" : "WAITING",
"subTasks" : [
{
@@ -474,13 +515,21 @@
]
},
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory static INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
},
{
"applicable" : true,
"hasPass" : false,
- "name" : "task (tasks-factory missing)",
+ "name" : "task (tasks-factory change INVALID)",
"status" : "WAITING",
"subTasks" : [
{
@@ -516,6 +565,18 @@
{
"applicable" : true,
"hasPass" : false,
+ "name" : "task (names-factory name Blank)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
"name" : "task (names-factory duplicate)",
"status" : "WAITING",
"subTasks" : [
@@ -566,38 +627,7 @@
"status" : "INVALID"
}
]
- },
- {
- "applicable" : true,
- "hasPass" : false,
- "name" : "task (tasks-factory changes loop)",
- "status" : "WAITING",
- "subTasks" : [
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change1_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change2_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- }
- ]
}
]
}
+```
diff --git a/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
new file mode 100644
index 0000000..3d1d7cc
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview a new root, original root with subtasks-external pointing to secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root with SECRET external"]
+ applicable = is:open
+ subtasks-external = SECRET
+
+ [external "SECRET"]
+ user = {secret_user}
+ file = secret.config
++
++[root "Root Preview Simple"]
++ subtask = simple task
+
++[task "simple task"]
++ applicable = is:open
++ pass = True
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root with SECRET external",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+}
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Preview Simple",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "simple task",
+ "status" : "PASS"
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
new file mode 100644
index 0000000..d192050
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview a non-secret user ref with subtasks-external pointing to secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Root for NON-SECRET external Preview with SECRET external"]
+ applicable = "is:open"
+ pass = True
+ subtasks-external = NON-SECRET
+
+[external "NON-SECRET"]
+ user = {non_secret_user}
+ file = sample.config
+```
+
+file: `All-Users:{non_secret_user_ref}:task/sample.config`
+```
+ [task "NON-SECRET task"]
+ applicable = is:open
+ pass = Fail
++ subtasks-external = SECRET
+
++[external "SECRET"]
++ user = {secret_user}
++ file = secret.config
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root for NON-SECRET external Preview with SECRET external",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "NON-SECRET task",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
new file mode 100644
index 0000000..a0f11eb
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
@@ -0,0 +1,47 @@
+# --task-preview non-root file with subtask pointing root task
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Points to subFile task with rootFile task preview"]
+ applicable = is:open
+ pass = True
+ subtask = foo/bar/baz.config^Preview pointing to rootFile task
+
+[task "Task in rootFile"]
+ applicable = is:open
+ pass = True
+```
+
+file: `All-Projects.git:refs/meta/config:task/foo/bar/baz.config`
+```
+ [task "Preview pointing to rootFile task"]
+ applicable = is:open
+ pass = Fail
++ subtask = ^Task in rootFile
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Points to subFile task with rootFile task preview",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Preview pointing to rootFile task",
+ "status" : "READY",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Task in rootFile",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
new file mode 100644
index 0000000..c0add08
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@
+# --task-preview root file with subtasks-external pointing to a non-secret user ref with subtasks-external pointing to a secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview NON-SECRET external with SECRET external"]
+ applicable = "is:open"
+ pass = True
++ subtasks-external = NON-SECRET with SECRET External
+
++[external "NON-SECRET with SECRET External"]
++ user = {non_secret_user}
++ file = secret_external.config
+```
+
+file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET external"]
+ applicable = is:open
+ pass = True
+ subtasks-external = SECRET external
+
+[external "SECRET external"]
+ user = {secret_user}
+ file = secret.config
+```
+
+file: `All-Users:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preview NON-SECRET external with SECRET external",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "NON-SECRET with SECRET external",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
new file mode 100644
index 0000000..4d81b12
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
@@ -0,0 +1,40 @@
+# --task-preview root file with subtasks-external pointing to secret user ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external"]
+ applicable = is:open
+ pass = True
++ subtasks-external = SECRET external
+
++[external "SECRET external"]
++ user = {secret_user}
++ file = secret.config
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET Task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preview SECRET external",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET Task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
new file mode 100644
index 0000000..504d5a8
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@
+# --task-preview root file with subtask pointing to a non-secret group ref with subtask pointing to a secret group ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview NON-SECRET group subtask with SECRET group subtask"]
+ applicable = "is:open"
+ pass = True
++ subtask = %{non_secret_group_name}/secret_external.config^NON-SECRET with SECRET subtask
+```
+
+file: `All-Users.git:refs/groups/{sharded_non_secret_group_uuid}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET subtask"]
+ applicable = is:open
+ pass = True
+ subtask = %{secret_group_name}/secret.config^SECRET task
+```
+
+file: `All-Users:refs/groups/{sharded_secret_group_uuid}:task/secret.config`
+```
+[task "SECRET task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preview NON-SECRET group subtask with SECRET group subtask",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "NON-SECRET with SECRET subtask",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+ }
+ ]
+}
+```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
new file mode 100644
index 0000000..9a1932c
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@
+# --task-preview root file with subtask pointing to secret group ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external group"]
+ applicable = is:open
+ pass = True
++ subtask = %{secret_group_name}/secret.config^SECRET Task
+```
+
+file: `All-Users.git:refs/groups/{sharded_secret_group_uuid}:task/secret.config`
+```
+[task "SECRET Task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preview SECRET external group",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET Task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+}
+```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
new file mode 100644
index 0000000..fad2f1d
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@
+# --task-preview root file with subtask pointing to a non-secret user ref with subtask pointing to a secret user ref.
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview NON-SECRET subtask with SECRET subtask"]
+ applicable = "is:open"
+ pass = True
++ subtask = @{non_secret_user}/secret_external.config^NON-SECRET with SECRET subtask
+```
+
+file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config`
+```
+[task "NON-SECRET with SECRET subtask"]
+ applicable = is:open
+ pass = True
+ subtask = @{secret_user}/secret.config^SECRET task
+```
+
+file: `All-Users:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preview NON-SECRET subtask with SECRET subtask",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "NON-SECRET with SECRET subtask",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
new file mode 100644
index 0000000..d2b3349
--- /dev/null
+++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@
+# --task-preview root file with subtask pointing to secret user ref
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+ [root "Root Preview SECRET external"]
+ applicable = is:open
+ pass = True
++ subtask = @{secret_user}/secret.config^SECRET Task
+```
+
+file: `All-Users.git:{secret_user_ref}:task/secret.config`
+```
+[task "SECRET Task"]
+ applicable = is:open
+ pass = Fail
+```
+
+json:
+```
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preview SECRET external",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN", # Only Test Suite: non-secret
+ "status" : "UNKNOWN" # Only Test Suite: non-secret
+ "applicable" : true, # Only Test Suite: secret
+ "hasPass" : true, # Only Test Suite: secret
+ "name" : "SECRET Task", # Only Test Suite: secret
+ "status" : "READY" # Only Test Suite: secret
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md
index ea09541..7a6a8d0 100644
--- a/src/main/resources/Documentation/test/task_states.md
+++ b/src/main/resources/Documentation/test/task_states.md
@@ -17,6 +17,7 @@
The config below is expected to be in the `task.config` file in project
`All-Projects` on ref `refs/meta/config`.
+file: `All-Projects:refs/meta/config:task.config`
```
[root "Root N/A"]
applicable = is:closed # Assumes test query is "is:open"
@@ -55,12 +56,12 @@
[root "Root PASS"]
pass = True
-{
- "applicable" : true,
- "hasPass" : true,
- "name" : "Root PASS",
- "status" : "PASS"
-}
+{ # Test Suite: task_only
+ "applicable" : true, # Test Suite: task_only
+ "hasPass" : true, # Test Suite: task_only
+ "name" : "Root PASS", # Test Suite: task_only
+ "status" : "PASS" # Test Suite: task_only
+} # Test Suite: task_only
[root "Root FAIL"]
fail = True
@@ -72,6 +73,26 @@
"status" : "FAIL"
}
+[root "Root PASS SR"]
+ pass = is:true_task
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root PASS SR",
+ "status" : "PASS"
+}
+
+[root "Root FAIL SR"]
+ fail = is:true_task
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root FAIL SR",
+ "status" : "FAIL"
+}
+
[root "Root straight PASS"]
applicable = is:open
pass = is:open
@@ -570,6 +591,12 @@
"hasPass" : true,
"name" : "userfile task/special.config FAIL",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
}
]
}
@@ -597,6 +624,12 @@
"status" : "FAIL"
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
+ },
+ {
"name" : "UNKNOWN",
"status" : "INVALID"
}
@@ -630,6 +663,12 @@
"status" : "FAIL"
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
+ },
+ {
"name" : "UNKNOWN",
"status" : "INVALID"
}
@@ -661,6 +700,12 @@
"hasPass" : true,
"name" : "userfile task/special.config FAIL",
"status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
}
]
}
@@ -724,12 +769,14 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL"
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
"name" : "_change2_number",
"status" : "FAIL"
@@ -765,37 +812,192 @@
"status" : "PASS"
}
-[root "Root Properties"]
- set-root-property = root-value
- subtask = Subtask Properties
+[root "Root Same Name - Different Tasks-Factory"]
+ subtasks-factory = parent tasks-factory Same Name - Different Tasks-Factory
-[task "Subtask Properties"]
- subtask = Subtask Properties Hints
-
-[task "Subtask Properties Hints"]
- set-first-property = first-value
- set-second-property = ${first-property} second-extra ${third-property}
- set-third-property = third-value
+[tasks-factory "parent tasks-factory Same Name - Different Tasks-Factory"]
+ names-factory = parent names-factory Same Name - Different Tasks-Factory
fail = True
- fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property})
+ subtasks-factory = child tasks-factory Same Name - Different Tasks-Factory
+
+[names-factory "parent names-factory Same Name - Different Tasks-Factory"]
+ type = static
+ name = Same Name
+
+[tasks-factory "child tasks-factory Same Name - Different Tasks-Factory"]
+ names-factory = child names-factory Same Name - Different Tasks-Factory
+ fail = False
+
+[names-factory "child names-factory Same Name - Different Tasks-Factory"]
+ type = static
+ name = Same Name
{
"applicable" : true,
"hasPass" : false,
- "name" : "Root Properties",
+ "name" : "Root Same Name - Different Tasks-Factory",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
+ "hasPass" : true,
+ "name" : "Same Name",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Same Name",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Same Name - Different Change"]
+ subtasks-factory = init tasks-factory Same Name - Different Change
+
+[tasks-factory "init tasks-factory Same Name - Different Change"]
+ names-factory = init names-factory Same Name - Different Change
+ subtask = Same Name - Different Change
+
+[names-factory "init names-factory Same Name - Different Change"]
+ type = change
+ changes = change:_change2_number
+
+[task "Same Name - Different Change"]
+ subtasks-factory = tasks-factory Same Name - Different Change
+ pass = False
+ ready-hint = continues on to change _change1_number
+ fail-hint = stops here since we are change _change1_number
+ fail = change:_change1_number
+
+[tasks-factory "tasks-factory Same Name - Different Change"]
+ names-factory = names-factory Same Name - Different Change
+ subtask = Same Name - Different Change
+
+[names-factory "names-factory Same Name - Different Change"]
+ type = change
+ changes = change:_change1_number NOT change:${_change_number}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Same Name - Different Change",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change2_number,
"hasPass" : false,
- "name" : "Subtask Properties",
+ "name" : "_change2_number",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : true,
- "hint" : "root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value)",
- "name" : "Subtask Properties Hints",
+ "name" : "Same Name - Different Change",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : false,
+ "name" : "_change1_number",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "stops here since we are change _change1_number",
+ "name" : "Same Name - Different Change",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Property References"]
+ set-first-property = first-value
+ set-backward-reference = first-[${first-property}]
+ set-forward-reference = last-[${last-property}]
+ set-last-property = last-value
+ fail = True
+ fail-hint = backward-reference(${backward-reference}) forward-reference(${forward-reference})
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "backward-reference(first-[first-value]) forward-reference(last-[last-value])",
+ "name" : "Root Property References",
+ "status" : "FAIL"
+}
+
+[root "Root Deep Property References"]
+ set-first-property = first-value
+ set-direct-reference = first-[${first-property}]
+ set-deep-reference = deep-{${direct-reference}}
+ fail = True
+ fail-hint = deep-reference(${deep-reference})
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "deep-reference(deep-{first-[first-value]})",
+ "name" : "Root Deep Property References",
+ "status" : "FAIL"
+}
+
+[root "Root Properties Referenced Twice"]
+ set-first-property = first-value
+ set-referenced-twice = first-[${first-property}] first-[${first-property}]
+ fail = True
+ fail-hint = first-[${first-property}] referenced-twice(${referenced-twice}) referenced-twice(${referenced-twice})
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-[first-value] referenced-twice(first-[first-value] first-[first-value]) referenced-twice(first-[first-value] first-[first-value])",
+ "name" : "Root Properties Referenced Twice",
+ "status" : "FAIL"
+}
+
+[root "Root Inherited Properties"]
+ set-root-property = root-value
+ subtask = Subtask Parent Inherited Properties
+
+[task "Subtask Parent Inherited Properties"]
+ set-parent-property = parent-value
+ subtask = Subtask Inherited Properties
+
+[task "Subtask Inherited Properties"]
+ set-my-property = my-value
+ fail = True
+ fail-hint = root-property(${root-property}) parent-property(${parent-property}) my-property(${my-property})
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Inherited Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Parent Inherited Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "root-property(root-value) parent-property(parent-value) my-property(my-value)",
+ "name" : "Subtask Inherited Properties",
"status" : "FAIL"
}
]
@@ -803,6 +1005,41 @@
]
}
+[root "Root Inherited Distant Properties"]
+ set-root-property = root-value
+ set-root-change-property = ${_change_number}
+ subtask = Subtask Parent Inherited Distant Properties
+
+[task "Subtask Parent Inherited Distant Properties"]
+ subtask = Subtask Inherited Distant Properties
+
+[task "Subtask Inherited Distant Properties"]
+ fail = True
+ fail-hint = root-property(${root-property}) root-change-property(${root-change-property})
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Inherited Distant Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Parent Inherited Distant Properties",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "root-property(root-value) root-change-property(_change_number)",
+ "name" : "Subtask Inherited Distant Properties",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+}
[root "Root Properties Reset By Subtask"]
set-root-to-reset-by-subtask = reset-my-root-value
@@ -829,6 +1066,46 @@
]
}
+[root "Root Inherited Property References"]
+ set-root-property = root-value
+ subtask = Subtask Parent Inherited Property References
+
+[task "Subtask Parent Inherited Property References"]
+ set-parent-property = parent-value
+ set-parent-inherited-root-reference = root-property(${root-property})
+ subtask = Subtask Inherited Property References
+
+[task "Subtask Inherited Property References"]
+ set-inherited-root-reference = root-[${root-property}]
+ set-inherited-parent-reference = parent-[${parent-property}]
+ set-inherited-root-deep-reference = parent-inherited-root-reference-[${parent-inherited-root-reference}]
+ fail = True
+ fail-hint = inherited-root-reference(${inherited-root-reference}) inherited-parent-reference(${inherited-parent-reference}) inherited-root-deep-reference(${inherited-root-deep-reference})
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Inherited Property References",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Subtask Parent Inherited Property References",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "inherited-root-reference(root-[root-value]) inherited-parent-reference(parent-[parent-value]) inherited-root-deep-reference(parent-inherited-root-reference-[root-property(root-value)])",
+ "name" : "Subtask Inherited Property References",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+}
+
[root "Root Properties Exports"]
export-root-exported = ${_name}
subtask = Subtask Properties Exports
@@ -927,6 +1204,99 @@
]
}
+[root "Root applicable Property"]
+ subtask = Subtask applicable Property
+ subtasks-factory = tasks-factory branch NOT applicable Property
+
+[tasks-factory "tasks-factory branch NOT applicable Property"]
+ names-factory = names-factory branch NOT applicable Property
+ applicable = branch:dev
+ fail = True
+
+[names-factory "names-factory branch NOT applicable Property"]
+ type = static
+ name = NOT Applicable 1
+ name = NOT Applicable 2
+ name = NOT Applicable 3
+
+[task "Subtask applicable Property"]
+ applicable = change:${_change_number}
+ fail = True
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root applicable Property",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask applicable Property",
+ "status" : "FAIL"
+ }, # Only Test Suite: all
+ { # Only Test Suite: all
+ "applicable" : false, # Only Test Suite: all
+ "hasPass" : true, # Only Test Suite: all
+ "name" : "NOT Applicable 1", # Only Test Suite: all
+ "status" : "FAIL" # Only Test Suite: all
+ }, # Only Test Suite: all
+ { # Only Test Suite: all
+ "applicable" : false, # Only Test Suite: all
+ "hasPass" : true, # Only Test Suite: all
+ "name" : "NOT Applicable 2", # Only Test Suite: all
+ "status" : "FAIL" # Only Test Suite: all
+ }, # Only Test Suite: all
+ { # Only Test Suite: all
+ "applicable" : false, # Only Test Suite: all
+ "hasPass" : true, # Only Test Suite: all
+ "name" : "NOT Applicable 3", # Only Test Suite: all
+ "status" : "FAIL" # Only Test Suite: all
+ }
+ ]
+}
+
+[root "Root branch applicable Property"]
+ subtasks-factory = tasks-factory branch applicable Property
+
+[tasks-factory "tasks-factory branch applicable Property"]
+ names-factory = names-factory branch applicable Property
+ applicable = branch:master
+ fail = True
+
+[names-factory "names-factory branch applicable Property"]
+ type = static
+ name = Applicable 1
+ name = Applicable 2
+ name = Applicable 3
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root branch applicable Property",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Applicable 1",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Applicable 2",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Applicable 3",
+ "status" : "FAIL"
+ }
+ ]
+}
+
[root "Root Properties tasks-factory STATIC"]
subtasks-factory = tasks-factory STATIC Properties
@@ -973,7 +1343,7 @@
set-welcome-message = Welcome to the pleasuredome
names-factory = names-factory a change
fail-hint = ${welcome-message} Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic})
- fail = True
+ fail = change:_change1_number
[names-factory "names-factory a change"]
type = change
@@ -987,6 +1357,7 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"hint" : "Welcome to the pleasuredome Name(_change1_number) Change Number(_change1_number) Change Id(_change1_id) Change Project(_change1_project) Change Branch(_change1_branch) Change Status(_change1_status) Change Topic(_change1_topic)",
"name" : "_change1_number",
@@ -994,9 +1365,48 @@
},
{
"applicable" : true,
+ "change" : _change2_number,
"hasPass" : true,
- "hint" : "Welcome to the pleasuredome Name(_change2_number) Change Number(_change2_number) Change Id(_change2_id) Change Project(_change2_project) Change Branch(_change2_branch) Change Status(_change2_status) Change Topic(_change2_topic)",
"name" : "_change2_number",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root tasks-factory _name Property Reference"]
+ subtasks-factory = Properties tasks-factory _name Property Reference
+
+[tasks-factory "Properties tasks-factory _name Property Reference"]
+ set-name-reference = first-property ${_name}
+ fail-hint = ${name-reference}
+ fail = true
+ names-factory = names-factory static list
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root tasks-factory _name Property Reference",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-property my a task",
+ "name" : "my a task",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-property my b task",
+ "name" : "my b task",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "hint" : "first-property my c task",
+ "name" : "my c task",
"status" : "FAIL"
}
]
@@ -1082,12 +1492,14 @@
"subTasks" : [
{
"applicable" : true,
+ "change" : _change_number,
"hasPass" : true,
"name" : "_change_number",
"status" : "FAIL"
},
{
"applicable" : true,
+ "change" : _change1_number,
"hasPass" : true,
"name" : "_change1_number",
"status" : "FAIL"
@@ -1095,50 +1507,39 @@
]
}
-[root "Root Properties Expansion"]
- applicable = status:open
- subtask = Subtask Property Expansion fail-hint
+[root "Root CHANGE constant subtask list CHANGE Properties"]
+ subtasks-factory = tasks-factory Properties CHANGE names-factory CHANGE
-[task "Subtask Property Expansion fail-hint"]
- subtasks-factory = tasks-factory Property Expansion fail-hint
+[tasks-factory "tasks-factory Properties CHANGE names-factory CHANGE"]
+ names-factory = Properties names-factory current CHANGE
+ subtask = Current CHANGE Property
-[tasks-factory "tasks-factory Property Expansion fail-hint"]
- set-first-property = first-property ${_name}
- fail-hint = ${first-property}
- fail = true
- names-factory = names-factory static list
+[task "Current CHANGE Property"]
+ fail = True
+ fail-hint = Current Change: ${_change_number}
+
+[names-factory "Properties names-factory current CHANGE"]
+ type = change
+ changes = change:${_change_number}
{
"applicable" : true,
"hasPass" : false,
- "name" : "Root Properties Expansion",
+ "name" : "Root CHANGE constant subtask list CHANGE Properties",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
+ "change" : _change_number,
"hasPass" : false,
- "name" : "Subtask Property Expansion fail-hint",
+ "name" : "_change_number",
"status" : "WAITING",
"subTasks" : [
{
"applicable" : true,
"hasPass" : true,
- "hint" : "first-property my a task",
- "name" : "my a task",
- "status" : "FAIL"
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "hint" : "first-property my b task",
- "name" : "my b task",
- "status" : "FAIL"
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "hint" : "first-property my c task",
- "name" : "my c task",
+ "hint" : "Current Change: _change_number",
+ "name" : "Current CHANGE Property",
"status" : "FAIL"
}
]
@@ -1176,6 +1577,158 @@
]
}
+[root "Root Properties names-factory Reference"]
+ subtasks-factory = tasks-factory Properties names-factory Reference
+ set-predicate = change:_change1_number
+
+[tasks-factory "tasks-factory Properties names-factory Reference"]
+ names-factory = Properties names-factory Reference
+ fail = True
+
+[names-factory "Properties names-factory Reference"]
+ type = change
+ changes = ${predicate} OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Reference",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root Properties names-factory Deep Reference"]
+ subtasks-factory = tasks-factory Properties names-factory Deep Reference
+ set-predicate-reference = ${predicate}
+ set-predicate = change:_change1_number
+
+[tasks-factory "tasks-factory Properties names-factory Deep Reference"]
+ names-factory = Properties names-factory Deep Reference
+ fail = True
+
+[names-factory "Properties names-factory Deep Reference"]
+ type = change
+ changes = ${predicate-reference} OR change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Deep Reference",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root Properties names-factory Reference Internal"]
+ subtasks-factory = tasks-factory Properties names-factory Reference Internal
+ set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+[tasks-factory "tasks-factory Properties names-factory Reference Internal"]
+ names-factory = Properties names-factory Reference Internal
+ fail = True
+
+[names-factory "Properties names-factory Reference Internal"]
+ type = change
+ changes = change:_change1_number OR ${predicate}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Reference Internal",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+}
+
+[root "Root Properties names-factory Reference Inherited"]
+ subtask = task Properties names-factory Reference Inherited
+ set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch}
+
+[task "task Properties names-factory Reference Inherited"]
+ subtasks-factory = tasks-factory Properties names-factory Reference Inherited
+
+[tasks-factory "tasks-factory Properties names-factory Reference Inherited"]
+ names-factory = Properties names-factory Reference Inherited
+ fail = True
+
+[names-factory "Properties names-factory Reference Inherited"]
+ type = change
+ changes = change:_change1_number OR ${predicate}
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Properties names-factory Reference Inherited",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task Properties names-factory Reference Inherited",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change_number,
+ "hasPass" : true,
+ "name" : "_change_number",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL"
+ }
+ ]
+ }
+ ]
+}
+
[root "Root Preload Preload"]
subtask = Subtask Preload Preload
@@ -1354,10 +1907,17 @@
subtask = Subtask Preload Properties
[task "Subtask Preload Properties"]
- preload-task = Subtask Properties Hints
+ preload-task = Subtask Preload Properties Hints
set-fourth-property = fourth-value
fail-hint = second-property(${second-property}) fourth-property(${fourth-property})
+[task "Subtask Preload Properties Hints"]
+ set-first-property = first-value
+ set-second-property = ${first-property} second-extra ${third-property}
+ set-third-property = third-value
+ fail = True
+ fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property})
+
{
"applicable" : true,
"hasPass" : false,
@@ -1374,13 +1934,834 @@
]
}
+[root "Root Preload tasks-factory"]
+ subtasks-factory = tasks-factory Preload tasks-factory
+
+[tasks-factory "tasks-factory Preload tasks-factory"]
+ names-factory = names-factory static list
+ preload-task = Subtask PASS
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Preload tasks-factory",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my a task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my b task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my c task",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Looping"]
+ subtask = Looping
+
+[task "Looping"]
+ subtask = Looping
+ pass = True
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Looping",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Looping",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "Looping",
+ "status" : "DUPLICATE"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Looping DuplicateKey"]
+ preload-task = DuplicateKey
+
+[task "Looping DuplicateKey"]
+ preload-task = DuplicateKey
+ pass = True
+
+[task "DuplicateKey"]
+ duplicate-key = 1234
+ subtask = Looping DuplicateKey
+
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Looping DuplicateKey",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "Looping DuplicateKey",
+ "status" : "DUPLICATE"
+ }
+ ]
+}
+
+[root "Root changes loop"]
+ subtask = task (tasks-factory changes loop)
+
+[task "task (tasks-factory changes loop)"]
+ subtasks-factory = tasks-factory change loop
+
+[tasks-factory "tasks-factory change loop"]
+ names-factory = names-factory change constant
+ subtask = task (tasks-factory changes loop)
+ fail = True
+
+[names-factory "names-factory change constant"]
+ changes = change:_change1_number OR change:_change2_number
+ type = change
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root changes loop",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "_change1_number",
+ "status" : "DUPLICATE"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : true,
+ "name" : "_change2_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "_change1_number",
+ "status" : "DUPLICATE"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "_change2_number",
+ "status" : "DUPLICATE"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : true,
+ "name" : "_change2_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : true,
+ "name" : "_change1_number",
+ "status" : "FAIL",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory changes loop)",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "change" : _change1_number,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "_change1_number",
+ "status" : "DUPLICATE"
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "_change2_number",
+ "status" : "DUPLICATE"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "change" : _change2_number,
+ "hasPass" : false,
+ "hint" : "Duplicate task is non blocking and empty to break the loop",
+ "name" : "_change2_number",
+ "status" : "DUPLICATE"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Import tasks using absolute syntax"]
+ applicable = is:open
+ subtask = /relative.config^Root Import task from subdir using relative syntax
+ subtask = /dir/common.config^Root Import task from root task.config
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import tasks using absolute syntax",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import task from subdir using relative syntax",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Sample relative task in sub dir",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import task from root task.config",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Import relative tasks from root config"]
+ applicable = is:open
+ subtask = dir/common.config^Root Import task from root task.config
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import relative tasks from root config",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import task from root task.config",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root subtasks-external user ref with Absolute and Relative syntaxes"]
+ subtasks-external = user absolute and relative syntaxes
+
+[external "user absolute and relative syntaxes"]
+ user = testuser
+ file = dir/sample.config
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root subtasks-external user ref with Absolute and Relative syntaxes",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Referencing single task from same user ref",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Relative Task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Relative Task in sub dir",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in user root config file",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Import user tasks"]
+ applicable = is:open
+ subtask = @testuser/foo/bar.config^Absolute Task
+ subtask = @testuser^task in user root config file
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import user tasks",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in user root config file",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Import group tasks"]
+ applicable = is:open
+ subtask = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1
+ subtask = %{non_secret_group_name_without_space}^task in group root config file 1
+ subtask = %{non_secret_group_name_with_space}/foo/bar.config^Absolute Task 3
+ subtask = %{non_secret_group_name_with_space}^task in group root config file 3
+ subtask = %%{non_secret_group_uuid}/foo/bar.config^Absolute Task 2
+ subtask = %%{non_secret_group_uuid}^task in group root config file 2
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Import group tasks",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task 1",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in group root config file 1",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task 3",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in group root config file 3",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task 2",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in group root config file 2",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Reference tasks from All-Projects"]
+ applicable = is:open
+ subtask = //^Subtask PASS
+ subtask = @testuser/dir/relative.config^Import All-Projects root task
+ subtask = @testuser/dir/relative.config^Import All-Projects non-root task
+ subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects root task - groups
+ subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects non-root task - groups
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Reference tasks from All-Projects",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Import All-Projects root task",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Import All-Projects non-root task",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Sample relative task in sub dir",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Import All-Projects root task - groups",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Import All-Projects non-root task - groups",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Sample relative task in sub dir",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Preload from all-projects sub-dir which has preload-task in same file"]
+ preload-task = //dir/common.config^Sample task in sub dir with preload-task from same file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir which has preload-task in same file",
+ "status" : "PASS"
+}
+
+[root "Root Preload from all-projects sub-dir which has preload-task in different file"]
+ preload-task = //dir/common.config^Sample task in sub dir with preload-task from different file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir which has preload-task in different file",
+ "status" : "PASS"
+}
+
+[root "Root Preload from all-projects sub-dir"]
+ preload-task = //dir/common.config^Sample relative task in sub dir
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir",
+ "status" : "PASS"
+}
+
+[root "Root Preload from all-projects sub-dir which has subtask in same file"]
+ preload-task = //dir/common.config^Sample relative task in sub dir with subtask from same file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir which has subtask in same file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Sample relative task in sub dir",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from all-projects sub-dir which has subtask in different file"]
+ preload-task = //dir/common.config^Sample relative task in sub dir with subtask from different file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir which has subtask in different file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Passing task",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from all-projects sub-dir which has subtasks-factory in same file"]
+ preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-factory from same file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir which has subtasks-factory in same file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my a task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my b task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my c task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "my d task",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from all-projects sub-dir which has subtasks-external in same file"]
+ preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-external from same file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from all-projects sub-dir which has subtasks-external in same file",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "userfile task/special.config PASS",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "userfile task/special.config FAIL",
+ "status" : "FAIL"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "file task/common.config Preload PASS",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Referencing single task from same user ref",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Relative Task",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Relative Task in sub dir",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in user root config file",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task",
+ "status" : "PASS"
+ }
+ ]
+ }
+ ]
+}
+
+[root "Root Preload from group ref which has subtasks-file"]
+ preload-task = %{non_secret_group_name_without_space}^Sample task with subtasks-file
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from group ref which has subtasks-file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task 1",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task 2",
+ "status" : "PASS"
+ },
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Absolute Task",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from user ref"]
+ preload-task = @testuser/dir/relative.config^Relative Task
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from user ref",
+ "status" : "PASS"
+}
+
+[root "Root Preload from user ref which has subtask in same file"]
+ preload-task = @testuser/dir/relative.config^Relative Task with subtask
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from user ref which has subtask in same file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Passing task",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from user ref which has subtask in different file"]
+ preload-task = @testuser/dir/relative.config^Import All-Projects root task
+
+{
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "Root Preload from user ref which has subtask in different file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from group ref"]
+ preload-task = %{non_secret_group_name_without_space}^task in group root config file 1
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from group ref",
+ "status" : "PASS"
+}
+
+[root "Root Preload from group ref which has subtask in same file"]
+ preload-task = %{non_secret_group_name_without_space}^task in group root with subtask
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from group ref which has subtask in same file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Passing task",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from group ref which has subtask in different file"]
+ preload-task = %{non_secret_group_name_without_space}^task in group root with subtask from all-projects
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from group ref which has subtask in different file",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask PASS",
+ "status" : "PASS"
+ }
+ ]
+}
+
+[root "Root Preload from group ref which has subtask in different group ref"]
+ preload-task = %{non_secret_group_name_without_space}^task in group root with subtask from another group
+
+{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Root Preload from group ref which has subtask in different group ref",
+ "status" : "PASS",
+ "subTasks" : [
+ {
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "task in group root config file 3",
+ "status" : "PASS"
+ }
+ ]
+}
+
[root "Root INVALID Preload"]
preload-task = missing
-{
- "name" : "UNKNOWN",
- "status" : "INVALID"
-}
+{ # Test Suite: task_only
+ "name" : "UNKNOWN", # Test Suite: task_only
+ "status" : "INVALID" # Test Suite: task_only
+} # Test Suite: task_only
[root "INVALIDS"]
subtasks-file = invalids.config
@@ -1486,6 +2867,23 @@
]
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Blank",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "Bad APPLICABLE query", # Only Test Suite: visible
+ "name" : "UNKNOWN", # Only Test Suite: !visible
+ "status" : "INVALID"
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -1506,9 +2904,13 @@
"status" : "INVALID" # Only Test Suite: !all
},
{
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
"applicable" : true,
"hasPass" : false,
- "name" : "Looping",
+ "name" : "task (tasks-factory missing)",
"status" : "WAITING",
"subTasks" : [
{
@@ -1518,13 +2920,21 @@
]
},
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory static INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
},
{
"applicable" : true,
"hasPass" : false,
- "name" : "task (tasks-factory missing)",
+ "name" : "task (tasks-factory change INVALID)",
"status" : "WAITING",
"subTasks" : [
{
@@ -1560,6 +2970,18 @@
{
"applicable" : true,
"hasPass" : false,
+ "name" : "task (names-factory name Blank)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
"name" : "task (names-factory duplicate)",
"status" : "WAITING",
"subTasks" : [
@@ -1610,38 +3032,6 @@
"status" : "INVALID"
}
]
- },
- {
- "applicable" : true,
- "hasPass" : false,
- "name" : "task (tasks-factory changes loop)",
- "status" : "WAITING",
- "subTasks" : [
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change1_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change2_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- }
- ]
}
]
}
@@ -1773,6 +3163,23 @@
]
},
{
+ "applicable" : true,
+ "hasPass" : true,
+ "name" : "Subtask Blank",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "name" : "Bad APPLICABLE query", # Only Test Suite: visible
+ "name" : "UNKNOWN", # Only Test Suite: !visible
+ "status" : "INVALID"
+ },
+ {
"applicable" : false,
"hasPass" : true,
"name" : "NA Bad PASS query",
@@ -1793,9 +3200,13 @@
"status" : "INVALID" # Only Test Suite: !all
},
{
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ },
+ {
"applicable" : true,
"hasPass" : false,
- "name" : "Looping",
+ "name" : "task (tasks-factory missing)",
"status" : "WAITING",
"subTasks" : [
{
@@ -1805,13 +3216,21 @@
]
},
{
- "name" : "UNKNOWN",
- "status" : "INVALID"
+ "applicable" : true,
+ "hasPass" : false,
+ "name" : "task (tasks-factory static INVALID)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
},
{
"applicable" : true,
"hasPass" : false,
- "name" : "task (tasks-factory missing)",
+ "name" : "task (tasks-factory change INVALID)",
"status" : "WAITING",
"subTasks" : [
{
@@ -1847,6 +3266,18 @@
{
"applicable" : true,
"hasPass" : false,
+ "name" : "task (names-factory name Blank)",
+ "status" : "WAITING",
+ "subTasks" : [
+ {
+ "name" : "UNKNOWN",
+ "status" : "INVALID"
+ }
+ ]
+ },
+ {
+ "applicable" : true,
+ "hasPass" : false,
"name" : "task (names-factory duplicate)",
"status" : "WAITING",
"subTasks" : [
@@ -1897,46 +3328,13 @@
"status" : "INVALID"
}
]
- },
- {
- "applicable" : true,
- "hasPass" : false,
- "name" : "task (tasks-factory changes loop)",
- "status" : "WAITING",
- "subTasks" : [
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change1_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- },
- {
- "applicable" : true,
- "hasPass" : true,
- "name" : "_change2_number",
- "status" : "FAIL",
- "subTasks" : [
- {
- "name" : "UNKNOWN",
- "status" : "INVALID"
- }
- ]
- }
- ]
}
]
}
```
-`task/common.config` file in project `All-Projects` on ref `refs/meta/config`.
-
+file: `All-Projects:refs/meta/config:task/common.config`
```
[task "file task/common.config PASS"]
applicable = is:open
@@ -1947,8 +3345,84 @@
fail = is:open
```
-`task/invalids.config` file in project `All-Projects` on ref `refs/meta/config`.
+file: `All-Projects:refs/meta/config:task/relative.config`
+```
+[task "Root Import task from subdir using relative syntax"]
+ subtask = dir/common.config^Sample relative task in sub dir
+[task "Passing task"]
+ applicable = is:open
+ pass = True
+```
+
+file: `All-Projects:refs/meta/config:task/dir/common.config`
+```
+[task "Sample relative task in sub dir"]
+ applicable = is:open
+ pass = is:open
+
+[task "Sample relative task in sub dir with subtask from same file"]
+ applicable = is:open
+ pass = is:open
+ subtask = Sample relative task in sub dir
+
+[task "Sample relative task in sub dir with subtasks-factory from same file"]
+ applicable = is:open
+ pass = is:open
+ set-my-factory-prop = simple static tasks-factory 2
+ subtasks-factory = simple static tasks-factory 1
+ subtasks-factory = ${my-factory-prop}
+
+[tasks-factory "simple static tasks-factory 1"]
+ names-factory = names-factory static list 1
+ pass = True
+
+[names-factory "names-factory static list 1"]
+ type = static
+ name = my a task
+ name = my b task
+ name = my c task
+
+[tasks-factory "simple static tasks-factory 2"]
+ names-factory = names-factory static list 2
+ pass = True
+
+[names-factory "names-factory static list 2"]
+ type = static
+ name = my d task
+
+[task "Sample relative task in sub dir with subtasks-external from same file"]
+ applicable = is:open
+ pass = is:open
+ set-my-external-prop = user sample config
+ subtasks-external = user special tasks
+ subtasks-external = ${my-external-prop}
+
+[external "user special tasks"]
+ user = testuser
+ file = special.config
+
+[external "user sample config"]
+ user = testuser
+ file = dir/sample.config
+
+[task "Sample relative task in sub dir with subtask from different file"]
+ applicable = is:open
+ pass = is:open
+ subtask = //relative.config^Passing task
+
+[task "Root Import task from root task.config"]
+ applicable = is:open
+ subtask = ^Subtask PASS
+
+[task "Sample task in sub dir with preload-task from same file"]
+ preload-task = Sample relative task in sub dir
+
+[task "Sample task in sub dir with preload-task from different file"]
+ preload-task = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1
+```
+
+file: `All-Projects:refs/meta/config:task/invalids.config`
```
[task "No PASS criteria"]
fail-hint = Invalid without Pass criteria and without subtasks
@@ -1977,6 +3451,14 @@
[task "Subtask Optional"]
subtask = MISSING | MISSING
+[task "Subtask Blank"]
+ pass = True
+ subtask =
+
+[task "Bad APPLICABLE query"]
+ applicable = bad:query
+ fail = True
+
[task "NA Bad PASS query"]
applicable = NOT is:open # Assumes test query is "is:open"
fail = True
@@ -1992,23 +3474,30 @@
fail = True
in-progress = has:bad
-[task "Looping"]
- subtask = Looping
-
[task "Looping Properties"]
set-A = ${B}
set-B = ${A}
+ fail-hint = ${A}
fail = True
[task "task (tasks-factory missing)"]
subtasks-factory = missing
+[task "task (tasks-factory static INVALID)"]
+ subtasks-factory = tasks-factory (preload-task missing)
+
+[task "task (tasks-factory change INVALID)"]
+ subtasks-factory = tasks-factory change (preload-task missing)
+
[task "task (names-factory type missing)"]
subtasks-factory = tasks-factory (names-factory type missing)
[task "task (names-factory type INVALID)"]
subtasks-factory = tasks-factory (names-factory type INVALID)
+[task "task (names-factory name Blank)"]
+ subtasks-factory = tasks-factory (names-factory name Blank)
+
[task "task (names-factory duplicate)"]
subtasks-factory = tasks-factory (names-factory duplicate)
@@ -2021,16 +3510,27 @@
[task "task (names-factory changes invalid)"]
subtasks-factory = tasks-factory change (names-factory changes invalid)
-[task "task (tasks-factory changes loop)"]
- subtasks-factory = tasks-factory change loop
-
[tasks-factory "tasks-factory (names-factory type missing)"]
names-factory = names-factory (type missing)
fail = True
+[tasks-factory "tasks-factory (preload-task missing)"]
+ names-factory = names-factory static
+ fail = True
+ preload-task = missing
+
+[tasks-factory "tasks-factory change (preload-task missing)"]
+ names-factory = names-factory change list
+ fail = True
+ preload-task = missing
+
[tasks-factory "tasks-factory (names-factory type INVALID)"]
names-factory = name-factory (type INVALID)
+[tasks-factory "tasks-factory (names-factory name Blank)"]
+ names-factory = names-factory (name Blank)
+ fail = True
+
[tasks-factory "tasks-factory (names-factory duplicate)"]
names-factory = names-factory duplicate
fail = True
@@ -2047,10 +3547,13 @@
names-factory = names-factory change list (changes invalid)
fail = True
-[tasks-factory "tasks-factory change loop"]
- names-factory = names-factory change constant
- subtask = task (tasks-factory changes loop)
- fail = True
+[names-factory "names-factory static"]
+ name = task A
+ type = static
+
+[names-factory "names-factory change list"]
+ changes = change:_change1_number OR change:_change2_number
+ type = change
[names-factory "names-factory (type missing)"]
name = no type test
@@ -2062,6 +3565,10 @@
name = invalid type test
type = invalid
+[names-factory "names-factory (name Blank)"]
+ name =
+ type = static
+
[names-factory "names-factory duplicate"]
name = duplicate
name = duplicate
@@ -2071,17 +3578,12 @@
type = change
[names-factory "names-factory change list (changes invalid)"]
- change = change:invalidChange
- type = change
-
-[names-factory "names-factory change constant"]
- changes = change:_change1_number OR change:_change2_number
+ changes = change:invalidChange
type = change
```
-`task/special.config` file in project `All-Users` on ref `refs/users/self`.
-
+file: `All-Users:refs/meta/config:task/special.config`
```
[task "userfile task/special.config PASS"]
applicable = is:open
@@ -2090,4 +3592,154 @@
[task "userfile task/special.config FAIL"]
applicable = is:open
fail = is:open
+
+[task "file task/common.config Preload PASS"]
+ preload-task = userfile task/special.config PASS
+```
+
+file: `All-Users:refs/users/self:task/common.config`
+```
+[task "file task/common.config PASS"]
+ applicable = is:open
+ pass = is:open
+
+[task "file task/common.config FAIL"]
+ applicable = is:open
+ fail = is:open
+```
+
+file: `All-Users:refs/users/self:task.config`
+```
+[task "task in user root config file"]
+ applicable = is:open
+ pass = is:open
+```
+
+file: `All-Users:refs/users/self:task/dir/sample.config`
+```
+[task "Referencing single task from same user ref"]
+ applicable = is:open
+ pass = is:open
+ subtask = relative.config^Relative Task
+ subtask = sub_dir/relative.config^Relative Task in sub dir
+ subtask = ^task in user root config file
+ subtask = /foo/bar.config^Absolute Task
+```
+
+file: `All-Users:refs/users/self:task/dir/relative.config`
+```
+[task "Relative Task"]
+ applicable = is:open
+ pass = is:open
+
+[task "Relative Task with subtask"]
+ applicable = is:open
+ pass = is:open
+ subtask = Passing task
+
+[task "Passing task"]
+ applicable = is:open
+ pass = True
+
+[task "Import All-Projects root task"]
+ applicable = is:open
+ subtask = //^Subtask PASS
+
+[task "Import All-Projects non-root task"]
+ applicable = is:open
+ subtask = //dir/common.config^Sample relative task in sub dir
+```
+
+file: `All-Users:refs/users/self:task/dir/sub_dir/relative.config`
+```
+[task "Relative Task in sub dir"]
+ applicable = is:open
+ pass = is:open
+```
+
+file: `All-Users:refs/users/self:task/foo/bar.config`
+```
+[task "Absolute Task"]
+ applicable = is:open
+ pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/dir/relative.config`
+```
+[task "Import All-Projects root task - groups"]
+ applicable = is:open
+ subtask = //^Subtask PASS
+
+[task "Import All-Projects non-root task - groups"]
+ applicable = is:open
+ subtask = //dir/common.config^Sample relative task in sub dir
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo/bar.config`
+```
+[task "Absolute Task 1"]
+ applicable = is:open
+ pass = is:open
+
+[task "Absolute Task 2"]
+ applicable = is:open
+ pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo.config`
+```
+[task "Absolute Task"]
+ applicable = is:open
+ pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task.config`
+```
+[task "task in group root config file 1"]
+ applicable = is:open
+ pass = is:open
+
+[task "task in group root with subtask"]
+ applicable = is:open
+ pass = is:open
+ subtask = Passing task
+
+[task "Passing task"]
+ applicable = is:open
+ pass = True
+
+[task "task in group root with subtask from all-projects"]
+ applicable = is:open
+ pass = is:open
+ subtask = //^Subtask PASS
+
+[task "task in group root with subtask from another group"]
+ applicable = is:open
+ pass = is:open
+ subtask = %{non_secret_group_name_with_space}^task in group root config file 3
+
+[task "task in group root config file 2"]
+ applicable = is:open
+ pass = is:open
+
+[task "Sample task with subtasks-file"]
+ applicable = is:open
+ pass = is:open
+ set-my-prop = foo.config
+ subtasks-file = foo/bar.config
+ subtasks-file = ${my-prop}
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task/foo/bar.config`
+```
+[task "Absolute Task 3"]
+ applicable = is:open
+ pass = is:open
+```
+
+file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task.config`
+```
+[task "task in group root config file 3"]
+ applicable = is:open
+ pass = is:open
```
diff --git a/src/test/java/com/google/gerrit/common/BooleanTableTest.java b/src/test/java/com/google/gerrit/common/BooleanTableTest.java
new file mode 100644
index 0000000..746a0e9
--- /dev/null
+++ b/src/test/java/com/google/gerrit/common/BooleanTableTest.java
@@ -0,0 +1,81 @@
+// 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.common;
+
+import junit.framework.TestCase;
+
+public class BooleanTableTest extends TestCase {
+
+ public void testNulls() {
+ BooleanTable<String, String> cbt = new BooleanTable<>();
+ assertNull(cbt.get("r1", "c1"));
+ assertNull(cbt.get("r0", "c0"));
+
+ cbt.put("r1", "c0", true);
+ assertNull(cbt.get("r1", "c1"));
+ assertNull(cbt.get("r0", "c0"));
+
+ cbt.put("r0", "c1", true);
+ assertNull(cbt.get("r1", "c1"));
+ assertNull(cbt.get("r0", "c0"));
+ }
+
+ public void testRowColumn() {
+ BooleanTable<String, String> cbt = new BooleanTable<>();
+ cbt.put("r1", "c1", true);
+ cbt.put("r2", "c2", false);
+ assertTrue(cbt.get("r1", "c1"));
+ assertNull(cbt.get("r1", "c2"));
+ assertNull(cbt.get("r2", "c1"));
+ assertFalse(cbt.get("r2", "c2"));
+ }
+
+ public void testRowColumnOverride() {
+ BooleanTable<String, String> cbt = new BooleanTable<>();
+ cbt.put("r1", "c1", true);
+ assertTrue(cbt.get("r1", "c1"));
+
+ cbt.put("r1", "c1", false);
+ assertFalse(cbt.get("r1", "c1"));
+ }
+
+ public void testRepeatedColumns() {
+ BooleanTable<String, String> cbt = new BooleanTable<>();
+ cbt.put("r1", "c1", true);
+ cbt.put("r2", "c1", false);
+ assertTrue(cbt.get("r1", "c1"));
+ assertFalse(cbt.get("r2", "c1"));
+ }
+
+ public void testRepeatedRows() {
+ BooleanTable<String, String> cbt = new BooleanTable<>();
+ cbt.put("r1", "c1", true);
+ cbt.put("r1", "c2", false);
+ assertTrue(cbt.get("r1", "c1"));
+ assertFalse(cbt.get("r1", "c2"));
+ }
+
+ public void testRepeatedRowsAndColumns() {
+ BooleanTable<String, String> cbt = new BooleanTable<>();
+ cbt.put("r1", "c1", true);
+ cbt.put("r2", "c1", false);
+ cbt.put("r1", "c2", true);
+ cbt.put("r2", "c2", false);
+ assertTrue(cbt.get("r1", "c1"));
+ assertFalse(cbt.get("r2", "c1"));
+ assertTrue(cbt.get("r1", "c2"));
+ assertFalse(cbt.get("r2", "c2"));
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
new file mode 100644
index 0000000..8ea0009
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -0,0 +1,260 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import junit.framework.TestCase;
+import org.mockito.Mockito;
+
+/*
+ * <ul>
+ * <li><code> "simple" -> ("simple") required</code>
+ * <li><code> "world | peace" -> ("world", "peace") required</code>
+ * <li><code> "shadenfreud |" -> ("shadenfreud") optional</code>
+ * <li><code> "foo | bar |" -> ("foo", "bar") optional</code>
+ * <li><code> "/foo^bar | baz |" -> ("task/foo^bar", "baz") optional</code>
+ * <li><code> "foo^bar | baz |" -> ("cur_dir/foo^bar", "baz") optional</code>
+ * <li><code> "^bar | baz |" -> ("task.config^bar", "baz") optional</code>
+ * </ul>
+ */
+public class TaskExpressionTest extends TestCase {
+ public static String SIMPLE = "simple";
+ public static String WORLD = "world";
+ public static String PEACE = "peace";
+ public static FileKey file = createFileKey("foo", "bar", "baz");
+
+ public static TaskKey SIMPLE_TASK = TaskKey.create(file, SIMPLE);
+ public static TaskKey WORLD_TASK = TaskKey.create(file, WORLD);
+ public static TaskKey PEACE_TASK = TaskKey.create(file, PEACE);
+
+ public static String SAMPLE = "sample";
+ public static String TASK_CFG = "task.config";
+ public static String SIMPLE_CFG = "task/simple.config";
+ public static String PEACE_CFG = "task/peace.config";
+ public static String WORLD_PEACE_CFG = "task/world/peace.config";
+ public static String REL_WORLD_PEACE_CFG = "world/peace.config";
+ public static String ABS_PEACE_CFG = "/peace.config";
+
+ public void testBlank() {
+ TaskExpression exp = getTaskExpression("");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testRequiredSingleName() {
+ TaskExpression exp = getTaskExpression(SIMPLE);
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testOptionalSingleName() {
+ TaskExpression exp = getTaskExpression(SIMPLE + "|");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertFalse(it.hasNext());
+ }
+
+ public void testRequiredTwoNames() {
+ TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE);
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), WORLD_TASK);
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), PEACE_TASK);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testOptionalTwoNames() {
+ TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE + "|");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), WORLD_TASK);
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), PEACE_TASK);
+ assertFalse(it.hasNext());
+ }
+
+ public void testBlankSpaces() {
+ TaskExpression exp = getTaskExpression(" ");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testRequiredSingleNameLeadingSpaces() {
+ TaskExpression exp = getTaskExpression(" " + SIMPLE);
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testRequiredSingleNameTrailingSpaces() {
+ TaskExpression exp = getTaskExpression(SIMPLE + " ");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testOptionalSingleNameLeadingSpaces() {
+ TaskExpression exp = getTaskExpression(" " + SIMPLE + "|");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertFalse(it.hasNext());
+ }
+
+ public void testOptionalSingleNameTrailingSpaces() {
+ TaskExpression exp = getTaskExpression(SIMPLE + "| ");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertFalse(it.hasNext());
+ }
+
+ public void testOptionalSingleNameMiddleSpaces() {
+ TaskExpression exp = getTaskExpression(SIMPLE + " |");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), SIMPLE_TASK);
+ assertFalse(it.hasNext());
+ }
+
+ public void testRequiredTwoNamesMiddleSpaces() {
+ TaskExpression exp = getTaskExpression(WORLD + " | " + PEACE);
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), WORLD_TASK);
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), PEACE_TASK);
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testAbsoluteAndRelativeReference() {
+ TaskExpression exp =
+ getTaskExpression(
+ createFileKey(SIMPLE_CFG),
+ REL_WORLD_PEACE_CFG + "^" + SAMPLE + " | " + ABS_PEACE_CFG + "^" + SAMPLE);
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), TaskKey.create(createFileKey(WORLD_PEACE_CFG), SAMPLE));
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), TaskKey.create(createFileKey(PEACE_CFG), SAMPLE));
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testAbsoluteAndRelativeReferenceFromRoot() {
+ TaskExpression exp =
+ getTaskExpression(
+ createFileKey(TASK_CFG),
+ REL_WORLD_PEACE_CFG + "^" + SAMPLE + " | " + ABS_PEACE_CFG + "^" + SAMPLE);
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), TaskKey.create(createFileKey(WORLD_PEACE_CFG), SAMPLE));
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), TaskKey.create(createFileKey(PEACE_CFG), SAMPLE));
+ assertTrue(it.hasNext());
+ assertNoSuchElementException(it);
+ }
+
+ public void testReferenceFromRoot() {
+ TaskExpression exp = getTaskExpression(createFileKey(SIMPLE_CFG), " ^" + SAMPLE + " | ");
+ Iterator<TaskKey> it = exp.iterator();
+ assertTrue(it.hasNext());
+ assertEquals(it.next(), TaskKey.create(createFileKey(TASK_CFG), SAMPLE));
+ assertNoSuchElementException(it);
+ }
+
+ public void testDifferentKeyOnDifferentFile() {
+ TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+ TaskExpression otherExp = getTaskExpression(createFileKey("foo", "bar", "other"), SIMPLE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ public void testDifferentKeyOnDifferentBranch() {
+ TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+ TaskExpression otherExp = getTaskExpression(createFileKey("foo", "other", "baz"), SIMPLE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ public void testDifferentKeyOnDifferentProject() {
+ TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE);
+ TaskExpression otherExp = getTaskExpression(createFileKey("other", "bar", "baz"), SIMPLE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ public void testDifferentKeyOnDifferentExpression() {
+ TaskExpression exp = getTaskExpression(SIMPLE);
+ TaskExpression otherExp = getTaskExpression(PEACE);
+ assertFalse(exp.key.equals(otherExp.key));
+ }
+
+ protected static void assertNoSuchElementException(Iterator<TaskKey> it) {
+ try {
+ it.next();
+ assertTrue(false);
+ } catch (NoSuchElementException e) {
+ assertTrue(true);
+ }
+ }
+
+ protected TaskExpression getTaskExpression(String expression) {
+ return getTaskExpression(file, expression);
+ }
+
+ protected TaskExpression getTaskExpression(FileKey file, String expression) {
+ AccountCache accountCache = Mockito.mock(AccountCache.class);
+ GroupCache groupCache = Mockito.mock(GroupCache.class);
+ TaskReference.Factory factory = Mockito.mock(TaskReference.Factory.class);
+ Mockito.when(factory.create(Mockito.any(), Mockito.any()))
+ .thenAnswer(
+ invocation ->
+ new TaskReference(
+ new TaskKey.Builder(
+ (FileKey) invocation.getArguments()[0],
+ new AllProjectsName("All-Projects"),
+ new AllUsersName("All-Users"),
+ accountCache,
+ groupCache),
+ (String) invocation.getArguments()[1]));
+ return new TaskExpression(factory, file, expression);
+ }
+
+ protected static FileKey createFileKey(String file) {
+ return createFileKey("foo", "bar", file);
+ }
+
+ protected static FileKey createFileKey(String project, String branch, String file) {
+ return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
new file mode 100644
index 0000000..e08240d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -0,0 +1,299 @@
+// 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.googlesource.gerrit.plugins.task;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import java.sql.Timestamp;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import junit.framework.TestCase;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class TaskReferenceTest extends TestCase {
+ private static final String ALL_USERS = "All-Users";
+ private static final String ALL_PROJECTS = "All-Projects";
+ public static String SIMPLE = "simple";
+ public static String ROOT = "task.config";
+ public static String COMMON = "task/common.config";
+ public static String SUB_COMMON = "task/dir/common.config";
+ public static FileKey ROOT_CFG = createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, ROOT);
+ public static FileKey COMMON_CFG = createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, COMMON);
+ public static FileKey SUB_COMMON_CFG =
+ createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, SUB_COMMON);
+
+ public static FileKey SAMPLE_PROJ_CFG = createFileKey("foo", RefNames.REFS_CONFIG, ROOT);
+
+ public static final String TEST_USER = "testuser";
+ public static final int TEST_USER_ID = 100000;
+ public static final Account TEST_USER_ACCOUNT =
+ Account.builder(Account.id(TEST_USER_ID), new Timestamp(0L)).build();
+ public static final String TEST_USER_REF =
+ "refs/users/" + String.format("%02d", TEST_USER_ID % 100) + "/" + TEST_USER_ID;
+ public static final FileKey TEST_USER_ROOT_CFG = createFileKey(ALL_USERS, TEST_USER_REF, ROOT);
+ public static final FileKey TEST_USER_COMMON_CFG =
+ createFileKey(ALL_USERS, TEST_USER_REF, COMMON);
+
+ public static final AccountGroup.NameKey TEST_GROUP1_NAME = AccountGroup.nameKey("testgroup");
+ public static final AccountGroup.NameKey TEST_GROUP2_NAME = AccountGroup.nameKey("test group");
+ public static final String TEST_GROUP1_UUID = "526d2bf882635380fbd3b72320464e342fc14533";
+ public static final String TEST_GROUP2_UUID = "62aa5663241f31b9483bad66132bd5d416b2bef9";
+ public static final InternalGroup TEST_GROUP1 =
+ buildTestGroup(AccountGroup.id(1), TEST_GROUP1_NAME, AccountGroup.uuid(TEST_GROUP1_UUID));
+ public static final InternalGroup TEST_GROUP2 =
+ buildTestGroup(AccountGroup.id(2), TEST_GROUP2_NAME, AccountGroup.uuid(TEST_GROUP2_UUID));
+ public static final String TEST_GROUP1_REF =
+ "refs/groups/" + TEST_GROUP1_UUID.substring(0, 2) + "/" + TEST_GROUP1_UUID;
+ public static final String TEST_GROUP2_REF =
+ "refs/groups/" + TEST_GROUP2_UUID.substring(0, 2) + "/" + TEST_GROUP2_UUID;
+ public static final FileKey TEST_GROUP1_ROOT_CFG =
+ createFileKey(ALL_USERS, TEST_GROUP1_REF, ROOT);
+ public static final FileKey TEST_GROUP1_COMMON_CFG =
+ createFileKey(ALL_USERS, TEST_GROUP1_REF, COMMON);
+ public static final FileKey TEST_GROUP2_ROOT_CFG =
+ createFileKey(ALL_USERS, TEST_GROUP2_REF, ROOT);
+ public static final FileKey TEST_GROUP2_COMMON_CFG =
+ createFileKey(ALL_USERS, TEST_GROUP2_REF, COMMON);
+
+ static InternalGroup buildTestGroup(
+ AccountGroup.Id id, AccountGroup.NameKey nameKey, AccountGroup.UUID uuid) {
+ return InternalGroup.builder()
+ .setGroupUUID(uuid)
+ .setNameKey(nameKey)
+ .setOwnerGroupUUID(uuid)
+ .setId(id)
+ .setVisibleToAll(true)
+ .setCreatedOn(new Timestamp(0L))
+ .setMembers(ImmutableSet.of())
+ .setSubgroups(ImmutableSet.of())
+ .build();
+ }
+
+ @Test
+ public void testReferencingTaskFromSameFile() throws Exception {
+ assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, SIMPLE));
+ }
+
+ @Test
+ public void testReferencingTaskFromRootConfig() throws Exception {
+ String reference = "^" + SIMPLE;
+ assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRelativeTaskFromRootConfig() throws Exception {
+ String reference = " dir/common.config^" + SIMPLE;
+ assertEquals(createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingAbsoluteTaskFromRootConfig() throws Exception {
+ String reference = " /common.config^" + SIMPLE;
+ assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRelativeDirTask() throws Exception {
+ String reference = " dir/common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRelativeFileTask() throws Exception {
+ String reference = "common.config^" + SIMPLE;
+ assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingAbsoluteTask() throws Exception {
+ String reference = " /common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRootAllProjectsTask() throws Exception {
+ String reference = "//^" + SIMPLE;
+ assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SAMPLE_PROJ_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingAllProjectsTask() throws Exception {
+ String reference = "//common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SAMPLE_PROJ_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRootUserTask() throws Exception {
+ String reference = "@" + TEST_USER + "^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_USER_ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingUserTaskDir() throws Exception {
+ String reference = "@" + TEST_USER + "/common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_USER_COMMON_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testMultipleUpchars() throws Exception {
+ String reference = " ^ /common.config^" + SIMPLE;
+ assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testEmptyReference() throws Exception {
+ String empty = "";
+ assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, empty));
+ }
+
+ @Test
+ public void testReferencingRootGroupNameWithoutSpaceTask() throws Exception {
+ String reference = "%" + TEST_GROUP1_NAME.get() + "^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRootGroupNameWithSpaceTask() throws Exception {
+ String reference = "%" + TEST_GROUP2_NAME.get() + "^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_GROUP2_ROOT_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingGroupNameWithoutSpaceTaskDir() throws Exception {
+ String reference = "%" + TEST_GROUP1_NAME.get() + "/common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingUnknownGroupName() throws Exception {
+ String reference = "%unknown^" + SIMPLE;
+ assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingEmptyGroupName() throws Exception {
+ String reference = "%^" + SIMPLE;
+ assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingGroupNameWithSpaceTaskDir() throws Exception {
+ String reference = "%" + TEST_GROUP2_NAME.get() + "/common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_GROUP2_COMMON_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingRootGroupUUIDTask() throws Exception {
+ String reference = "%%" + TEST_GROUP1_UUID + "^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingGroupUUIDTaskDir() throws Exception {
+ String reference = "%%" + TEST_GROUP1_UUID + "/common.config^" + SIMPLE;
+ assertEquals(
+ createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE),
+ getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingUnknownGroupUUID() throws Exception {
+ String reference = "%%a8341ade45d83e867c24a2d37f47b410cfdbea6d^" + SIMPLE;
+ assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ @Test
+ public void testReferencingEmptyGroupUUID() throws Exception {
+ String reference = "%%^" + SIMPLE;
+ assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference));
+ }
+
+ protected static TaskKey getTaskFromReference(FileKey file, String expression) {
+ AccountCache accountCache = Mockito.mock(AccountCache.class);
+ GroupCache groupCache = Mockito.mock(GroupCache.class);
+ Mockito.when(accountCache.getByUsername(TEST_USER))
+ .thenReturn(Optional.of(AccountState.forAccount(TEST_USER_ACCOUNT)));
+ Mockito.when(groupCache.get(TEST_GROUP1_NAME)).thenReturn(Optional.of(TEST_GROUP1));
+ Mockito.when(groupCache.get(TEST_GROUP2_NAME)).thenReturn(Optional.of(TEST_GROUP2));
+ Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP1_UUID)))
+ .thenReturn(Optional.of(TEST_GROUP1));
+ Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP2_UUID)))
+ .thenReturn(Optional.of(TEST_GROUP2));
+
+ try {
+ return new TaskReference(
+ new TaskKey.Builder(
+ file,
+ new AllProjectsName(ALL_PROJECTS),
+ new AllUsersName(ALL_USERS),
+ accountCache,
+ groupCache),
+ expression)
+ .getTaskKey();
+ } catch (ConfigInvalidException e) {
+ throw new NoSuchElementException();
+ }
+ }
+
+ protected static TaskKey createTaskKey(FileKey file, String task) {
+ return TaskKey.create(file, task);
+ }
+
+ protected static FileKey createFileKey(String project, String branch, String file) {
+ return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file);
+ }
+
+ protected static void assertNoSuchElementException(Executable f) throws Exception {
+ try {
+ f.run();
+ assertTrue(false);
+ } catch (NoSuchElementException e) {
+ assertTrue(true);
+ }
+ }
+
+ @FunctionalInterface
+ interface Executable {
+ void run() throws Exception;
+ }
+}
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh
index 5b7e161..30e2ece 100755
--- a/test/check_task_statuses.sh
+++ b/test/check_task_statuses.sh
@@ -15,277 +15,82 @@
# limitations under the License.
# Usage:
-# All-Projects.git - must have 'Push' rights on refs/meta/config
+# 1. All-Projects.git - must have 'Push' rights on refs/meta/config for test user
+# 2. All-Projects.git - must have 'viewTaskPaths' capability for test user
+# 3. All-Projects.git - must have 'accessDatabase' capability for test user
+# 4. All-Users.git - must have 'push' rights on refs/users/* for test user
+# 5. All-Users.git - must have 'push' rights on refs/users/${shardeduserid} for Registered Users
+# 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users
+# 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users
+# 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users
+# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit
+# site's git directory (as group ref updates are done directly to git).
-# ---- TEST RESULTS ----
-result() { # test [error_message]
- local result=$?
- if [ $result -eq 0 ] ; then
- echo "PASSED - $1 test"
- else
- echo "*** FAILED *** - $1 test"
- RESULT=$result
- [ $# -gt 1 ] && echo "$2"
- fi
-}
+create_configs_from_task_states() {
+ for marker in $(md_file_markers "$DOC_STATES") ; do
+ local project_name="$(md_file_marker_project "$marker")"
+ local project_dir="$OUT/$project_name"
+ local file="$(md_file_marker_file "$marker")"
+ local ref="$(md_file_marker_ref "$marker")"
-# output must match expected to pass
-result_out() { # test expected actual
- local name=$1 expected=$2 actual=$3
-
- [ "$expected" = "$actual" ]
- result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
-}
-
-result_root() { # group root expected_file actual_file
- local name="$1 - $(echo "$2" | sed -es'/Root //')"
- result_out "$name" "$(get_root "$2" < "$3")" "$(get_root "$2" < "$4")"
-}
-
-# -------- Git Config
-
-config() { git config -f "$CONFIG" "$@" ; } # [args]...
-config_section_keys() { # section > keys ...
- # handlers.handler-filter filter.sh -> handler-filter
- config -l --name-only |\
- grep "^$1\." | \
- sed -es"/^$1\.//;s/\..*$//" |\
- awk '$0 != prev ; {prev = $0}'
-}
-
-# -------- Pre JSON --------
-#
-# pre_json is a "templated json" used in the test docs to express test results. It looks
-# like json but has some extra comments to express when a certain output should be used.
-# These comments look like: "# Only Test Suite: <suite>"
-#
-
-remove_suite() { # suite < pre_json > json
- grep -v "# Only Test Suite: $1" | \
- sed -e's/# Only Test Suite:.*$//; s/ *$//'
-}
-
-remove_not_suite() { remove_suite !"$1" ; } # suite < pre_json > json
-
-# -------- Test Doc Format --------
-#
-# Test Doc Format has intermixed git config task definitions with json roots. This
-# makes it easy to define tests close to their outputs. Be aware that all of the
-# config will get consolidated into a single file, so non root config will be shared
-# amongst all the roots.
-#
-
-# Sample Test Doc for 2 roots:
-#
-# [root "Root PASS"]
-# pass = True
-#
-# {
-# "applicable" : true,
-# "hasPass" : true,
-# "name" : "Root PASS",
-# "status" : "PASS"
-# }
-#
-# [root "Root FAIL"]
-# fail = True
-#
-# {
-# <other root>
-# }
-
-# Strip the json from Test Doc formatted text. For the sample above, the output would be:
-#
-# [root "Root PASS"]
-# pass = True
-#
-# [root "Root FAIL"]
-# fail = True
-# ...
-#
-testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config
-
-# Strip the git config from Test Doc formatted text. For the sample above, the output would be:
-#
-# { "plugins" : [
-# { "name" : "task",
-# "roots" : [
-# {
-# "applicable" : true,
-# "hasPass" : true,
-# "name" : "Root PASS",
-# "status" : "PASS"
-# },
-# {
-# <other root>
-# },
-# ...
-# }
-testdoc_2_pjson() { # < testdoc_format > pjson_task_roots
- awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \
- /^\{/ { open=1 }; \
- open && end { print "}," ; end=0 }; \
- /^\}/ { open=0 ; end=1 }; \
- open; \
- END { print "}]}]}" }'
-}
-
-# ---- JSON PARSING ----
-
-json_pp() { # < json > json
- python -c "import sys, json; \
- print json.dumps(json.loads(sys.stdin.read()), indent=3, \
- separators=(',', ' : '), sort_keys=True)"
-}
-
-json_val_by() { # json index|'key' > value
- echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]"
-}
-json_val_by_key() { json_val_by "$1" "'$2'" ; } # json key > value
-
-# --------
-gssh() { ssh -x -p "$PORT" "$SERVER" gerrit "$@" ; } # cmd [args]...
-
-q() { "$@" > /dev/null 2>&1 ; } # cmd [args...] # quiet a command
-
-gen_change_id() { echo "I$(uuidgen | openssl dgst -sha1 -binary | xxd -p)"; } # > change_id
-
-commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
-
-err() { echo "ERROR: $1" >&2 ; exit 1 ; }
-
-# Run a test setup command quietly, exit on failure
-q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...]
-
-ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data
-
-set_change() { # change_json
- { CHANGE=("$(json_val_by_key "$1" number)" \
- "$(json_val_by_key "$1" id)" \
- "$(json_val_by_key "$1" project)" \
- "refs/heads/$(json_val_by_key "$1" branch)" \
- "$(json_val_by_key "$1" status)" \
- "$(json_val_by_key "$1" topic)") ; } 2> /dev/null
-}
-
-# change_token change_number change_id project branch status topic < templated_txt > change_txt
-replace_change_properties() {
- sed -e "s|_change$1_number|$2|g" \
- -e "s|_change$1_id|$3|g" \
- -e "s|_change$1_project|$4|g" \
- -e "s|_change$1_branch|$5|g" \
- -e "s|_change$1_status|$6|g" \
- -e "s|_change$1_topic|$7|g"
-}
-
-replace_default_changes() {
- replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
-}
-
-replace_user() { # < text_with_testuser > text_with_$USER
- sed -e"s/testuser/$USER/"
-}
-
-strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
-strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
-
-get_root() { # root < task_plugin_ouptut > root_json
- python -c "if True: # NOP to start indent
- import sys, json
-
- roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
- for root in roots:
- if 'name' in root.keys() and root['name']=='$1':
- print json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)"
-}
-
-example() { # example_num
- echo "$DOC_STATES" | awk '/```/{Q++;E=(Q+1)/2};E=='"$1" | grep -v '```' | replace_user
-}
-
-get_change_num() { # < gerrit_push_response > changenum
- local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
- echo "${url##*\/}" | tr -d -c '[:digit:]'
-}
-
-install_changeid_hook() { # repo
- local hook=$(git rev-parse --git-dir)/hooks/commit-msg
- scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
- chmod +x "$hook"
-}
-
-setup_repo() { # repo remote ref [--initial-commit]
- local repo=$1 remote=$2 ref=$3 init=$4
- git init "$repo"
- (
- cd "$repo"
- install_changeid_hook "$repo"
- git fetch "$remote" "$ref"
- if ! git checkout FETCH_HEAD ; then
- if [ "$init" = "--initial-commit" ] ; then
- git commit --allow-empty -a -m "Initial Commit"
- fi
+ if [[ "$ref" == refs/groups/* ]] ; then
+ project_dir="$project_dir-${ref:(-7)}}"
+ q_setup setup_repo "$project_dir" "$REMOTE_USERS" "$ref"
fi
- )
-}
-update_repo() { # repo remote ref
- local repo=$1 remote=$2 ref=$3
- (
- cd "$repo"
- git add .
- git commit -m 'Testing task plugin'
- git push "$remote" HEAD:"$ref"
- )
-}
+ mkdir -p "$(dirname "$project_dir/$file")"
+ md_marker_content "$DOC_STATES" "$marker" | replace_user \
+ | testdoc_2_cfg > "$project_dir/$file"
-create_repo_change() { # repo remote ref [change_id] > change_num
- local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
- (
- q cd "$repo"
- date > file
- q git add .
- [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
- q git commit -m "$msg"
- git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
- )
-}
-
-query_plugins() { # query
- gssh query "$@" --format json | head -1 | python -c "import sys, json; \
- plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
- print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
-}
-
-test_tasks() { # name expected_file task_args...
- local name=$1 expected=$2 ; shift 2
- local output=$STATUSES.$name out root
-
- query_plugins "$@" > "$output"
- echo "$ROOTS" | while read root ; do
- result_root "$name" "$root" "$expected" "$output"
+ if [[ "$ref" == refs/groups/* ]] ; then
+ # As support for pushing a change to group refs [1] is not yet in any release,
+ # push the update behind gerrit's back, directly into git.
+ # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614
+ q_setup update_repo "$project_dir" "$GERRIT_GIT_DIR/All-Users.git" "$ref"
+ fi
done
- out=$(diff "$expected" "$output" | head -15)
- [ -z "$out" ]
- result "$name - Full Test Suite" "$out"
}
-test_generated() { # name task_args...
+test_2generated() { # name task_args...
local name=$1 ; shift
- test_tasks "$name" "$EXPECTED.$name" "$@"
+ local out=$(query "$@")
+ results_suite "$name" "$EXPECTED.$name" "$(echo "$out" | change_plugins 1)"
+ results_suite "$name 2nd change" "$EXPECTED.$name"2 "$(echo "$out" | change_plugins 2)"
}
-test_file() { # name task_args...
+test_generated() { # name [-l query_user] task_args...
local name=$1 ; shift
- local expected=$MYDIR/$name output=$STATUSES.$name
+ query "$@" | change_plugins 1 > "$ACTUAL.$name"
+ results_suite "$name" "$EXPECTED.$name" "$( < "$ACTUAL.$name")"
+}
- query "$@" | awk '$0==" \"plugins\" : [",$0==" ],"' > "$output"
- out=$(diff "$expected" "$output")
- result "$name" "$out"
+usage() { # [error_message]
+ cat <<-EOF
+Usage:
+ "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user>
+ --untrusted-user <untrusted user>
+
+ --help|-h help text
+ --server|-s gerrit host
+ --non-secret-user user who don't have permission
+ to view other user refs.
+ --untrusted-user user who doesn't have permission
+ to view refs/meta/config ref on All-Projects repo
+ --non-secret-group-without-space non-secret group name without spaces
+ --non-secret-group-with-space non-secret group name with spaces
+EOF
+
+ [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
+ exit 0
}
readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+MYPROG=$(basename -- "$0")
+
+source "$MYDIR/lib/lib_helper.sh"
+source "$MYDIR/lib/lib_md.sh"
+
DOCS=$MYDIR/.././src/main/resources/Documentation/test
OUT=$MYDIR/../target/tests
@@ -295,20 +100,36 @@
USERS=$OUT/All-Users
USER_TASKS=$USERS/task
-DOC_PREVIEW=$DOCS/preview.md
EXPECTED=$OUT/expected
-STATUSES=$OUT/statuses
+ACTUAL=$OUT/actual
ROOT_CFG=$ALL/task.config
-COMMON_CFG=$ALL_TASKS/common.config
-INVALIDS_CFG=$ALL_TASKS/invalids.config
-USER_SPECIAL_CFG=$USER_TASKS/special.config
# --- Args ----
-SERVER=$1
-[ -z "$SERVER" ] && { echo "You must specify a server" ; exit ; }
+
+while (( "$#" )) ; do
+ case "$1" in
+ --help|-h) usage ;;
+ --server|-s) shift ; SERVER=$1 ;;
+ --non-secret-user) shift ; NON_SECRET_USER=$1 ;;
+ --untrusted-user) shift ; UNTRUSTED_USER=$1 ;;
+ --non-secret-group-without-space) shift ; GROUP_NAME_WITHOUT_SPACE=$1 ;;
+ --non-secret-group-with-space) shift ; GROUP_NAME_WITH_SPACE=$1 ;;
+ *) usage "invalid argument $1" ;;
+ esac
+ shift
+done
+
+[ -z "$SERVER" ] && usage "You must specify --server"
+[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
+[ -z "$UNTRUSTED_USER" ] && usage "You must specify --untrusted-user"
+[ -z "$GROUP_NAME_WITHOUT_SPACE" ] && usage "You must specify --non-secret-group-without-space"
+[ -z "$GROUP_NAME_WITH_SPACE" ] && usage "You must specify --non-secret-group-with-space"
+[ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set"
+
PORT=29418
+HTTP_PORT=8080
PROJECT=test
BRANCH=master
REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects
@@ -320,6 +141,16 @@
CONFIG=$ROOT_CFG
+declare -A USER_REFS
+USER_REFS["{testuser_user_ref}"]="$(get_user_ref "$USER")"
+
+declare -A GROUP_EXPANDED_BY_PLACEHOLDER
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_without_space}"]="$GROUP_NAME_WITHOUT_SPACE"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_with_space}"]="$GROUP_NAME_WITH_SPACE"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_uuid}"]="$(get_group_uuid "$GROUP_NAME_WITHOUT_SPACE")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_without_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITHOUT_SPACE")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_with_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITH_SPACE")"
+
mkdir -p "$OUT" "$ALL_TASKS" "$USER_TASKS"
q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
@@ -329,12 +160,12 @@
changes=$(gssh query "status:open limit:2" --format json)
set_change "$(echo "$changes" | awk 'NR==1')" ; CHANGE1=("${CHANGE[@]}")
set_change "$(echo "$changes" | awk 'NR==2')" ; CHANGE2=("${CHANGE[@]}")
-DOC_STATES=$(replace_default_changes < "$DOCS/task_states.md")
-example 2 | replace_user | testdoc_2_cfg > "$ROOT_CFG"
-example 3 > "$COMMON_CFG"
-example 4 > "$INVALIDS_CFG"
-example 5 > "$USER_SPECIAL_CFG"
+DOC_STATES=$(replace_tokens < "$DOCS/task_states.md")
+DOC_PREVIEW=$(replace_tokens < "$DOCS/preview.md")
+DOC_PATHS=$(replace_tokens < "$DOCS/paths.md")
+
+create_configs_from_task_states
ROOTS=$(config_section_keys "root") || err "Invalid ROOTS"
@@ -342,9 +173,12 @@
q_setup update_repo "$USERS" "$REMOTE_USERS" "$REF_USERS"
change3_id=$(gen_change_id)
+change4_id=$(gen_change_id)
+change4_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change4_id")
change3_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change3_id")
-all_pjson=$(example 2 | testdoc_2_pjson | \
+ex2_pjson=$(example "$DOC_STATES" 2 | testdoc_2_pjson)
+all_pjson=$(echo "$ex2_pjson" | \
replace_change_properties \
"" \
"$change3_number" \
@@ -354,36 +188,82 @@
"NEW" \
"")
-no_all_json=$(echo "$all_pjson" | remove_suite all)
+all2_pjson=$(echo "$ex2_pjson" | \
+ replace_change_properties \
+ "" \
+ "$change4_number" \
+ "$change4_id" \
+ "$PROJECT" \
+ "refs\/heads\/$BRANCH" \
+ "NEW" \
+ "")
-echo "$no_all_json" | strip_non_applicable | \
+no_all_visible_json=$(echo "$all_pjson" | remove_suites "all" "!visible")
+no_all_no_visible_json=$(echo "$all_pjson" | remove_suites "all" "visible")
+no_all_visible2_json=$(echo "$all2_pjson" | remove_suites "all" "!visible")
+no_all_no_visible2_json=$(echo "$all2_pjson" | remove_suites "all" "visible")
+
+echo "$no_all_visible_json" | strip_non_applicable | \
grep -v "\"applicable\" :" > "$EXPECTED".applicable
-echo "$all_pjson" | remove_not_suite all | ensure json_pp > "$EXPECTED".all
+echo "$no_all_no_visible_json" | strip_non_applicable | \
+ grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility
-echo "$no_all_json" | strip_non_invalid > "$EXPECTED".invalid
+echo "$no_all_visible2_json" | strip_non_applicable | \
+ grep -v "\"applicable\" :" > "$EXPECTED".applicable2
+
+echo "$no_all_no_visible2_json" | strip_non_applicable | \
+ grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility2
+
+echo "$all_pjson" | remove_suites "!all" "!visible" | ensure json_pp > "$EXPECTED".all
+
+echo "$no_all_visible_json" | strip_non_invalid > "$EXPECTED".invalid
strip_non_invalid < "$EXPECTED".applicable > "$EXPECTED".invalid-applicable
-preview_pjson=$(testdoc_2_pjson < "$DOC_PREVIEW" | replace_default_changes)
-echo "$preview_pjson" | remove_suite invalid | ensure json_pp > "$EXPECTED".preview
-echo "$preview_pjson" | remove_not_suite invalid | strip_non_invalid > "$EXPECTED".preview-invalid
+preview_pjson=$(example "$DOC_PREVIEW" 1 | testdoc_2_pjson)
+echo "$preview_pjson" | remove_suites "invalid" "secret" | \
+ ensure json_pp > "$EXPECTED".preview-non-secret
+echo "$preview_pjson" | remove_suites "invalid" "!secret" | \
+ ensure json_pp > "$EXPECTED".preview-admin
+echo "$preview_pjson" | remove_suites "secret" "!invalid" | \
+ strip_non_invalid > "$EXPECTED".preview-invalid
-testdoc_2_cfg < "$DOC_PREVIEW" | replace_user > "$ROOT_CFG"
+example "$DOC_PREVIEW" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG"
cnum=$(create_repo_change "$ALL" "$REMOTE_ALL" "$REF_ALL")
PREVIEW_ROOTS=$(config_section_keys "root")
RESULT=0
-query="change:$change3_number status:open"
-test_generated applicable --task--applicable "$query"
+query="(change:$change3_number OR change:$change4_number) status:open"
+test_2generated applicable --task--applicable "$query"
+test_2generated applicable-visibility -l "$UNTRUSTED_USER" --task--applicable "$query"
test_generated all --task--all "$query"
test_generated invalid --task--invalid "$query"
test_generated invalid-applicable --task--applicable --task--invalid "$query"
ROOTS=$PREVIEW_ROOTS
-test_generated preview --task--preview "$cnum,1" --task--all "$query"
+test_generated preview-admin --task--preview "$cnum,1" --task--all "$query"
+test_generated preview-non-secret -l "$NON_SECRET_USER" --task--preview "$cnum,1" --task--all "$query"
test_generated preview-invalid --task--preview "$cnum,1" --task--invalid "$query"
+
+example "$DOC_STATES" 2 | keep_suites "task_only" | testdoc_2_pjson | \
+ ensure json_pp > "$EXPECTED".task-roots-filter
+test_generated task-roots-filter --task--all --task--only "Root\ PASS" "$query"
+
+example "$DOC_PATHS" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG"
+q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+ROOTS=$(config_section_keys "root")
+example "$DOC_PATHS" 1 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths
+
+test_generated task-paths --task--all --task--include-paths "$query"
+
+example "$DOC_PATHS" 2 | testdoc_2_cfg > "$ROOT_CFG"
+q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL"
+ROOTS=$(config_section_keys "root")
+example "$DOC_PATHS" 2 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths.non-secret
+test_generated task-paths.non-secret -l "$NON_SECRET_USER" --task--all --task--include-paths "$query"
+
exit $RESULT
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh
new file mode 100755
index 0000000..1dabec4
--- /dev/null
+++ b/test/check_task_visibility.sh
@@ -0,0 +1,248 @@
+#!/usr/bin/env bash
+#
+# 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.
+
+# Usage:
+# 1. All-Projects.git - must have 'Push' rights on refs/meta/config for test user
+# 2. All-Projects.git - must have 'viewTaskPaths' capability for test user
+# 3. All-Projects.git - must have 'accessDatabase' capability for test user
+# 4. All-Users.git - must have 'push' rights on refs/users/* for test user
+# 5. All-Users.git - must have 'push' rights on refs/users/${shardeduserid} for Registered Users
+# 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users
+# 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users
+# 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users
+# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit
+# site's git directory (as group ref updates are done directly to git).
+
+readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS
+MYDIR=$(dirname -- "$(readlink -f -- "$0")")
+MYPROG=$(basename -- "$0")
+
+source "$MYDIR/lib/lib_helper.sh"
+source "$MYDIR/lib/lib_md.sh"
+
+# Visibility tests cases are described using a markdown file.
+# Each file has a list of config files specified by file
+# markers. The initial state of task configs is created using
+# them. Only one of the config file has an inline diff. Gerrit
+# change is created by applying that diff to the specified file
+# marker and the expected json is asserted by using that change
+# as an input to the '--task-preview' switch.
+
+# The syntax for inline diff is similar to diff --unified=MAX_INT.
+# All lines start with a leading space and if a specific line is
+# part of diff, we use diff indicators (+/-) instead of a leading
+# space.
+
+# Example input for all diff functions:
+#
+# [root "Root Preview SECRET external"]
+# applicable = is:open
+# pass = True
+# - subtask = Subtask APPLICABLE
+# + subtasks-external = SECRET external
+#
+# +[external "SECRET external"]
+# + user = {secret_user}
+# + file = secret.config
+
+
+# Returns if a config has inline diff or not.
+diff_indicators_present() { # file_content
+ echo "$1" | grep -q "^-\|^+"
+}
+
+# file_content_with_diff_indicators > file_content_with_diff_applied
+# out:
+#[root "Root Preview SECRET external"]
+# applicable = is:open
+# pass = True
+# subtask = Subtask APPLICABLE
+diff_apply() {
+ sed -e '/^-/d' -e 's/^.//'
+}
+
+# file_content_with_diff_indicators > file_content_with_diff_reverted
+# out:
+#[root "Root Preview SECRET external"]
+# applicable = is:open
+# pass = True
+# subtasks-external = SECRET external
+#
+#[external "SECRET external"]
+# user = {secret_user}
+# file = secret.config
+diff_revert() {
+ sed -e '/^+/d' -e 's/^.//'
+}
+
+config_ensure() { # config_file_path
+ q git config --list -f "$1" || err "Invalid config file: $1"
+}
+
+get_remote() { # project > remote_url
+ echo "ssh://$SERVER:$PORT/$(basename "$1")"
+}
+
+# Gets json from the preview doc and creates
+# expected json in workspace to assert later.
+create_expected_json() {
+ local json=$(md_marker_content "$TEST_DOC" "json:")
+
+ echo "$json" | remove_suites "non-secret" | \
+ testdoc_2_pjson | ensure json_pp > "$EXPECTED_SECRET"
+ echo "$json" | remove_suites "secret" | \
+ testdoc_2_pjson | ensure json_pp > "$EXPECTED_NON_SECRET"
+}
+
+test_preview() { # preview_change_number
+ query --task--all --task--preview "$1,1" "change:1" \
+ | change_plugins 1 > "$ACTUAL_SECRET"
+ query -l "$NON_SECRET_USER" --task--all --task--preview "$1,1" "change:1" \
+ | change_plugins 1 > "$ACTUAL_NON_SECRET"
+
+ ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_SECRET")
+ results_suite "Visibility Secret Test" "$EXPECTED_SECRET" "$( < "$ACTUAL_SECRET" )"
+
+ ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_NON_SECRET")
+ results_suite "Visibility Non-Secret Test" "$EXPECTED_NON_SECRET" "$( < "$ACTUAL_NON_SECRET" )"
+}
+
+init_configs() {
+ for marker in $(md_file_markers "$TEST_DOC") ; do
+ local project="$OUT/$(md_file_marker_project "$marker")"
+ local ref="$(md_file_marker_ref "$marker")"
+ local file="$(md_file_marker_file "$marker")"
+ local content="$(md_marker_content "$TEST_DOC" "$marker")"
+ local tip_content
+
+ q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
+ mkdir -p "$(dirname "$project/$file")"
+
+ if diff_indicators_present "$content" ; then
+ CHANGE_FILE_MARKER=$marker
+ CHANGE_CONTENT=$(echo "$content" | diff_apply)
+ tip_content=$(echo "$content" | diff_revert)
+ else
+ tip_content=$content
+ fi
+
+ echo "$tip_content" > "$project/$file"
+ config_ensure "$project/$file"
+ if [[ "$ref" == refs/groups/* ]] ; then
+ # As support for pushing a change to group refs [1] is not yet in any release,
+ # push the update behind gerrit's back, directly into git.
+ # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614
+ q_setup update_repo "$project" "$GERRIT_GIT_DIR/All-Users.git" "$ref"
+ else
+ q_setup update_repo "$project" "$(get_remote "$project")" "$ref"
+ fi
+ done
+}
+
+test_change() {
+ local project="$OUT/$(md_file_marker_project "$CHANGE_FILE_MARKER")"
+ local ref="$(md_file_marker_ref "$CHANGE_FILE_MARKER")"
+ local file="$(md_file_marker_file "$CHANGE_FILE_MARKER")"
+ q_setup setup_repo "$project" "$(get_remote "$project")" "$ref"
+
+ echo "$CHANGE_CONTENT" > "$project/$file"
+ config_ensure "$project/$file"
+ local cnum=$(create_repo_change "$project" "$(get_remote "$project")" "$ref")
+
+ create_expected_json
+ test_preview "$cnum"
+}
+
+usage() { # [error_message]
+ cat <<-EOF
+Usage:
+ "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user>
+
+ --help|-h help text
+ --server|-s gerrit host
+ --non-secret-user user who doesn't have permission
+ to view other user refs.
+ --non-secret-group non-secret group name
+ --secret-group secret group name
+EOF
+
+ [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; }
+ exit 0
+}
+
+while (( "$#" )) ; do
+ case "$1" in
+ --help|-h) usage ;;
+ --server|-s) shift ; SERVER=$1 ;;
+ --non-secret-user) shift ; NON_SECRET_USER=$1 ;;
+ --non-secret-group) shift ; NON_SECRET_GROUP_NAME=$1 ;;
+ --secret-group) shift ; SECRET_GROUP_NAME=$1 ;;
+ *) usage "invalid argument $1" ;;
+ esac
+ shift
+done
+
+[ -z "$SERVER" ] && usage "You must specify --server"
+[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user"
+[ -z "$NON_SECRET_GROUP_NAME" ] && usage "You must specify --non-secret-group"
+[ -z "$SECRET_GROUP_NAME" ] && usage "You must specify --secret-group"
+[ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set"
+
+RESULT=0
+PORT=29418
+HTTP_PORT=8080
+OUT=$MYDIR/../target/preview
+EXPECTED_SECRET="$OUT/expected-secret"
+EXPECTED_NON_SECRET="$OUT/expected-non-secret"
+ACTUAL_SECRET="$OUT/actual-secret"
+ACTUAL_NON_SECRET="$OUT/actual-non-secret"
+TEST_DOC_DIR="$MYDIR/../src/main/resources/Documentation/test/task-preview/"
+
+declare -A USERS
+declare -A USER_REFS
+USERS["{secret_user}"]="$USER"
+USER_REFS["{secret_user_ref}"]="$(get_user_ref "$USER")"
+USERS["{non_secret_user}"]="$NON_SECRET_USER"
+USER_REFS["{non_secret_user_ref}"]="$(get_user_ref "$NON_SECRET_USER")"
+
+declare -A GROUP_EXPANDED_BY_PLACEHOLDER
+GROUP_EXPANDED_BY_PLACEHOLDER["{secret_group_name}"]="$SECRET_GROUP_NAME"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_secret_group_uuid}"]="$(get_sharded_group_uuid "$SECRET_GROUP_NAME")"
+GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name}"]="$NON_SECRET_GROUP_NAME"
+GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid}"]="$(get_sharded_group_uuid "$NON_SECRET_GROUP_NAME")"
+
+mkdir -p "$OUT"
+trap 'rm -rf "$OUT"' EXIT
+
+TESTS=(
+"new_root_with_original_with_external_secret_ref.md"
+"non-secret_ref_with_external_secret_ref.md"
+"root_with_external_non-secret_ref_with_external_secret_ref.md"
+"root_with_external_secret_ref.md"
+"non_root_with_subtask_from_root_task.md"
+"subtask_using_user_syntax/root_with_subtask_secret_ref.md"
+"subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md"
+"subtask_using_group_syntax/root_with_subtask_secret_ref.md"
+"subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md"
+)
+
+for test in "${TESTS[@]}" ; do
+ TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users | replace_groups)"
+ init_configs
+ test_change
+done
+
+exit $RESULT
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml
index 634cde4..a228122 100755
--- a/test/docker/docker-compose.yaml
+++ b/test/docker/docker-compose.yaml
@@ -11,6 +11,7 @@
- gerrit-net
volumes:
- "gerrit-site-etc:/var/gerrit/etc"
+ - "gerrit-site-git:/var/gerrit/git"
run_tests:
build: run_tests
@@ -19,10 +20,12 @@
volumes:
- "../../:/task:ro"
- "gerrit-site-etc:/server-ssh-key:ro"
+ - "gerrit-site-git:/gerrit-site-git"
depends_on:
- gerrit-01
environment:
- GERRIT_HOST=gerrit-01
+ - GERRIT_GIT_DIR=/gerrit-site-git
networks:
gerrit-net:
@@ -30,3 +33,4 @@
volumes:
gerrit-site-etc:
+ gerrit-site-git:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile
index 12aa74a..a407854 100755
--- a/test/docker/gerrit/Dockerfile
+++ b/test/docker/gerrit/Dockerfile
@@ -1,9 +1,11 @@
-FROM gerritcodereview/gerrit:3.4.0-ubuntu20
+FROM gerritcodereview/gerrit:3.5.6-ubuntu20
ENV GERRIT_SITE /var/gerrit
RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \
DEVELOPMENT_BECOME_ANY_ACCOUNT
+RUN touch "$GERRIT_SITE"/.firstTimeRedirect
COPY artifacts /tmp/
RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar"
RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true
+RUN chmod 777 "$GERRIT_SITE/git"
diff --git a/test/docker/run.sh b/test/docker/run.sh
index 75b9b3a..5f9b412 100755
--- a/test/docker/run.sh
+++ b/test/docker/run.sh
@@ -57,12 +57,12 @@
run_task_plugin_tests() {
docker-compose "${COMPOSE_ARGS[@]}" up --detach
- docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin run_tests \
+ docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \
'/task/test/docker/run_tests/start.sh'
}
retest() {
- docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin \
+ docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin \
run_tests task/test/docker/run_tests/start.sh retest
RESULT=$?
cleanup
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile
index 06691e1..dd5ba8c 100755
--- a/test/docker/run_tests/Dockerfile
+++ b/test/docker/run_tests/Dockerfile
@@ -2,12 +2,12 @@
ARG UID=1000
ARG GID=1000
-ENV USER gerrit_admin
+ENV USER admin
ENV USER_HOME /home/$USER
ENV RUN_TESTS_DIR task/test/docker/run_tests
ENV WORKSPACE $USER_HOME/workspace
-RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd
+RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd curl jq
RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config
RUN groupadd -f -g $GID users2
@@ -21,6 +21,6 @@
RUN chmod 400 "$USER_HOME"/.ssh/id_rsa
RUN chmod 400 "$USER_HOME"/.ssh/id_rsa.pub
RUN git config --global user.name "Gerrit Admin"
-RUN git config --global user.email "gerrit_admin@localdomain"
+RUN git config --global user.email "admin@example.com"
ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/create-one-time-test-data.sh b/test/docker/run_tests/create-one-time-test-data.sh
new file mode 100755
index 0000000..d949bb2
--- /dev/null
+++ b/test/docker/run_tests/create-one-time-test-data.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+
+die() { echo -e "\nERROR:" "$@" ; kill $$ ; exit 1 ; } # error_message
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...] # quiet a command
+
+gssh() { ssh -x -p "$SSH_PORT" "$GERRIT_HOST" gerrit "$@" ; } # run a gerrit ssh command
+
+create_test_users_and_group() {
+ echo "Creating test users and group ..."
+ gssh create-account "$NON_SECRET_USER" --full-name "$NON_SECRET_USER" \
+ --email "$NON_SECRET_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub
+
+ gssh create-account "$UNTRUSTED_USER" --full-name "$UNTRUSTED_USER" \
+ --email "$UNTRUSTED_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub
+
+ gssh create-group "Visible-All-Projects-Config" --member "$NON_SECRET_USER"
+
+ local secret_user=$USER
+ gssh create-group "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" \
+ --member "$NON_SECRET_USER" --member "$secret_user"
+ gssh create-group "\"$NON_SECRET_GROUP_NAME_WITH_SPACE\"" \
+ --member "$NON_SECRET_USER" --member "$secret_user"
+ gssh create-group "$SECRET_GROUP_NAME" --member "$secret_user"
+}
+
+setup_all_projects_repo() {
+ echo "Updating All-Projects repo ..."
+
+ local uuid=$(gssh ls-groups -v | awk '-F\t' '$1 == "Visible-All-Projects-Config" {print $2}')
+ ( cd "$WORKSPACE"
+ q git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/All-Projects allProjects
+ cd allProjects
+ q git fetch origin refs/meta/config ; q git checkout FETCH_HEAD
+ echo -e "$uuid\tVisible-All-Projects-Config" >> groups
+ git config -f "project.config" \
+ --add access."refs/meta/config".read "group Visible-All-Projects-Config"
+ git config -f "project.config" \
+ --add capability.viewTaskPaths "group Administrators"
+# After migrating to version 3.5, it is no longer feasible to assign read permissions to
+# Administrators for another user's ref. To address this, add the 'accessDatabase' capability,
+# allowing admins to read the user ref of other users
+ git config -f "project.config" \
+ --add capability.accessDatabase "group Administrators"
+ q git add . && q git commit -m "project config update"
+ q git push origin HEAD:refs/meta/config
+ )
+}
+
+SSH_PORT=29418
+USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
+while (( "$#" )) ; do
+ case "$1" in
+ --non-secret-user) shift ; NON_SECRET_USER="$1" ;;
+ --untrusted-user) shift ; UNTRUSTED_USER="$1" ;;
+ --non-secret-group-without-space) shift ; NON_SECRET_GROUP_NAME_WITHOUT_SPACE="$1" ;;
+ --non-secret-group-with-space) shift ; NON_SECRET_GROUP_NAME_WITH_SPACE="$1" ;;
+ --secret-group) shift ; SECRET_GROUP_NAME="$1" ;;
+ *) die "invalid argument '$1'" ;;
+ esac
+ shift
+done
+
+[ -z "$NON_SECRET_USER" ] && die "non-secret-user not set"
+[ -z "$UNTRUSTED_USER" ] && die "untrusted-user not set"
+[ -z "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" ] && die "non-secret-group-without-space not set"
+[ -z "$NON_SECRET_GROUP_NAME_WITH_SPACE" ] && die "non-secret-group-with-space not set"
+[ -z "$SECRET_GROUP_NAME" ] && die "secret-group not set"
+
+"$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh
+"$USER_RUN_TESTS_DIR"/update-all-users-project.sh
+create_test_users_and_group
+setup_all_projects_repo
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh
index ac185d8..4a73f24 100755
--- a/test/docker/run_tests/start.sh
+++ b/test/docker/run_tests/start.sh
@@ -1,23 +1,55 @@
#!/usr/bin/env bash
+die() { echo "ERROR: $1" >&2 ; exit 1 ; } # errormsg
+
+is_plugin_loaded() { # plugin_name
+ ssh -p 29418 "$GERRIT_HOST" gerrit plugin ls | awk '{print $1}' | grep -q "^$1\$"
+}
+
USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR"
-cp -r /task "$USER_HOME"/
+mkdir "$USER_HOME"/task && cp -r /task/{src,test} "$USER_HOME"/task
if [ "$1" = "retest" ] ; then
cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST"
exit $?
fi
-./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 -t 60 -- echo "gerrit is up"
+./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 \
+ -t -60 || die "Failed to start gerrit"
+echo "gerrit is up"
-echo "Creating a default user account ..."
+echo "Update admin account ..."
cat "$USER_HOME"/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \
- "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit create-account \
- --ssh-key - --email "gerrit_admin@localdomain" --group "Administrators" "gerrit_admin"
+ "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit set-account \
+ admin --add-ssh-key -
-./"$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh
-./"$USER_RUN_TESTS_DIR"/update-all-users-project.sh
+PASSWORD=$(uuidgen)
+echo "machine $GERRIT_HOST login $USER password $PASSWORD" > "$USER_HOME"/.netrc
+ssh -p 29418 "$GERRIT_HOST" gerrit set-account --http-password "$PASSWORD" "$USER"
+
+is_plugin_loaded "task" || die "Task plugin is not installed"
+
+NON_SECRET_USER="non_secret_user"
+UNTRUSTED_USER="untrusted_user"
+GROUP_NAME_WITHOUT_SPACE="test.group"
+GROUP_NAME_WITH_SPACE="test group"
+SECRET_GROUP="private_group"
+"$USER_RUN_TESTS_DIR"/create-one-time-test-data.sh --non-secret-user "$NON_SECRET_USER" \
+ --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \
+ --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" --secret-group "$SECRET_GROUP"
echo "Running Task plugin tests ..."
-cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST"
+
+RESULT=0
+
+"$USER_RUN_TESTS_DIR"/../../check_task_statuses.sh \
+ --server "$GERRIT_HOST" --non-secret-user "$NON_SECRET_USER" \
+ --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \
+ --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" || RESULT=1
+
+"$USER_RUN_TESTS_DIR"/../../check_task_visibility.sh --server "$GERRIT_HOST" \
+ --non-secret-user "$NON_SECRET_USER" --non-secret-group "$GROUP_NAME_WITHOUT_SPACE" \
+ --secret-group "$SECRET_GROUP" || RESULT=1
+
+exit $RESULT
diff --git a/test/docker/run_tests/update-all-users-project.sh b/test/docker/run_tests/update-all-users-project.sh
index d0e1527..cfe2def 100755
--- a/test/docker/run_tests/update-all-users-project.sh
+++ b/test/docker/run_tests/update-all-users-project.sh
@@ -4,7 +4,13 @@
cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":29418/All-Users allusers && cd allusers
git fetch origin refs/meta/config && git checkout FETCH_HEAD
-git config -f project.config access."refs/users/*".read "group Administrators"
git config -f project.config access."refs/users/*".push "group Administrators"
-git config -f project.config access."refs/users/*".create "group Administrators"
+
+git config -f project.config access.'refs/users/${shardeduserid}'.read "group Registered Users"
+git config -f project.config access.'refs/users/${shardeduserid}'.push "group Registered Users"
+git config -f project.config access.'refs/users/${shardeduserid}'.create "group Registered Users"
+git config -f "project.config" \
+ access."refs/*".read "deny group Anonymous Users"
+echo -e "global:Registered-Users\tRegistered Users" >> groups
+echo -e "global:Anonymous-Users\tAnonymous Users" >> groups
git add . && git commit -m "project config update" && git push origin HEAD:refs/meta/config
diff --git a/test/lib/lib_helper.sh b/test/lib/lib_helper.sh
new file mode 100644
index 0000000..f4006db
--- /dev/null
+++ b/test/lib/lib_helper.sh
@@ -0,0 +1,350 @@
+#!/usr/bin/env bash
+#
+# 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.
+
+# ---- TEST RESULTS ----
+result() { # test [error_message]
+ local result=$?
+ if [ $result -eq 0 ] ; then
+ echo "PASSED - $1 test"
+ else
+ echo "*** FAILED *** - $1 test"
+ RESULT=$result
+ [ $# -gt 1 ] && echo "$2"
+ fi
+}
+
+# output must match expected to pass
+result_out() { # test expected actual
+ local name=$1 expected=$2 actual=$3
+
+ [ "$expected" = "$actual" ]
+ result "$name" "$(diff <(echo "$expected") <(echo "$actual"))"
+}
+
+result_root() { # group root
+ local name="$1 - $(echo "$2" | sed -es'/Root //')"
+ result_out "$name" "${EXPECTED_ROOTS[$2]}" "${OUTPUT_ROOTS[$2]}"
+}
+
+# -------- Git Config
+
+config() { git config -f "$CONFIG" "$@" ; } # [args]...
+config_section_keys() { # section > keys ...
+ # handlers.handler-filter filter.sh -> handler-filter
+ config -l --name-only |\
+ grep "^$1\." | \
+ sed -es"/^$1\.//;s/\..*$//" |\
+ awk '$0 != prev ; {prev = $0}'
+}
+
+# -------- Pre JSON --------
+#
+# pre_json is a "templated json" used in the test docs to express test results. It looks
+# like json but has some extra comments to express when a certain output should be used.
+# These comments look like: "# Only Test Suite: <suite>"
+#
+
+remove_suites() { # suites... < pre_json > json
+ grep -vE "# Only Test Suite: ($(echo "$@" | sed "s/ /|/g"))" | \
+ sed -e's/# Only Test Suite:.*$//; s/# Test Suite:.*$//; s/ *$//'
+}
+
+# pre_json is a "templated json" used in the test docs to express test results. It looks
+# like json but has some extra comments to express when a certain output should be used.
+# These comments look like: "# Test Suite: <suite>[, <suite>][, <suite>]..."
+#
+
+keep_suites() { # suites... < pre_json > json
+ grep -E "# Test Suite: (.*, )?($(echo "$@" | sed "s/ /|/g"))(, .*)?$" | \
+ sed -e's/# Only Test Suite:.*$//; s/# Test Suite:.*$//; s/ *$//'
+}
+
+remove_not_suite() { remove_suites !"$1" ; } # suite < pre_json > json
+
+# -------- Test Doc Format --------
+#
+# Test Doc Format has intermixed git config task definitions with json roots. This
+# makes it easy to define tests close to their outputs. Be aware that all of the
+# config will get consolidated into a single file, so non root config will be shared
+# amongst all the roots.
+#
+
+# Sample Test Doc for 2 roots:
+#
+# [root "Root PASS"]
+# pass = True
+#
+# {
+# "applicable" : true,
+# "hasPass" : true,
+# "name" : "Root PASS",
+# "status" : "PASS"
+# }
+#
+# [root "Root FAIL"]
+# fail = True
+#
+# {
+# <other root>
+# }
+
+# Strip the json from Test Doc formatted text. For the sample above, the output would be:
+#
+# [root "Root PASS"]
+# pass = True
+#
+# [root "Root FAIL"]
+# fail = True
+# ...
+#
+testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config
+
+# Strip the git config from Test Doc formatted text. For the sample above, the output would be:
+#
+# { "plugins" : [
+# { "name" : "task",
+# "roots" : [
+# {
+# "applicable" : true,
+# "hasPass" : true,
+# "name" : "Root PASS",
+# "status" : "PASS"
+# },
+# {
+# <other root>
+# },
+# ...
+# }
+testdoc_2_pjson() { # < testdoc_format > pjson_task_roots
+ awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \
+ /^\{/ { open=1 }; \
+ open && end { print "}," ; end=0 }; \
+ /^\}/ { open=0 ; end=1 }; \
+ open; \
+ END { print "}]}]}" }'
+}
+
+# ---- JSON PARSING ----
+
+json_pp() { # < json > json
+ python -c "import sys, json; \
+ print json.dumps(json.loads(sys.stdin.read()), indent=3, \
+ separators=(',', ' : '), sort_keys=True)"
+}
+
+json_val_by() { # json index|'key' > value
+ echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]"
+}
+json_val_by_key() { json_val_by "$1" "'$2'" ; } # json key > value
+
+# --------
+
+gssh() { # [-l user] cmd [args]...
+ local user_args=()
+ [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
+ ssh -x -p "$PORT" "${user_args[@]}" "$SERVER" gerrit "$@"
+}
+
+q() { "$@" > /dev/null 2>&1 ; } # cmd [args...] # quiet a command
+
+gen_change_id() { echo "I$(uuidgen | sha1sum | awk '{print $1}')"; } # > change_id
+
+commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg
+
+err() { echo "ERROR: $1" >&2 ; exit 1 ; }
+
+# Run a test setup command quietly, exit on failure
+q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...]
+
+ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data
+
+set_change() { # change_json
+ { CHANGE=("$(json_val_by_key "$1" number)" \
+ "$(json_val_by_key "$1" id)" \
+ "$(json_val_by_key "$1" project)" \
+ "refs/heads/$(json_val_by_key "$1" branch)" \
+ "$(json_val_by_key "$1" status)" \
+ "$(json_val_by_key "$1" topic)") ; } 2> /dev/null
+}
+
+# change_token change_number change_id project branch status topic < templated_txt > change_txt
+replace_change_properties() {
+ sed -e "s|_change$1_number|$2|g" \
+ -e "s|_change$1_id|$3|g" \
+ -e "s|_change$1_project|$4|g" \
+ -e "s|_change$1_branch|$5|g" \
+ -e "s|_change$1_status|$6|g" \
+ -e "s|_change$1_topic|$7|g"
+}
+
+replace_default_changes() {
+ replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}"
+}
+
+replace_groups() { # < text_with_groups > test_with_expanded_groups
+ local text="$(< /dev/stdin)"
+ for placeholder in "${!GROUP_EXPANDED_BY_PLACEHOLDER[@]}" ; do
+ text="${text//"$placeholder"/${GROUP_EXPANDED_BY_PLACEHOLDER["$placeholder"]}}"
+ done
+ echo "$text"
+}
+
+get_group_uuid() { # group_name > group_uuid
+ gssh ls-groups -v | awk '-F\t' '$1 == "'"$1"'" {print $2}'
+}
+
+get_sharded_group_uuid() { # group_name > sharded_group_uuid
+ local group_id=$(get_group_uuid "$1")
+ echo "${group_id:0:2}/$group_id"
+}
+
+replace_users() { # < text_with_users > test_with_expanded_users
+ local text="$(< /dev/stdin)"
+ for user in "${!USERS[@]}" ; do
+ text="${text//"$user"/${USERS["$user"]}}"
+ done
+ echo "$text"
+}
+
+replace_user() { # < text_with_testuser > text_with_$USER
+ sed -e"s/testuser/$USER/"
+}
+
+get_user_ref() { # username > refs/users/<accountidshard>/<accountid>
+ local user_account_id="$(curl --netrc --silent "http://$SERVER:$HTTP_PORT/a/accounts/$1" | \
+ sed -e '1!b' -e "/^)]}'$/d" | jq ._account_id)"
+ echo "refs/users/${user_account_id:(-2)}/$user_account_id"
+}
+
+replace_user_refs() { # < text_with_user_refs > test_with_expanded_user_refs
+ local text="$(< /dev/stdin)"
+ for user in "${!USER_REFS[@]}" ; do
+ text="${text//"$user"/${USER_REFS["$user"]}}"
+ done
+ echo "$text"
+}
+
+replace_tokens() { # < text > text with replacing all tokens(changes, user)
+ replace_default_changes | replace_user_refs | replace_user | replace_groups
+}
+
+strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json
+strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json
+
+define_jsonByRoot() { # task_plugin_ouptut > jsonByRoot_array_definition
+ local record root=''
+ local -A jsonByRoot
+ while IFS= read -r -d '' record ; do
+ if [ -z "$root" ] ; then
+ root=$record
+ else
+ jsonByRoot[$root]=$record
+ root=''
+ fi
+ done < <(python -c "if True: # NOP to start indent
+ import sys, json
+
+ roots=json.loads(sys.stdin.read())['plugins'][0]['roots']
+ for root in roots:
+ root_json = json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)
+ sys.stdout.write(root['name'] + '\x00' + root_json + '\x00')"
+ )
+
+ local def=$(declare -p jsonByRoot)
+ echo "${def#*=}" # declare -A jsonByRoot='(...)' > '(...)'
+}
+
+get_plugins() { # < change_json > plugins_json
+ python -c "import sys, json; \
+ plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \
+ print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)"
+}
+
+example() { # doc example_num > text_for_example_num
+ echo "$1" | awk '/```/{Q++;E=(Q+1)/2};E=='"$2" | grep -v '```'
+}
+
+get_change_num() { # < gerrit_push_response > changenum
+ local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }')
+ echo "${url##*\/}" | tr -d -c '[:digit:]'
+}
+
+install_changeid_hook() { # repo
+ local hook=$(git rev-parse --git-dir)/hooks/commit-msg
+ scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook"
+ chmod +x "$hook"
+}
+
+setup_repo() { # repo remote ref [--initial-commit]
+ local repo=$1 remote=$2 ref=$3 init=$4
+ git init "$repo"
+ (
+ cd "$repo"
+ install_changeid_hook "$repo"
+ git fetch "$remote" "$ref"
+ if ! git checkout FETCH_HEAD ; then
+ if [ "$init" = "--initial-commit" ] ; then
+ git commit --allow-empty -a -m "Initial Commit"
+ fi
+ fi
+ )
+}
+
+update_repo() { # repo remote ref
+ local repo=$1 remote=$2 ref=$3
+ (
+ cd "$repo"
+ git add .
+ git commit -m 'Testing task plugin'
+ git push "$remote" HEAD:"$ref"
+ )
+}
+
+create_repo_change() { # repo remote ref [change_id] > change_num
+ local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change"
+ (
+ q cd "$repo"
+ uuidgen > file
+ q git add .
+ [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id")
+ q git commit -m "$msg"
+ git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num
+ )
+}
+
+query() { # [-l user] query > json lines
+ local user_args=()
+ [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; }
+ gssh "${user_args[@]}" query "$@" --format json
+}
+
+# N < json lines > changeN_json
+change_plugins() { awk "NR==$1" | get_plugins | json_pp ; }
+
+results_suite() { # name expected_file plugins_json
+ local name=$1 expected=$2 actual=$3
+
+ local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected")
+ local -A OUTPUT_ROOTS=$(echo "$actual" | define_jsonByRoot)
+
+ local out root
+ echo "$ROOTS" | while read root ; do
+ result_root "$name" "$root"
+ done
+ out=$(diff "$expected" <(echo "$actual") | head -15)
+ [ -z "$out" ]
+ result "$name - Full Test Suite" "$out"
+}
diff --git a/test/lib/lib_md.sh b/test/lib/lib_md.sh
new file mode 100644
index 0000000..2838163
--- /dev/null
+++ b/test/lib/lib_md.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+#
+# 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.
+
+# ---- Markdown Format Helpers ----
+
+# Example markdown file:
+# (Using block comment to better understand the file syntax.)
+
+: <<'END'
+# Test case description header
+
+file: `All-Projects.git:refs/meta/config:task.config`
+```
+[root "Test root"]
+ applicable = "is:open"
+ pass = True
+```
+
+file: `All-Users:refs/users/some_ref:task/sample.config`
+```
+ [task "NON-SECRET task"]
+ applicable = is:open
+ pass = Fail
++ subtasks-external = SECRET
+
++[external "SECRET"]
++ user = {secret_user}
++ file = secret.config
+```
+
+json:
+```
+{
+ {
+ "some": "example"
+ }
+}
+END
+
+# (For example above)
+# out:
+# `All-Projects.git:refs/meta/config:task.config`
+# `All-Users:refs/users/some_ref:task/sample.config`
+md_file_markers() { # DOC_CONTENT
+ echo "$1" | grep -o "^file: .*" | cut -f2 -d'`'
+}
+
+# (For example above)
+# in: `All-Projects.git:refs/meta/config:task.config`
+# out:
+#[root "Test root"]
+# applicable = "is:open"
+# pass = True
+#
+# in: json:
+# out :
+# {
+# {
+# "some": "example"
+# }
+# }
+md_marker_content() { # DOC marker
+ local start_line=$(echo "$1" | grep -n "$2" | cut -f1 -d':')
+ echo "$1" | tail -n+"$start_line" | \
+ sed '1,/```/d;/```/,$d' | grep -v '```'
+}
+
+# file_marker > project
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+# out: All-Projects.git
+md_file_marker_project() {
+ echo "$1" | cut -f1 -d':'
+}
+
+# file_marker > ref
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+# out: refs/meta/config
+md_file_marker_ref() {
+ echo "$1" | cut -f2 -d':'
+}
+
+# file_marker > file
+# in: `All-Projects.git:refs/meta/config:task/task.config`
+#out: task/task.config
+md_file_marker_file() {
+ echo "$1" | cut -f3 -d':'
+}
\ No newline at end of file
diff --git a/test/strip_non_applicable.py b/test/strip_non_applicable.py
index 1ff097a..41c21fa 100755
--- a/test/strip_non_applicable.py
+++ b/test/strip_non_applicable.py
@@ -43,7 +43,7 @@
status=''
if STATUS in task.keys():
status = task[STATUS]
- if status != 'INVALID':
+ if status != 'INVALID' and status != 'DUPLICATE':
del tasks[i]
nexti = i
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..5df79bb
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,6 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ _junit_tests = "junit_tests",
+)
+
+junit_tests = _junit_tests
\ No newline at end of file
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..4871c7b
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,4 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", _gerrit = "GERRIT", _maven_jar = "maven_jar")
+
+maven_jar = _maven_jar
+GERRIT = _gerrit
\ No newline at end of file
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 89a1643..67536ef 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -2,7 +2,9 @@
"@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
_gerrit_plugin = "gerrit_plugin",
_plugin_deps = "PLUGIN_DEPS",
+ _plugin_test_deps = "PLUGIN_TEST_DEPS",
)
gerrit_plugin = _gerrit_plugin
PLUGIN_DEPS = _plugin_deps
+PLUGIN_TEST_DEPS = _plugin_test_deps
\ No newline at end of file
diff --git a/tools/playbooks/install_maven.yaml b/tools/playbooks/install_maven.yaml
new file mode 100644
index 0000000..ae1690d
--- /dev/null
+++ b/tools/playbooks/install_maven.yaml
@@ -0,0 +1,8 @@
+- hosts: all
+ tasks:
+ - name: Install maven
+ become: true
+ package:
+ name:
+ - maven
+ state: present
diff --git a/tools/playbooks/install_python3-distutils.yaml b/tools/playbooks/install_python3-distutils.yaml
new file mode 100644
index 0000000..75f5a2a
--- /dev/null
+++ b/tools/playbooks/install_python3-distutils.yaml
@@ -0,0 +1,10 @@
+- hosts: all
+ roles:
+ - name: ensure-python
+ tasks:
+ - name: Install python3-distutils
+ become: true
+ package:
+ name:
+ - python3-distutils
+ state: present
diff --git a/tools/workspace_status.py b/tools/workspace_status.py
index d948424..fb5ec6d 100644
--- a/tools/workspace_status.py
+++ b/tools/workspace_status.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# This script will be run by bazel when the build process starts to
# generate key-value information that represents the status of the
diff --git a/yarn.lock b/yarn.lock
index 0bdc3f3..da0237a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,53 +2,91 @@
# yarn lockfile v1
-"@babel/code-frame@^7.0.0":
- version "7.12.13"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
- integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
+"@babel/code-frame@7.12.11":
+ version "7.12.11"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
+ integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
dependencies:
- "@babel/highlight" "^7.12.13"
+ "@babel/highlight" "^7.10.4"
-"@babel/helper-validator-identifier@^7.14.0":
- version "7.14.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288"
- integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==
+"@babel/helper-validator-identifier@^7.18.6":
+ version "7.19.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+ integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
-"@babel/highlight@^7.12.13":
- version "7.14.0"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf"
- integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==
+"@babel/highlight@^7.10.4":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+ integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
dependencies:
- "@babel/helper-validator-identifier" "^7.14.0"
+ "@babel/helper-validator-identifier" "^7.18.6"
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@bazel/rollup@^3.4.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
- integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
+"@bazel/rollup@~5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.1.0.tgz#dc858ddc93c9fdb9cc2e7982e632c939c646ebdc"
+ integrity sha512-wEiWdSyVbsycSirSYjR6FGfPGbRNI7sGNAYmrV0hIzYIi+KqXeTNcwKIRSE9PESP3mb0VWbZmHvXvmrWk6daPQ==
+ dependencies:
+ "@bazel/worker" "5.1.0"
-"@bazel/terser@^3.4.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b"
- integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA==
+"@bazel/terser@~5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-5.1.0.tgz#5c82b93f4d9def8103c16be2dd33900d156fa066"
+ integrity sha512-uE3hTqfkZr4nvlk3jwi0xx6URqqI7r6GGPtDAU02/PVei+O4PfThaov7cwHO+D1FnoLncDqChb9Iolr7Crw/8A==
+
+"@bazel/worker@5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.1.0.tgz#6f1e0f3ef628e3449d424cacd341c9abd09a3735"
+ integrity sha512-u3aU93UtHz3vL6ozezq0jnw83s1cNT4dAnW+vvB7M++YKFlB3CWzZFb0JRJbCp1b6DDe30ML0WOdd3nVYuylpw==
+ dependencies:
+ google-protobuf "^3.6.1"
+
+"@eslint/eslintrc@^0.4.3":
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
+ integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.1.1"
+ espree "^7.3.0"
+ globals "^13.9.0"
+ ignore "^4.0.6"
+ import-fresh "^3.2.1"
+ js-yaml "^3.13.1"
+ minimatch "^3.0.4"
+ strip-json-comments "^3.1.1"
+
+"@humanwhocodes/config-array@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
+ integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
+ dependencies:
+ "@humanwhocodes/object-schema" "^1.2.0"
+ debug "^4.1.1"
+ minimatch "^3.0.4"
+
+"@humanwhocodes/object-schema@^1.2.0":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
+ integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
-acorn-jsx@^5.2.0:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
- integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
+acorn-jsx@^5.3.1:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
-acorn@^7.1.1:
+acorn@^7.4.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-ajv@^6.10.0, ajv@^6.10.2:
+ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -58,31 +96,39 @@
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ansi-escapes@^4.2.1:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
- integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+ajv@^8.0.1:
+ version "8.11.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
+ integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
dependencies:
- type-fest "^0.21.3"
+ fast-deep-equal "^3.1.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+ uri-js "^4.2.2"
-ansi-regex@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
- integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+ansi-colors@^4.1.1:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
+ integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"
-ansi-styles@^4.1.0:
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@@ -96,30 +142,31 @@
dependencies:
sprintf-js "~1.0.2"
-array-includes@^3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a"
- integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==
+array-includes@^3.1.4:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb"
+ integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
+ get-intrinsic "^1.1.1"
+ is-string "^1.0.7"
+
+array.prototype.flat@^1.2.5:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b"
+ integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.18.0-next.2"
- get-intrinsic "^1.1.1"
- is-string "^1.0.5"
+ es-abstract "^1.19.2"
+ es-shim-unscopables "^1.0.0"
-array.prototype.flat@^1.2.4:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
- integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
- dependencies:
- call-bind "^1.0.0"
- define-properties "^1.1.3"
- es-abstract "^1.18.0-next.1"
-
-astral-regex@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
- integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+astral-regex@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+ integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
balanced-match@^1.0.0:
version "1.0.2"
@@ -152,7 +199,7 @@
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
-chalk@^2.0.0, chalk@^2.1.0:
+chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -161,31 +208,14 @@
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@^4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
- integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
+chalk@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chardet@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
- integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
-
-cli-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
- integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
- dependencies:
- restore-cursor "^3.1.0"
-
-cli-width@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
- integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
-
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -215,26 +245,24 @@
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
-comment-parser@^0.7.2:
- version "0.7.6"
- resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12"
- integrity sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg==
+comment-parser@1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.5.tgz#453627ef8f67dbcec44e79a9bd5baa37f0bce9b2"
+ integrity sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-cross-spawn@^6.0.5:
- version "6.0.5"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
- integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+cross-spawn@^7.0.2:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
- nice-try "^1.0.4"
- path-key "^2.0.1"
- semver "^5.5.0"
- shebang-command "^1.2.0"
- which "^1.2.9"
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
debug@^2.6.9:
version "2.6.9"
@@ -264,10 +292,17 @@
dependencies:
ms "2.1.2"
-deep-is@~0.1.3:
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
- integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+debug@^4.3.1:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+deep-is@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+ integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
define-properties@^1.1.3:
version "1.1.3"
@@ -276,6 +311,14 @@
dependencies:
object-keys "^1.0.12"
+define-properties@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
+ integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
+ dependencies:
+ has-property-descriptors "^1.0.0"
+ object-keys "^1.1.1"
+
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -311,60 +354,79 @@
dependencies:
domelementtype "^2.2.0"
-domutils@^2.5.2:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7"
- integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==
+domhandler@^4.2.2:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+ integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+ dependencies:
+ domelementtype "^2.2.0"
+
+domutils@^2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+ integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.2.0"
domhandler "^4.2.0"
-emoji-regex@^7.0.1:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
- integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
-
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+enquirer@^2.3.5:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+ integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+ dependencies:
+ ansi-colors "^4.1.1"
+
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
-error-ex@^1.3.1:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
- integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
- dependencies:
- is-arrayish "^0.2.1"
+entities@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+ integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
-es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
- version "1.18.6"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456"
- integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==
+es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.2.tgz#8495a07bc56d342a3b8ea3ab01bd986700c2ccb3"
+ integrity sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==
dependencies:
call-bind "^1.0.2"
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
- get-intrinsic "^1.1.1"
+ function.prototype.name "^1.1.5"
+ get-intrinsic "^1.1.2"
get-symbol-description "^1.0.0"
has "^1.0.3"
- has-symbols "^1.0.2"
+ has-property-descriptors "^1.0.0"
+ has-symbols "^1.0.3"
internal-slot "^1.0.3"
is-callable "^1.2.4"
- is-negative-zero "^2.0.1"
+ is-negative-zero "^2.0.2"
is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.2"
is-string "^1.0.7"
- object-inspect "^1.11.0"
+ is-weakref "^1.0.2"
+ object-inspect "^1.12.2"
object-keys "^1.1.1"
- object.assign "^4.1.2"
- string.prototype.trimend "^1.0.4"
- string.prototype.trimstart "^1.0.4"
- unbox-primitive "^1.0.1"
+ object.assign "^4.1.4"
+ regexp.prototype.flags "^1.4.3"
+ string.prototype.trimend "^1.0.5"
+ string.prototype.trimstart "^1.0.5"
+ unbox-primitive "^1.0.2"
+
+es-shim-unscopables@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
+ integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==
+ dependencies:
+ has "^1.0.3"
es-to-primitive@^1.2.1:
version "1.2.1"
@@ -380,10 +442,15 @@
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-eslint-config-google@^0.13.0:
- version "0.13.0"
- resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.13.0.tgz#e277d16d2cb25c1ffd3fd13fb0035ad7421382fe"
- integrity sha512-ELgMdOIpn0CFdsQS+FuxO+Ttu4p+aLaXHv9wA9yVnzqlUGV7oN/eRRnJekk7TCur6Cu2FXX0fqfIXRBaM14lpQ==
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-config-google@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
+ integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
eslint-import-resolver-node@^0.3.6:
version "0.3.6"
@@ -393,57 +460,53 @@
debug "^3.2.7"
resolve "^1.20.0"
-eslint-module-utils@^2.6.2:
- version "2.6.2"
- resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534"
- integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==
+eslint-module-utils@^2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974"
+ integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==
dependencies:
debug "^3.2.7"
- pkg-dir "^2.0.0"
-eslint-plugin-html@^6.0.0:
- version "6.1.2"
- resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.1.2.tgz#fa26e4804428956c80e963b6499c192061c2daf3"
- integrity sha512-bhBIRyZFqI4EoF12lGDHAmgfff8eLXx6R52/K3ESQhsxzCzIE6hdebS7Py651f7U3RBotqroUnC3L29bR7qJWQ==
+eslint-plugin-html@^6.1.2:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.2.0.tgz#715bc00b50bbd0d996e28f953c289a5ebec69d43"
+ integrity sha512-vi3NW0E8AJombTvt8beMwkL1R/fdRWl4QSNRNMhVQKWm36/X0KF0unGNAY4mqUF06mnwVWZcIcerrCnfn9025g==
dependencies:
- htmlparser2 "^6.0.1"
+ htmlparser2 "^7.1.2"
-eslint-plugin-import@^2.20.1:
- version "2.24.2"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da"
- integrity sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==
+eslint-plugin-import@^2.22.1:
+ version "2.26.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"
+ integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==
dependencies:
- array-includes "^3.1.3"
- array.prototype.flat "^1.2.4"
+ array-includes "^3.1.4"
+ array.prototype.flat "^1.2.5"
debug "^2.6.9"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.6"
- eslint-module-utils "^2.6.2"
- find-up "^2.0.0"
+ eslint-module-utils "^2.7.3"
has "^1.0.3"
- is-core-module "^2.6.0"
- minimatch "^3.0.4"
- object.values "^1.1.4"
- pkg-up "^2.0.0"
- read-pkg-up "^3.0.0"
- resolve "^1.20.0"
- tsconfig-paths "^3.11.0"
+ is-core-module "^2.8.1"
+ is-glob "^4.0.3"
+ minimatch "^3.1.2"
+ object.values "^1.1.5"
+ resolve "^1.22.0"
+ tsconfig-paths "^3.14.1"
-eslint-plugin-jsdoc@^19.2.0:
- version "19.2.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-19.2.0.tgz#f522b970878ae402b28ce62187305b33dfe2c834"
- integrity sha512-QdNifBFLXCDGdy+26RXxcrqzEZarFWNybCZQVqJQYEYPlxd6lm+LPkrs6mCOhaGc2wqC6zqpedBQFX8nQJuKSw==
+eslint-plugin-jsdoc@^32.3.0:
+ version "32.3.4"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-32.3.4.tgz#6888f3b2dbb9f73fb551458c639a4e8c84fe9ddc"
+ integrity sha512-xSWfsYvffXnN0OkwLnB7MoDDDDjqcp46W7YlY1j7JyfAQBQ+WnGCfLov3gVNZjUGtK9Otj8mEhTZTqJu4QtIGA==
dependencies:
- 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"
+ comment-parser "1.1.5"
+ debug "^4.3.1"
+ jsdoctypeparser "^9.0.0"
+ lodash "^4.17.21"
+ regextras "^0.7.1"
+ semver "^7.3.5"
+ spdx-expression-parse "^3.0.1"
-eslint-scope@^5.0.0:
+eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -451,76 +514,84 @@
esrecurse "^4.3.0"
estraverse "^4.1.1"
-eslint-utils@^1.4.3:
- version "1.4.3"
- resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
- integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+eslint-utils@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
+ integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
dependencies:
eslint-visitor-keys "^1.1.0"
-eslint-visitor-keys@^1.1.0:
+eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
-eslint@^6.6.0:
- version "6.8.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
- integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+eslint-visitor-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
+ integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
+
+eslint@^7.24.0:
+ version "7.32.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
+ integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
dependencies:
- "@babel/code-frame" "^7.0.0"
+ "@babel/code-frame" "7.12.11"
+ "@eslint/eslintrc" "^0.4.3"
+ "@humanwhocodes/config-array" "^0.5.0"
ajv "^6.10.0"
- chalk "^2.1.0"
- cross-spawn "^6.0.5"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.2"
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"
+ enquirer "^2.3.5"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^5.1.1"
+ eslint-utils "^2.1.0"
+ eslint-visitor-keys "^2.0.0"
+ espree "^7.3.1"
+ esquery "^1.4.0"
esutils "^2.0.2"
- file-entry-cache "^5.0.1"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
- glob-parent "^5.0.0"
- globals "^12.1.0"
+ glob-parent "^5.1.2"
+ globals "^13.6.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"
+ levn "^0.4.1"
+ lodash.merge "^4.6.2"
minimatch "^3.0.4"
- mkdirp "^0.5.1"
natural-compare "^1.4.0"
- optionator "^0.8.3"
+ optionator "^0.9.1"
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"
+ regexpp "^3.1.0"
+ semver "^7.2.1"
+ strip-ansi "^6.0.0"
+ strip-json-comments "^3.1.0"
+ table "^6.0.9"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
-espree@^6.1.2:
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
- integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
+espree@^7.3.0, espree@^7.3.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
+ integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
dependencies:
- acorn "^7.1.1"
- acorn-jsx "^5.2.0"
- eslint-visitor-keys "^1.1.0"
+ acorn "^7.4.0"
+ acorn-jsx "^5.3.1"
+ eslint-visitor-keys "^1.3.0"
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-esquery@^1.0.1:
+esquery@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
@@ -549,16 +620,7 @@
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-external-editor@^3.0.3:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
- integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
- dependencies:
- chardet "^0.7.0"
- iconv-lite "^0.4.24"
- tmp "^0.0.33"
-
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -568,45 +630,30 @@
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
- integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+ integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
-figures@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
- integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
+file-entry-cache@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
+ integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
dependencies:
- escape-string-regexp "^1.0.5"
+ flat-cache "^3.0.4"
-file-entry-cache@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
- integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
+flat-cache@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
+ integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
dependencies:
- flat-cache "^2.0.1"
+ flatted "^3.1.0"
+ rimraf "^3.0.2"
-find-up@^2.0.0, find-up@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
- integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
- dependencies:
- locate-path "^2.0.0"
-
-flat-cache@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
- integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
- dependencies:
- flatted "^2.0.0"
- rimraf "2.6.3"
- write "1.0.3"
-
-flatted@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
- integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+flatted@^3.1.0:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
+ integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
fs.realpath@^1.0.0:
version "1.0.0"
@@ -623,11 +670,26 @@
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function.prototype.name@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+ integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+ functions-have-names "^1.2.2"
+
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+functions-have-names@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
@@ -637,6 +699,15 @@
has "^1.0.3"
has-symbols "^1.0.1"
+get-intrinsic@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385"
+ integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.3"
+
get-symbol-description@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
@@ -645,7 +716,7 @@
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
-glob-parent@^5.0.0:
+glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -664,23 +735,28 @@
once "^1.3.0"
path-is-absolute "^1.0.0"
-globals@^12.1.0:
- version "12.4.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
- integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
+globals@^13.6.0, globals@^13.9.0:
+ version "13.17.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4"
+ integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==
dependencies:
- type-fest "^0.8.1"
+ type-fest "^0.20.2"
-graceful-fs@^4.1.2:
- version "4.2.8"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
- integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+google-protobuf@^3.6.1:
+ version "3.21.2"
+ resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4"
+ integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+has-bigints@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+ integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
+
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -691,11 +767,23 @@
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+has-property-descriptors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
+ integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==
+ dependencies:
+ get-intrinsic "^1.1.1"
+
has-symbols@^1.0.1, has-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
has-tostringtag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
@@ -710,34 +798,22 @@
dependencies:
function-bind "^1.1.1"
-hosted-git-info@^2.1.4:
- version "2.8.9"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
- integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
-
-htmlparser2@^6.0.1:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
- integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
+htmlparser2@^7.1.2:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5"
+ integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==
dependencies:
domelementtype "^2.0.1"
- domhandler "^4.0.0"
- domutils "^2.5.2"
- entities "^2.0.0"
-
-iconv-lite@^0.4.24:
- version "0.4.24"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
- integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
- dependencies:
- safer-buffer ">= 2.1.2 < 3"
+ domhandler "^4.2.2"
+ domutils "^2.8.0"
+ entities "^3.0.1"
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
-import-fresh@^3.0.0:
+import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -763,25 +839,6 @@
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-inquirer@^7.0.0:
- version "7.3.3"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
- integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
- dependencies:
- 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"
-
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@@ -791,11 +848,6 @@
has "^1.0.3"
side-channel "^1.0.4"
-is-arrayish@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
- integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
-
is-bigint@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
@@ -816,13 +868,20 @@
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
-is-core-module@^2.2.0, is-core-module@^2.6.0:
+is-core-module@^2.2.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
dependencies:
has "^1.0.3"
+is-core-module@^2.8.1, is-core-module@^2.9.0:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
+ integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
+ dependencies:
+ has "^1.0.3"
+
is-date-object@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
@@ -835,11 +894,6 @@
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
-is-fullwidth-code-point@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
- integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -852,10 +906,17 @@
dependencies:
is-extglob "^2.1.1"
-is-negative-zero@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
- integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+is-glob@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-negative-zero@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
+ integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
is-number-object@^1.0.4:
version "1.0.6"
@@ -872,6 +933,13 @@
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
+is-shared-array-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
+ integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
+ dependencies:
+ call-bind "^1.0.2"
+
is-string@^1.0.5, is-string@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
@@ -886,6 +954,13 @@
dependencies:
has-symbols "^1.0.2"
+is-weakref@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
+ integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==
+ dependencies:
+ call-bind "^1.0.2"
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -904,21 +979,21 @@
argparse "^1.0.7"
esprima "^4.0.0"
-jsdoctypeparser@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz#acfb936c26300d98f1405cb03e20b06748e512a8"
- integrity sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA==
-
-json-parse-better-errors@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
- integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+jsdoctypeparser@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26"
+ integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+json-schema-traverse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+ integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
@@ -931,41 +1006,35 @@
dependencies:
minimist "^1.2.0"
-levn@^0.3.0, levn@~0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
- integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+levn@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+ integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
dependencies:
- prelude-ls "~1.1.2"
- type-check "~0.3.2"
+ prelude-ls "^1.2.1"
+ type-check "~0.4.0"
-load-json-file@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
- integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
- dependencies:
- graceful-fs "^4.1.2"
- parse-json "^4.0.0"
- pify "^3.0.0"
- strip-bom "^3.0.0"
+lodash.merge@^4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+ integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
-locate-path@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
- integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
- dependencies:
- p-locate "^2.0.0"
- path-exists "^3.0.0"
+lodash.truncate@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
+ integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
+lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-mimic-fn@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
- integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
minimatch@^3.0.4:
version "3.0.4"
@@ -974,17 +1043,22 @@
dependencies:
brace-expansion "^1.1.7"
-minimist@^1.2.0, minimist@^1.2.5:
+minimatch@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-mkdirp@^0.5.1:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
- integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
- dependencies:
- minimist "^1.2.5"
+minimist@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+ integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
ms@2.0.0:
version "2.0.0"
@@ -1001,32 +1075,17 @@
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-mute-stream@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
- integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
-
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
-nice-try@^1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
- integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+object-inspect@^1.12.2:
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
+ integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
-normalize-package-data@^2.3.2:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
- integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
- dependencies:
- hosted-git-info "^2.1.4"
- resolve "^1.10.0"
- semver "2 || 3 || 4 || 5"
- validate-npm-package-license "^3.0.1"
-
-object-inspect@^1.11.0, object-inspect@^1.9.0:
+object-inspect@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
@@ -1036,29 +1095,24 @@
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
-object.assign@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
- integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+object.assign@^4.1.4:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
+ integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
dependencies:
- call-bind "^1.0.0"
- define-properties "^1.1.3"
- has-symbols "^1.0.1"
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ has-symbols "^1.0.3"
object-keys "^1.1.1"
-object.entries-ponyfill@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz#29abdf77cbfbd26566dd1aa24e9d88f65433d256"
- integrity sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY=
-
-object.values@^1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
- integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
+object.values@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
+ integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.18.2"
+ es-abstract "^1.19.1"
once@^1.3.0:
version "1.4.0"
@@ -1067,48 +1121,17 @@
dependencies:
wrappy "1"
-onetime@^5.1.0:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
- integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+optionator@^0.9.1:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+ integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
dependencies:
- mimic-fn "^2.1.0"
-
-optionator@^0.8.3:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
- integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
- dependencies:
- deep-is "~0.1.3"
- fast-levenshtein "~2.0.6"
- levn "~0.3.0"
- prelude-ls "~1.1.2"
- type-check "~0.3.2"
- word-wrap "~1.2.3"
-
-os-tmpdir@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
- integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
-
-p-limit@^1.1.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
- integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
- dependencies:
- p-try "^1.0.0"
-
-p-locate@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
- integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
- dependencies:
- p-limit "^1.1.0"
-
-p-try@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
- integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+ deep-is "^0.1.3"
+ fast-levenshtein "^2.0.6"
+ levn "^0.4.1"
+ prelude-ls "^1.2.1"
+ type-check "^0.4.0"
+ word-wrap "^1.2.3"
parent-module@^1.0.0:
version "1.0.1"
@@ -1117,64 +1140,25 @@
dependencies:
callsites "^3.0.0"
-parse-json@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
- integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
- dependencies:
- error-ex "^1.3.1"
- json-parse-better-errors "^1.0.1"
-
-path-exists@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
- integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
-
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-path-key@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
- integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-path-parse@^1.0.6:
+path-parse@^1.0.6, path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
-path-type@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
- integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
- dependencies:
- pify "^3.0.0"
-
-pify@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
- integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
-
-pkg-dir@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
- integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
- dependencies:
- find-up "^2.1.0"
-
-pkg-up@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
- integrity sha1-yBmscoBZpGHKscOImivjxJoATX8=
- dependencies:
- find-up "^2.1.0"
-
-prelude-ls@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
- integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+prelude-ls@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+ integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
progress@^2.0.0:
version "2.0.3"
@@ -1186,39 +1170,36 @@
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-read-pkg-up@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
- integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
+regexp.prototype.flags@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
+ integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
dependencies:
- find-up "^2.0.0"
- read-pkg "^3.0.0"
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ functions-have-names "^1.2.2"
-read-pkg@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
- integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
- dependencies:
- load-json-file "^4.0.0"
- normalize-package-data "^2.3.2"
- path-type "^3.0.0"
+regexpp@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+ integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
-regexpp@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
- integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
-
-regextras@^0.7.0:
+regextras@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2"
integrity sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==
+require-from-string@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
-resolve@^1.10.0, resolve@^1.20.0:
+resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -1226,18 +1207,19 @@
is-core-module "^2.2.0"
path-parse "^1.0.6"
-restore-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
- integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+resolve@^1.22.0:
+ version "1.22.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+ integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
- onetime "^5.1.0"
- signal-exit "^3.0.2"
+ is-core-module "^2.9.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
-rimraf@2.6.3:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
- integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
@@ -1248,44 +1230,24 @@
optionalDependencies:
fsevents "~2.3.1"
-run-async@^2.4.0:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
- integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
-
-rxjs@^6.6.0:
- version "6.6.7"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
- integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+semver@^7.2.1, semver@^7.3.5:
+ version "7.3.7"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+ integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
dependencies:
- tslib "^1.9.0"
+ lru-cache "^6.0.0"
-"safer-buffer@>= 2.1.2 < 3":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
- integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-"semver@2 || 3 || 4 || 5", semver@^5.5.0:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
- integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
-semver@^6.1.2, semver@^6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-shebang-command@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
- integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
- shebang-regex "^1.0.0"
+ shebang-regex "^3.0.0"
-shebang-regex@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
- integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
side-channel@^1.0.4:
version "1.0.4"
@@ -1296,19 +1258,14 @@
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
-signal-exit@^3.0.2:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
- integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
-
-slice-ansi@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
- integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
+slice-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+ integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
dependencies:
- ansi-styles "^3.2.0"
- astral-regex "^1.0.0"
- is-fullwidth-code-point "^2.0.0"
+ ansi-styles "^4.0.0"
+ astral-regex "^2.0.0"
+ is-fullwidth-code-point "^3.0.0"
source-map-support@~0.5.19:
version "0.5.19"
@@ -1328,20 +1285,12 @@
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-spdx-correct@^3.0.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
- integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
- dependencies:
- spdx-expression-parse "^3.0.0"
- spdx-license-ids "^3.0.0"
-
spdx-exceptions@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
-spdx-expression-parse@^3.0.0:
+spdx-expression-parse@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
@@ -1359,46 +1308,32 @@
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
-string-width@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
- integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
- dependencies:
- emoji-regex "^7.0.1"
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^5.1.0"
-
-string-width@^4.1.0:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
- integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
+string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.0"
+ strip-ansi "^6.0.1"
-string.prototype.trimend@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
- integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+string.prototype.trimend@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0"
+ integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
-string.prototype.trimstart@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
- integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+string.prototype.trimstart@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef"
+ integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
-
-strip-ansi@^5.1.0, strip-ansi@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
- integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
- dependencies:
- ansi-regex "^4.1.0"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
strip-ansi@^6.0.0:
version "6.0.0"
@@ -1407,12 +1342,19 @@
dependencies:
ansi-regex "^5.0.0"
+strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
-strip-json-comments@^3.0.1:
+strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -1431,15 +1373,21 @@
dependencies:
has-flag "^4.0.0"
-table@^5.2.3:
- version "5.4.6"
- resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
- integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+table@^6.0.9:
+ version "6.8.0"
+ resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
+ integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==
dependencies:
- ajv "^6.10.2"
- lodash "^4.17.14"
- slice-ansi "^2.1.0"
- string-width "^3.0.0"
+ ajv "^8.0.1"
+ lodash.truncate "^4.4.2"
+ slice-ansi "^4.0.0"
+ string-width "^4.2.3"
+ strip-ansi "^6.0.1"
terser@^5.6.1:
version "5.7.0"
@@ -1455,58 +1403,36 @@
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
-through@^2.3.6:
- version "2.3.8"
- resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
- integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
-
-tmp@^0.0.33:
- version "0.0.33"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
- integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
- dependencies:
- os-tmpdir "~1.0.2"
-
-tsconfig-paths@^3.11.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
- integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
+tsconfig-paths@^3.14.1:
+ version "3.14.1"
+ resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
+ integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==
dependencies:
"@types/json5" "^0.0.29"
json5 "^1.0.1"
- minimist "^1.2.0"
+ minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@^1.9.0:
- version "1.14.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
- integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-type-check@~0.3.2:
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
- integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+type-check@^0.4.0, type-check@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+ integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
dependencies:
- prelude-ls "~1.1.2"
+ prelude-ls "^1.2.1"
-type-fest@^0.21.3:
- version "0.21.3"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
- integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+type-fest@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+ integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
-type-fest@^0.8.1:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
- integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
-unbox-primitive@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
- integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+unbox-primitive@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+ integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
dependencies:
- function-bind "^1.1.1"
- has-bigints "^1.0.1"
- has-symbols "^1.0.2"
+ call-bind "^1.0.2"
+ has-bigints "^1.0.2"
+ has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
uri-js@^4.2.2:
@@ -1521,14 +1447,6 @@
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
-validate-npm-package-license@^3.0.1:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
- integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
- dependencies:
- spdx-correct "^3.0.0"
- spdx-expression-parse "^3.0.0"
-
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -1540,14 +1458,14 @@
is-string "^1.0.5"
is-symbol "^1.0.3"
-which@^1.2.9:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
- integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
-word-wrap@~1.2.3:
+word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
@@ -1557,9 +1475,7 @@
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-write@1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
- integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
- dependencies:
- mkdirp "^0.5.1"
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==