Merge "Bump gson version to 2.9.0"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ace6995..1a535ef 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5273,6 +5273,18 @@
+
By default, true.
+[[tracing.exportPerformanceMetrics]]tracing.exportPerformanceMetrics::
++
+Whether to export performance metrics.
++
+Performace logged when link:#tracing.performanceLogging[`performanceLogging`] is
+enabled, can be exported as metrics.
++
+NOTE: Since the payload returned could be of tens of thousands metrics,
+assess the latency of the metrics endpoint before enabling this option.
++
+By default, false.
+
[[tracing.traceid]]
==== Subsection tracing.<trace-id>
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 55aa9bd..c62b626 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -304,25 +304,17 @@
Matches votes that are equal to the minimal or maximal voting range. Or any votes.
-==== approverin:link:rest-api-groups.html#group-id[\{group-id\}]
+[[approverin]]
+==== approverin:link:#group-id[\{group-id\}]
Matches votes granted by a user who is a member of
-link:rest-api-groups.html#group-id[\{group-id\}].
+link:#group-id[\{group-id\}].
-Avoid using a group name with spaces (if it has spaces, use the group uuid).
-Although supported for convenience, it's better to use group uuid than group
-name since using names only works as long as the names are unique (and future
-groups with the same name will break the query).
-
-==== uploaderin:link:rest-api-groups.html#group-id[\{group-id\}]
+[[uploaderin]]
+==== uploaderin:link:#group-id[\{group-id\}]
Matches votes where the new patch set was uploaded by a member of
-link:rest-api-groups.html#group-id[\{group-id\}].
-
-Avoid using a group name with spaces (if it has spaces, use the group uuid).
-Although supported for convenience, it's better to use group uuid than group
-name since using names only works as long as the names are unique (and future
-groups with the same name will break the query).
+link:#group-id[\{group-id\}].
==== has:unchanged-files
@@ -330,6 +322,29 @@
Only 'unchanged-files' is supported for 'has'.
+[[group-id]]
+==== Group ID
+
+Some predicates (link:#approverin[approverin], link:#uploaderin[uploaderin])
+expect a group ID as value. This group ID can be any of the
+link:rest-api-groups.html#group-id[group identifiers] that are supported in the
+REST API: group UUID, group ID (for Gerrit internal groups only) and group name
+
+It's preferred to reference groups by UUID, rather than name. Referencing
+groups by name is not recommended because:
+
+* Groups may be renamed and then the group reference can no longer be resolved.
+ If this happens another group with different members can take over the group
+ name, so that exemptions which have been granted by this predicate apply to
+ the other group. This is a security concern.
+* Group names that contain spaces are not supported.
+* Ambiguous group names cannot be resolved. This means if another group with
+ the same name gets created at a later point in time, the group name can no
+ longer be resolved and the predicate breaks.
+
+Using the group UUID has a small drawback though, since it makes the condition
+less human-readable.
+
==== Example
----
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index bf4b2e4..59f9c83 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -327,7 +327,6 @@
* git-protocol-v2
* git-upload-archive
* notedb
-* no_rbe
* pgm
* rest
* server
@@ -609,6 +608,40 @@
--disk-size=200
```
+Due to outdated Git version in official RBE docker images, a custom RBE docker
+image must be used. To build custom docker imager, change to the directory
+`tools/platforms` and build and publish custom RBE docker image.
+
+To build the custom RBE docker image, run:
+
+```
+docker build -t gcr.io/api-project-164060093628/rbe-ubuntu18-04 .
+```
+
+To publish the custom RBE docker image, run:
+
+```
+docker push gcr.io/api-project-164060093628/rbe-ubuntu18-04
+[...]
+latest: digest: sha256:de5186d4313630a6111f9a2449b72563d0bc59ec9fb60956f063b69a38a76834 size: 1584
+```
+
+Re-build rbe_autoconfig project conduct a new release and switch to using it
+in `WORKSPACE` file.
+
+Note, to authenticate to the gcr.io registry, the following command must be
+used:
+
+```
+gcloud auth configure-docker
+```
+
+To see the documentation, developer must be added to this group:
+https://groups.google.com/forum/#!forum/rbe-alpha-customers.
+
+Documentation can be found at:
+https://cloud.google.com/remote-build-execution/docs.
+
To use RBE, execute
```
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index e43e021..f0fe1e5 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -378,6 +378,12 @@
that they are vetted long enough before they go into a release and we can be sure
that the update doesn't introduce a regression.
+[[escalation-channel-to-google]]
+== Escalation channel to Google
+
+If anything urgent is blocking that requires the attention of a Googler you may
+escalate this by writing an email to Han-Wen Nienhuys: hanwen@google.com
+
[[deprecating-features]]
== Deprecating features
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index d8b6250..9df4b04 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -93,6 +93,9 @@
** `plugin`:
The name of the plugin that performed the operation.
+Performance metrics can be enabled via the
+link:config.gerrit.html#tracing.exportPerformanceMetrics[`tracing.exportPerformanceMetrics`]
+setting.
=== Pushes
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index b376d6e..0e1dfd0 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -253,5 +253,5 @@
In case of rollback from NoteDB to ReviewDB, all the meta refs and the
sequence ref need to be removed.
-The [remove-notedb-refs.sh,role=external,window=_blank](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/contrib/remove-notedb-refs.sh)
+The link:https://gerrit.googlesource.com/gerrit/+/refs/heads/master/contrib/remove-notedb-refs.sh[remove-notedb-refs.sh,role=external,window=_blank]
script has been written to automate this process.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 82a9638..a9b31b0 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -7114,7 +7114,11 @@
|==========================
|Field Name ||Description
|`name` ||The name of the file.
-|`content_type`||The content type of the file.
+|`content_type`||The content type of the file. For the commit message and merge
+list the value is `text/x-gerrit-commit-message` and `text/x-gerrit-merge-list`
+respectively. For git links the value is `x-git/gitlink`. For symlinks the value
+is `x-git/symlink`. For regular files the value is the file mime type (e.g.
+`text/x-java`, `text/x-c++src`, etc...).
|`lines` ||The total number of lines in the file.
|`web_links` |optional|
Links to the file in external sites as a list of
@@ -8286,13 +8290,16 @@
submit requirement did not define an applicability expression.
Note that fields `expression`, `passing_atoms` and `failing_atoms` are always
omitted for the `applicability_expression_result`.
-|`submittability_expression_result`||
+|`submittability_expression_result`|optional|
A link:#submit-requirement-expression-info[SubmitRequirementExpressionInfo]
-containing the result of evaluating the submittability expression.
+containing the result of evaluating the submittability expression. +
+If the submit requirement does not apply, the expression is not evaluated and
+the field is not set.
|`override_expression_result`|optional|
A link:#submit-requirement-expression-info[SubmitRequirementExpressionInfo]
-containing the result of evaluating the override expression. Not set if the
-submit requirement did not define an override expression.
+containing the result of evaluating the override expression. +
+Not set if the submit requirement did not define an override expression or
+if it does not apply.
|===========================
[[submitted-together-info]]
diff --git a/WORKSPACE b/WORKSPACE
index 67bc7094..84ae4f8 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -35,11 +35,11 @@
http_archive(
name = "rbe_jdk11",
- sha256 = "766796de71916118e528b9f4334c29c9c9b4e926227bf3264dee555e6a4306c8",
- strip_prefix = "rbe_autoconfig-2.0.0",
+ sha256 = "5939e2a4e56d1fc53b6c44c6db97ee068c9f4bd18e86c762f6ab8b4fff5e294b",
+ strip_prefix = "rbe_autoconfig-3.0.0",
urls = [
- "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v2.0.0.tar.gz",
- "https://github.com/davido/rbe_autoconfig/archive/v2.0.0.tar.gz",
+ "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v3.0.0.tar.gz",
+ "https://github.com/davido/rbe_autoconfig/archive/v3.0.0.tar.gz",
],
)
diff --git a/java/com/google/gerrit/acceptance/TestOnStoreSubmitRequirementResultModifier.java b/java/com/google/gerrit/acceptance/TestOnStoreSubmitRequirementResultModifier.java
new file mode 100644
index 0000000..63e130a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/TestOnStoreSubmitRequirementResultModifier.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.acceptance;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.project.OnStoreSubmitRequirementResultModifier;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.ChangeContext;
+import java.util.Optional;
+
+/** Implementation of {@link OnStoreSubmitRequirementResultModifier} that is used in tests. */
+public class TestOnStoreSubmitRequirementResultModifier
+ implements OnStoreSubmitRequirementResultModifier {
+
+ private ModificationStrategy modificationStrategy = ModificationStrategy.KEEP;
+
+ /**
+ * The strategy, used by this modifier to transform {@link SubmitRequirementResult} on {@link
+ * OnStoreSubmitRequirementResultModifier#modifyResultOnStore} invocations.
+ */
+ public enum ModificationStrategy {
+ KEEP,
+ FAIL,
+ PASS,
+ OVERRIDE
+ }
+
+ public void setModificationStrategy(ModificationStrategy modificationStrategy) {
+ this.modificationStrategy = modificationStrategy;
+ }
+
+ @Override
+ public SubmitRequirementResult modifyResultOnStore(
+ SubmitRequirement submitRequirement,
+ SubmitRequirementResult result,
+ ChangeData cd,
+ ChangeContext ctx) {
+ if (modificationStrategy.equals(ModificationStrategy.KEEP)) {
+ return result;
+ }
+ if (modificationStrategy.equals(ModificationStrategy.OVERRIDE)) {
+ return result
+ .toBuilder()
+ .overrideExpressionResult(
+ Optional.of(
+ SubmitRequirementExpressionResult.create(
+ submitRequirement.submittabilityExpression(),
+ SubmitRequirementExpressionResult.Status.PASS,
+ ImmutableList.of(),
+ ImmutableList.of(
+ submitRequirement.submittabilityExpression().expressionString()))))
+ .build();
+ }
+ return result
+ .toBuilder()
+ .submittabilityExpressionResult(
+ SubmitRequirementExpressionResult.create(
+ submitRequirement.submittabilityExpression(),
+ modificationStrategy.equals(ModificationStrategy.FAIL)
+ ? SubmitRequirementExpressionResult.Status.FAIL
+ : SubmitRequirementExpressionResult.Status.PASS,
+ ImmutableList.of(),
+ ImmutableList.of(submitRequirement.submittabilityExpression().expressionString())))
+ .build();
+ }
+}
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 73b1d40..3e103c8 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -42,6 +42,7 @@
PLUGIN_SERVICEUSER,
PLUGIN_HIGH_AVAILABILITY,
PLUGIN_MULTI_SITE,
+ PLUGIN_WEBSESSION_FLATFILE,
PLUGINS_ALL, // Use this project if a method/type is generally made available to all plugins.
}
diff --git a/java/com/google/gerrit/common/data/FilenameComparator.java b/java/com/google/gerrit/common/data/FilenameComparator.java
index ebf423c..0b188df 100644
--- a/java/com/google/gerrit/common/data/FilenameComparator.java
+++ b/java/com/google/gerrit/common/data/FilenameComparator.java
@@ -24,7 +24,7 @@
public static final FilenameComparator INSTANCE = new FilenameComparator();
private static final Set<String> cppHeaderSuffixes =
- new HashSet<>(Arrays.asList(".h", ".hxx", ".hpp"));
+ new HashSet<>(Arrays.asList(".h", ".hh", ".hxx", ".hpp"));
private FilenameComparator() {}
diff --git a/java/com/google/gerrit/entities/SubmitRequirement.java b/java/com/google/gerrit/entities/SubmitRequirement.java
index 3f91cc7..673665d 100644
--- a/java/com/google/gerrit/entities/SubmitRequirement.java
+++ b/java/com/google/gerrit/entities/SubmitRequirement.java
@@ -44,7 +44,7 @@
* Expression of the condition that makes the requirement applicable. The expression should be
* evaluated for a specific {@link Change} and if it returns false, the requirement becomes
* irrelevant for the change (i.e. {@link #submittabilityExpression()} and {@link
- * #overrideExpression()} become irrelevant).
+ * #overrideExpression()} are not evaluated).
*
* <p>An empty {@link Optional} indicates that the requirement is applicable for any change.
*/
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
index a97560b..18c27a1 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -32,12 +32,18 @@
public abstract Optional<SubmitRequirementExpressionResult> applicabilityExpressionResult();
/**
- * Result of evaluating a {@link SubmitRequirement#submittabilityExpression()} ()} on a change.
+ * Result of evaluating a {@link SubmitRequirement#submittabilityExpression()} on a change.
+ *
+ * <p>Empty if submit requirement does not apply.
*/
- @Nullable
- public abstract SubmitRequirementExpressionResult submittabilityExpressionResult();
+ public abstract Optional<SubmitRequirementExpressionResult> submittabilityExpressionResult();
- /** Result of evaluating a {@link SubmitRequirement#overrideExpression()} ()} on a change. */
+ /**
+ * Result of evaluating a {@link SubmitRequirement#overrideExpression()} on a change.
+ *
+ * <p>Empty if submit requirement does not apply, or if the submit requirement did not define an
+ * override expression.
+ */
public abstract Optional<SubmitRequirementExpressionResult> overrideExpressionResult();
/** SHA-1 of the patchset commit ID for which the submit requirement was evaluated. */
@@ -71,11 +77,11 @@
"Applicability expression result has an error: "
+ applicabilityExpressionResult().get().errorMessage().get());
}
- if (submittabilityExpressionResult() != null
- && submittabilityExpressionResult().errorMessage().isPresent()) {
+ if (submittabilityExpressionResult().isPresent()
+ && submittabilityExpressionResult().get().errorMessage().isPresent()) {
return Optional.of(
"Submittability expression result has an error: "
- + submittabilityExpressionResult().errorMessage().get());
+ + submittabilityExpressionResult().get().errorMessage().get());
}
if (overrideExpressionResult().isPresent()
&& overrideExpressionResult().get().errorMessage().isPresent()) {
@@ -168,6 +174,9 @@
Optional<SubmitRequirementExpressionResult> value);
public abstract Builder submittabilityExpressionResult(
+ Optional<SubmitRequirementExpressionResult> value);
+
+ public abstract Builder submittabilityExpressionResult(
@Nullable SubmitRequirementExpressionResult value);
public abstract Builder overrideExpressionResult(
@@ -182,33 +191,25 @@
public abstract SubmitRequirementResult build();
}
- private boolean assertPass(Optional<SubmitRequirementExpressionResult> expressionResult) {
+ public static boolean assertPass(Optional<SubmitRequirementExpressionResult> expressionResult) {
return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.PASS);
}
- private boolean assertPass(@Nullable SubmitRequirementExpressionResult expressionResult) {
- return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.PASS);
- }
-
- private boolean assertFail(Optional<SubmitRequirementExpressionResult> expressionResult) {
+ public static boolean assertFail(Optional<SubmitRequirementExpressionResult> expressionResult) {
return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.FAIL);
}
- private boolean assertError(Optional<SubmitRequirementExpressionResult> expressionResult) {
+ public static boolean assertError(Optional<SubmitRequirementExpressionResult> expressionResult) {
return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.ERROR);
}
- private boolean assertError(@Nullable SubmitRequirementExpressionResult expressionResult) {
- return assertStatus(expressionResult, SubmitRequirementExpressionResult.Status.ERROR);
- }
-
- private boolean assertStatus(
- @Nullable SubmitRequirementExpressionResult expressionResult,
+ private static boolean assertStatus(
+ SubmitRequirementExpressionResult expressionResult,
SubmitRequirementExpressionResult.Status status) {
- return expressionResult != null && expressionResult.status() == status;
+ return expressionResult.status() == status;
}
- private boolean assertStatus(
+ private static boolean assertStatus(
Optional<SubmitRequirementExpressionResult> expressionResult,
SubmitRequirementExpressionResult.Status status) {
return expressionResult.isPresent() && assertStatus(expressionResult.get(), status);
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
index 7b87be8..4778038 100644
--- a/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.common;
+import com.google.gerrit.common.Nullable;
+
/** Result of evaluating a submit requirement on a change. */
public class SubmitRequirementResultInfo {
public enum Status {
@@ -63,11 +65,11 @@
public boolean isLegacy;
/** Result of evaluating the applicability expression. */
- public SubmitRequirementExpressionInfo applicabilityExpressionResult;
+ @Nullable public SubmitRequirementExpressionInfo applicabilityExpressionResult;
/** Result of evaluating the submittability expression. */
- public SubmitRequirementExpressionInfo submittabilityExpressionResult;
+ @Nullable public SubmitRequirementExpressionInfo submittabilityExpressionResult;
/** Result of evaluating the override expression. */
- public SubmitRequirementExpressionInfo overrideExpressionResult;
+ @Nullable public SubmitRequirementExpressionInfo overrideExpressionResult;
}
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 92e16ce..5b62f96 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -19,6 +19,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.httpd.restapi.ParameterParser;
@@ -40,7 +41,9 @@
public abstract class CacheBasedWebSession extends WebSession {
@VisibleForTesting public static final String ACCOUNT_COOKIE = "GerritAccount";
- protected static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
+
+ @UsedAt(UsedAt.Project.PLUGIN_WEBSESSION_FLATFILE)
+ public static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
private final HttpServletRequest request;
private final HttpServletResponse response;
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index ad6d7fb..2efa027 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -80,7 +80,7 @@
u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
u.addOp(
change.getId(),
- storeSubmitRequirementsOpFactory.create(change.submitRequirements().values()));
+ storeSubmitRequirementsOpFactory.create(change.submitRequirements().values(), change));
}
u.execute();
diff --git a/java/com/google/gerrit/server/change/SubmitRequirementsJson.java b/java/com/google/gerrit/server/change/SubmitRequirementsJson.java
index 7793b76..96c863e 100644
--- a/java/com/google/gerrit/server/change/SubmitRequirementsJson.java
+++ b/java/com/google/gerrit/server/change/SubmitRequirementsJson.java
@@ -47,11 +47,11 @@
result.overrideExpressionResult().get(),
/* hide= */ false);
}
- if (result.submittabilityExpressionResult() != null) {
+ if (result.submittabilityExpressionResult().isPresent()) {
info.submittabilityExpressionResult =
submitRequirementExpressionToInfo(
req.submittabilityExpression(),
- result.submittabilityExpressionResult(),
+ result.submittabilityExpressionResult().get(),
/* hide= */ false);
}
info.status = SubmitRequirementResultInfo.Status.valueOf(result.status().toString());
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 7d3ff12..7c0b85a 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -438,7 +438,9 @@
DynamicSet.setOf(binder(), SubmitRequirement.class);
DynamicSet.setOf(binder(), QuotaEnforcer.class);
DynamicSet.setOf(binder(), PerformanceLogger.class);
- DynamicSet.bind(binder(), PerformanceLogger.class).to(PerformanceMetrics.class);
+ if (cfg.getBoolean("tracing", "exportPerformanceMetrics", false)) {
+ DynamicSet.bind(binder(), PerformanceLogger.class).to(PerformanceMetrics.class);
+ }
DynamicSet.setOf(binder(), RequestListener.class);
DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
DynamicSet.setOf(binder(), ChangeETagComputation.class);
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 197183f..e7e0e8f 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -437,8 +437,8 @@
case TRIVIAL_REBASE:
return ": Patch Set " + priorPatchSetId.get() + " was rebased.";
case NO_CHANGE:
- return ": New patch set was added with same tree, parent"
- + (commit.getParentCount() != 1 ? "s" : "")
+ return ": New patch set was added with same tree, parent "
+ + (commit.getParentCount() != 1 ? "trees" : "tree")
+ ", and commit message as Patch Set "
+ priorPatchSetId.get()
+ ".";
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
index 8b3ab5a..37a38fe 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
@@ -35,6 +35,17 @@
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
+/**
+ * Provides {@link Gson} to parse {@link ChangeRevisionNote}, attached to the change update.
+ *
+ * <p>Apart from the adapters for the custom JSON format, this class also registers adapters that
+ * support forward/backward compatibility when modifying {@link ChangeNotes} storage format.
+ *
+ * <p>NOTE: All changes to the storage format must be both forward and backward compatible, see
+ * comment on {@link ChangeNotesParser}.
+ *
+ * <p>For JSON, such changes include e.g. modifications to the serialized {@code AutoValue} classes.
+ */
@Singleton
public class ChangeNoteJson {
private final Gson gson = newGson();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 0d59542..1d8ec82 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -110,6 +110,31 @@
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.util.RawParseUtils;
+/**
+ * Parses {@link ChangeNotesState} out of the change meta ref.
+ *
+ * <p>NOTE: all changes to the change notes storage format must be both forward and backward
+ * compatible, i.e.:
+ *
+ * <ul>
+ * <li>The server, running the new binary version must be able to parse the data, written by the
+ * previous binary version.
+ * <li>The server, running the old binary version must be able to parse the data, written by the
+ * new binary version.
+ * </ul>
+ *
+ * <p>Thus, when introducing storage format update, the following procedure must be used:
+ *
+ * <ol>
+ * <li>The read path ({@link ChangeNotesParser}) needs to be updated to handle both the old and
+ * the new data format.
+ * <li>In a separate change, the write path (e.g. {@link ChangeUpdate}, {@link ChangeNoteJson}) is
+ * updated to write the new format, guarded by {@link
+ * com.google.gerrit.server.experiments.ExperimentFeatures} flag, if possible.
+ * <li>Once the 'read' change is roll out and is roll back safe, the 'write' change can be
+ * submitted/the experiment flag can be flipped.
+ * </ol>
+ */
class ChangeNotesParser {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 3d7ae10..a4623d6 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -118,6 +118,13 @@
* there is a single author and timestamp for each update.
*
* <p>This class is not thread-safe.
+ *
+ * <p>NOTE: This class also serializes the change in a custom storage format, used in NoteDB. All
+ * changes to the storage format must be both forward and backward compatible, see comment on {@link
+ * ChangeNotesParser}.
+ *
+ * <p>Such changes include e.g. introducing/removing footers, modifying footer formats, mutations of
+ * the attached {@link ChangeRevisionNote}.
*/
public class ChangeUpdate extends AbstractChangeUpdate {
public interface Factory {
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
index 6d5f4be..8957407 100644
--- a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 The Android Open Source Project
+// 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.
@@ -17,6 +17,8 @@
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.server.project.OnStoreSubmitRequirementResultModifier;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.inject.Inject;
@@ -29,16 +31,32 @@
public class StoreSubmitRequirementsOp implements BatchUpdateOp {
private final boolean storeRequirementsInNoteDb;
private final Collection<SubmitRequirementResult> submitRequirementResults;
+ private final ChangeData changeData;
+ private final OnStoreSubmitRequirementResultModifier onStoreSubmitRequirementResultModifier;
public interface Factory {
- StoreSubmitRequirementsOp create(Collection<SubmitRequirementResult> submitRequirements);
+
+ /**
+ * {@code submitRequirements} are explicitly passed to the operation so that they are evaluated
+ * before the {@link #updateChange} is called.
+ *
+ * <p>This is because the return results of {@link ChangeData#submitRequirements()} depend on
+ * the status of the change, which can be modified by other {@link BatchUpdateOp}, sharing the
+ * same {@link ChangeContext}.
+ */
+ StoreSubmitRequirementsOp create(
+ Collection<SubmitRequirementResult> submitRequirements, ChangeData changeData);
}
@Inject
public StoreSubmitRequirementsOp(
ExperimentFeatures experimentFeatures,
- @Assisted Collection<SubmitRequirementResult> submitRequirementResults) {
+ OnStoreSubmitRequirementResultModifier onStoreSubmitRequirementResultModifier,
+ @Assisted Collection<SubmitRequirementResult> submitRequirementResults,
+ @Assisted ChangeData changeData) {
+ this.onStoreSubmitRequirementResultModifier = onStoreSubmitRequirementResultModifier;
this.submitRequirementResults = submitRequirementResults;
+ this.changeData = changeData;
this.storeRequirementsInNoteDb =
experimentFeatures.isFeatureEnabled(
ExperimentFeaturesConstants
@@ -59,8 +77,13 @@
// from NoteDb and convert them to submit requirement results. See
// ChangeData#submitRequirements().
.filter(srResult -> !srResult.isLegacy())
+ .map(
+ // Pass to OnStoreSubmitRequirementResultModifier for override
+ srResult ->
+ onStoreSubmitRequirementResultModifier.modifyResultOnStore(
+ srResult.submitRequirement(), srResult, changeData, ctx))
.collect(Collectors.toList());
update.putSubmitRequirementResults(nonLegacySubmitRequirements);
- return !submitRequirementResults.isEmpty();
+ return !nonLegacySubmitRequirements.isEmpty();
}
}
diff --git a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
index e655d92..a815f57 100644
--- a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
+++ b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
@@ -58,10 +58,10 @@
SubmitRequirementExpressionResultSerializer.serialize(
r.applicabilityExpressionResult().get()));
}
- if (r.submittabilityExpressionResult() != null) {
+ if (r.submittabilityExpressionResult().isPresent()) {
builder.setSubmittabilityExpressionResult(
SubmitRequirementExpressionResultSerializer.serialize(
- r.submittabilityExpressionResult()));
+ r.submittabilityExpressionResult().get()));
}
if (r.overrideExpressionResult().isPresent()) {
builder.setOverrideExpressionResult(
diff --git a/java/com/google/gerrit/server/project/CreateRefControl.java b/java/com/google/gerrit/server/project/CreateRefControl.java
index 69ac93e..2e76949 100644
--- a/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -105,7 +105,7 @@
// If the tag has a PGP signature, allow a lower level of permission
// than if it doesn't have a PGP signature.
PermissionBackend.ForRef forRef = permissionBackend.user(user.get()).ref(branch);
- if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
+ if (tag.getRawGpgSignature() != null) {
forRef.check(RefPermission.CREATE_SIGNED_TAG);
} else {
forRef.check(RefPermission.CREATE_TAG);
diff --git a/java/com/google/gerrit/server/project/OnStoreSubmitRequirementResultModifier.java b/java/com/google/gerrit/server/project/OnStoreSubmitRequirementResultModifier.java
new file mode 100644
index 0000000..7e6d93e
--- /dev/null
+++ b/java/com/google/gerrit/server/project/OnStoreSubmitRequirementResultModifier.java
@@ -0,0 +1,53 @@
+// 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.server.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Extension point that allows to modify the {@link SubmitRequirementResult} when it is stored
+ * NoteDB.
+ *
+ * <p>The submit requirements are only stored for the closed (merged and abandoned) changes and the
+ * modifier only affects what {@link SubmitRequirementResult} will be stored in NoteDB.
+ *
+ * <p>It has no impact on open changes or evaluations on merge, i.e. does not affect the
+ * submittability of the change (never blocks the ready change from submission or allows bypassing
+ * unsatisfied submit requirement).
+ *
+ * <p>The extension point only applies to non-legacy submit requirements (including non-applicable,
+ * since they are stored too) and does not affect submit rule results.
+ */
+@ExtensionPoint
+@ImplementedBy(OnStoreSubmitRequirementResultModifierImpl.class)
+public interface OnStoreSubmitRequirementResultModifier {
+
+ /**
+ * Evaluate a single {@link SubmitRequirement} using the change data, modifying the original
+ * {@code SubmitRequirementResult}, if needed.
+ *
+ * <p>Only affects how the submit requirement is stored in NoteDb for closed changes.
+ */
+ SubmitRequirementResult modifyResultOnStore(
+ SubmitRequirement submitRequirement,
+ SubmitRequirementResult submitRequirementResult,
+ ChangeData changeData,
+ ChangeContext ctx);
+}
diff --git a/java/com/google/gerrit/server/project/OnStoreSubmitRequirementResultModifierImpl.java b/java/com/google/gerrit/server/project/OnStoreSubmitRequirementResultModifierImpl.java
new file mode 100644
index 0000000..39a717f
--- /dev/null
+++ b/java/com/google/gerrit/server/project/OnStoreSubmitRequirementResultModifierImpl.java
@@ -0,0 +1,35 @@
+// 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.server.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.ChangeContext;
+
+/**
+ * Default implementation of {@link OnStoreSubmitRequirementResultModifier} that does not
+ * re-evaluate {@link SubmitRequirementResult}.
+ */
+public class OnStoreSubmitRequirementResultModifierImpl
+ implements OnStoreSubmitRequirementResultModifier {
+ @Override
+ public SubmitRequirementResult modifyResultOnStore(
+ SubmitRequirement submitRequirement,
+ SubmitRequirementResult result,
+ ChangeData cd,
+ ChangeContext ctx) {
+ return result;
+ }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 00f6876..3887604a 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -100,24 +100,27 @@
// Use a request context to execute predicates as an internal user with expanded visibility.
// This is so that the evaluation does not depend on who is running the current request (e.g.
// a "ownerin" predicate with group that is not visible to the person making this request).
- SubmitRequirementExpressionResult blockingResult =
- evaluateExpression(sr.submittabilityExpression(), cd);
Optional<SubmitRequirementExpressionResult> applicabilityResult =
sr.applicabilityExpression().isPresent()
? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
: Optional.empty();
-
- Optional<SubmitRequirementExpressionResult> overrideResult =
- sr.overrideExpression().isPresent()
- ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
- : Optional.empty();
+ Optional<SubmitRequirementExpressionResult> submittabilityResult = Optional.empty();
+ Optional<SubmitRequirementExpressionResult> overrideResult = Optional.empty();
+ if (!sr.applicabilityExpression().isPresent()
+ || SubmitRequirementResult.assertPass(applicabilityResult)) {
+ submittabilityResult = Optional.of(evaluateExpression(sr.submittabilityExpression(), cd));
+ overrideResult =
+ sr.overrideExpression().isPresent()
+ ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
+ : Optional.empty();
+ }
return SubmitRequirementResult.builder()
.legacy(Optional.of(false))
.submitRequirement(sr)
.patchSetCommitId(cd.currentPatchSet().commitId())
- .submittabilityExpressionResult(blockingResult)
+ .submittabilityExpressionResult(submittabilityResult)
.applicabilityExpressionResult(applicabilityResult)
.overrideExpressionResult(overrideResult)
.build();
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 98514b2..8dd0e78 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -131,7 +131,8 @@
u.addOp(notes.getChangeId(), op);
u.addOp(
notes.getChangeId(),
- storeSubmitRequirementsOpFactory.create(changeData.submitRequirements().values()));
+ storeSubmitRequirementsOpFactory.create(
+ changeData.submitRequirements().values(), changeData));
u.execute();
}
return op.getChange();
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 0160fc9..a243228 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -647,7 +647,7 @@
.get(project)
.addOp(
changeId,
- storeSubmitRequirementsOpFactory.create(cd.submitRequirements().values()));
+ storeSubmitRequirementsOpFactory.create(cd.submitRequirements().values(), cd));
}
try {
submissionExecutor.setAdditionalBatchUpdateListeners(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
index cb1b7b1..ff76546 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementIT.java
@@ -22,6 +22,7 @@
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@@ -1081,9 +1082,9 @@
SubmitRequirementResult result =
notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
- assertThat(result.submittabilityExpressionResult().status())
+ assertThat(result.submittabilityExpressionResult().get().status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
- assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ assertThat(result.submittabilityExpressionResult().get().expression().expressionString())
.isEqualTo("label:Code-Review=MAX");
}
}
@@ -1121,9 +1122,9 @@
SubmitRequirementResult result =
notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
- assertThat(result.submittabilityExpressionResult().status())
+ assertThat(result.submittabilityExpressionResult().get().status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
- assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ assertThat(result.submittabilityExpressionResult().get().expression().expressionString())
.isEqualTo("label:Code-Review=MAX");
}
}
@@ -1815,6 +1816,120 @@
/* passingAtoms= */ null,
/* failingAtoms= */ null,
/* fulfilled= */ true);
+ assertThat(requirement.submittabilityExpressionResult).isNotNull();
+ }
+
+ @Test
+ public void submitRequirement_nonApplicable_submittabilityAndOverrideNotEvaluated()
+ throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setApplicabilityExpression(
+ SubmitRequirementExpression.of("branch:refs/heads/non-existent"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
+ .setOverrideExpression(SubmitRequirementExpression.of("project:" + project.get()))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 2);
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
+ SubmitRequirementResultInfo requirement =
+ changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement());
+ assertSubmitRequirementExpression(
+ requirement.applicabilityExpressionResult,
+ /* expression= */ null,
+ /* passingAtoms= */ null,
+ /* failingAtoms= */ null,
+ /* fulfilled= */ false);
+ assertThat(requirement.submittabilityExpressionResult).isNull();
+ assertThat(requirement.overrideExpressionResult).isNull();
+ }
+
+ @Test
+ public void submitRequirement_emptyApplicable_submittabilityAndOverrideEvaluated()
+ throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setApplicabilityExpression(Optional.empty())
+ .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
+ .setOverrideExpression(SubmitRequirementExpression.of("project:non-existent"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 2);
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ SubmitRequirementResultInfo requirement =
+ changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement());
+ assertThat(requirement.applicabilityExpressionResult).isNull();
+ assertSubmitRequirementExpression(
+ requirement.submittabilityExpressionResult,
+ /* expression= */ SubmitRequirementExpression.maxCodeReview().expressionString(),
+ /* passingAtoms= */ ImmutableList.of(
+ SubmitRequirementExpression.maxCodeReview().expressionString()),
+ /* failingAtoms= */ ImmutableList.of(),
+ /* fulfilled= */ true);
+ assertSubmitRequirementExpression(
+ requirement.overrideExpressionResult,
+ /* expression= */ "project:non-existent",
+ /* passingAtoms= */ ImmutableList.of(),
+ /* failingAtoms= */ ImmutableList.of("project:non-existent"),
+ /* fulfilled= */ false);
+ }
+
+ @Test
+ public void submitRequirement_overriden_submittabilityEvaluated() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setApplicabilityExpression(Optional.empty())
+ .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
+ .setOverrideExpression(SubmitRequirementExpression.of("project:" + project.get()))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 1);
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
+ SubmitRequirementResultInfo requirement =
+ changeInfo.submitRequirements.stream()
+ .filter(sr -> !sr.isLegacy)
+ .collect(MoreCollectors.onlyElement());
+ assertThat(requirement.applicabilityExpressionResult).isNull();
+ assertSubmitRequirementExpression(
+ requirement.submittabilityExpressionResult,
+ /* expression= */ SubmitRequirementExpression.maxCodeReview().expressionString(),
+ /* passingAtoms= */ ImmutableList.of(),
+ /* failingAtoms= */ ImmutableList.of(
+ SubmitRequirementExpression.maxCodeReview().expressionString()),
+ /* fulfilled= */ false);
+ assertSubmitRequirementExpression(
+ requirement.overrideExpressionResult,
+ /* expression= */ "project:" + project.get(),
+ /* passingAtoms= */ ImmutableList.of("project:" + project.get()),
+ /* failingAtoms= */ ImmutableList.of(),
+ /* fulfilled= */ true);
}
@Test
@@ -2132,9 +2247,9 @@
SubmitRequirementResult result =
notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
- assertThat(result.submittabilityExpressionResult().status())
+ assertThat(result.submittabilityExpressionResult().get().status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
- assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ assertThat(result.submittabilityExpressionResult().get().expression().expressionString())
.isEqualTo("topic:test");
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/OnStoreSubmitRequirementResultModifierIT.java b/javatests/com/google/gerrit/acceptance/server/project/OnStoreSubmitRequirementResultModifierIT.java
new file mode 100644
index 0000000..ae70832
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/project/OnStoreSubmitRequirementResultModifierIT.java
@@ -0,0 +1,258 @@
+// 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.acceptance.server.project;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestOnStoreSubmitRequirementResultModifier;
+import com.google.gerrit.acceptance.TestOnStoreSubmitRequirementResultModifier.ModificationStrategy;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.change.TestSubmitInput;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.server.project.OnStoreSubmitRequirementResultModifier;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link OnStoreSubmitRequirementResultModifier} on the closed changes. */
+@NoHttpd
+public class OnStoreSubmitRequirementResultModifierIT extends AbstractDaemonTest {
+
+ private TestOnStoreSubmitRequirementResultModifier testOnStoreSrModifier;
+
+ @Override
+ public Module createModule() {
+ testOnStoreSrModifier = new TestOnStoreSubmitRequirementResultModifier();
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(OnStoreSubmitRequirementResultModifier.class).toInstance(testOnStoreSrModifier);
+ }
+ };
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.disabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirementsNotStored_overrideNoOp() throws Exception {
+
+ testOnStoreSrModifier.setModificationStrategy(ModificationStrategy.OVERRIDE);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ approve(changeId);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(changeId).current().submit();
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirementStored_canBeOverriddenForMergedChanges() throws Exception {
+ testOnStoreSrModifier.setModificationStrategy(ModificationStrategy.OVERRIDE);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ approve(changeId);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(changeId).current().submit();
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirementStored_canBeOverriddenForAbandonedChanges() throws Exception {
+ testOnStoreSrModifier.setModificationStrategy(ModificationStrategy.OVERRIDE);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ approve(changeId);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(changeId).abandon();
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void overrideToUnsatisfied_unsatisfied_doesNotBlockSubmission() throws Exception {
+ testOnStoreSrModifier.setModificationStrategy(ModificationStrategy.FAIL);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ approve(changeId);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(changeId).current().submit();
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void overrideToUnsatisfied_doesNotBlockSubmissionWithRetries() throws Exception {
+ testOnStoreSrModifier.setModificationStrategy(ModificationStrategy.FAIL);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ approve(changeId);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ TestSubmitInput input = new TestSubmitInput();
+ input.generateLockFailures = new ArrayDeque<>(ImmutableList.of(true));
+ gApi.changes().id(changeId).current().submit(input);
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void overrideToSatisfied_doesNotBypassSubmitRequirement() throws Exception {
+ testOnStoreSrModifier.setModificationStrategy(ModificationStrategy.PASS);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+ assertThrows(
+ ResourceConflictException.class, () -> gApi.changes().id(changeId).current().submit());
+ }
+
+ private void assertSubmitRequirementStatus(
+ Collection<SubmitRequirementResultInfo> results,
+ String requirementName,
+ SubmitRequirementResultInfo.Status status,
+ boolean isLegacy) {
+ assertWithMessage(
+ "Could not find submit requirement %s with status %s, legacy=%s, (results = %s)",
+ requirementName,
+ status,
+ isLegacy,
+ results.stream()
+ .map(r -> String.format("%s=%s, legacy=%s", r.name, r.status, r.isLegacy))
+ .collect(toImmutableList()))
+ .that(
+ results.stream()
+ .filter(
+ result ->
+ result.name.equals(requirementName)
+ && result.status == status
+ && result.isLegacy == isLegacy)
+ .count())
+ .isEqualTo(1);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 61e204f..43c7b6b 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -238,6 +238,68 @@
}
@Test
+ public void submittabilityAndOverrideNotEvaluated_whenApplicabilityIsFalse() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:non-existent-project",
+ /* submittabilityExpr= */ "message:\"Fix bug\"",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.NOT_APPLICABLE);
+ assertThat(result.applicabilityExpressionResult().get().status()).isEqualTo(Status.FAIL);
+ assertThat(result.submittabilityExpressionResult().isPresent()).isFalse();
+ assertThat(result.overrideExpressionResult().isPresent()).isFalse();
+ }
+
+ @Test
+ public void submittabilityAndOverrideEvaluated_whenApplicabilityIsEmpty() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ null,
+ /* submittabilityExpr= */ "message:\"Fix bug\"",
+ /* overrideExpr= */ "label:\"build-cop-override=-1\"");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(result.applicabilityExpressionResult().isPresent()).isFalse();
+ assertThat(result.submittabilityExpressionResult().get().status()).isEqualTo(Status.PASS);
+ assertThat(result.overrideExpressionResult().get().status()).isEqualTo(Status.FAIL);
+ }
+
+ @Test
+ public void submittabilityAndOverrideEvaluated_whenApplicabilityIsTrue() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "message:\"Fix bug\"",
+ /* overrideExpr= */ "label:\"build-cop-override=-1\"");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(result.applicabilityExpressionResult().get().status()).isEqualTo(Status.PASS);
+ assertThat(result.submittabilityExpressionResult().get().status()).isEqualTo(Status.PASS);
+ assertThat(result.overrideExpressionResult().get().status()).isEqualTo(Status.FAIL);
+ }
+
+ @Test
+ public void submittabilityIsEvaluated_whenOverrideApplies() throws Exception {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ null,
+ /* submittabilityExpr= */ "message:\"Fix bug\"",
+ /* overrideExpr= */ "project:" + project.get());
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.OVERRIDDEN);
+
+ assertThat(result.applicabilityExpressionResult().isPresent()).isFalse();
+ assertThat(result.submittabilityExpressionResult().get().status()).isEqualTo(Status.PASS);
+ assertThat(result.overrideExpressionResult().get().status()).isEqualTo(Status.PASS);
+ }
+
+ @Test
public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsTrue() throws Exception {
SubmitRequirement sr =
createSubmitRequirement(
@@ -260,7 +322,7 @@
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
- assertThat(result.submittabilityExpressionResult().failingAtoms())
+ assertThat(result.submittabilityExpressionResult().get().failingAtoms())
.containsExactly("label:\"Code-Review=+2\"");
}
@@ -314,7 +376,7 @@
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
- assertThat(result.submittabilityExpressionResult().errorMessage().get())
+ assertThat(result.submittabilityExpressionResult().get().errorMessage().get())
.isEqualTo("Unsupported operator invalid_field:invalid_value");
}
diff --git a/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java b/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
index ec71e05..d959c2a 100644
--- a/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
+++ b/javatests/com/google/gerrit/common/data/FilenameComparatorTest.java
@@ -45,6 +45,7 @@
@Test
public void cppExtensions() {
+ assertThat(comparator.compare("abc/file.hh", "abc/file.cc")).isLessThan(0);
assertThat(comparator.compare("abc/file.h", "abc/file.cc")).isLessThan(0);
assertThat(comparator.compare("abc/file.c", "abc/file.hpp")).isGreaterThan(0);
assertThat(comparator.compare("abc..xyz.file.h", "abc.xyz.file.cc")).isLessThan(0);
diff --git a/javatests/com/google/gerrit/git/BUILD b/javatests/com/google/gerrit/git/BUILD
index b12924a..c2c9cce 100644
--- a/javatests/com/google/gerrit/git/BUILD
+++ b/javatests/com/google/gerrit/git/BUILD
@@ -7,10 +7,7 @@
size = "medium",
timeout = "short",
srcs = MEDIUM_TESTS,
- tags = [
- "no_rbe",
- "no_windows",
- ],
+ tags = ["no_windows"],
deps = [
"//java/com/google/gerrit/git",
"//lib:guava",
diff --git a/javatests/com/google/gerrit/integration/git/BUILD b/javatests/com/google/gerrit/integration/git/BUILD
index bcb16f4..28755af 100644
--- a/javatests/com/google/gerrit/integration/git/BUILD
+++ b/javatests/com/google/gerrit/integration/git/BUILD
@@ -3,10 +3,7 @@
acceptance_tests(
srcs = ["GitProtocolV2IT.java"],
group = "protocol-v2",
- labels = [
- "git-protocol-v2",
- "no_rbe",
- ],
+ labels = ["git-protocol-v2"],
)
acceptance_tests(
diff --git a/javatests/com/google/gerrit/pgm/BUILD b/javatests/com/google/gerrit/pgm/BUILD
index 0655bb2..0fe4fad 100644
--- a/javatests/com/google/gerrit/pgm/BUILD
+++ b/javatests/com/google/gerrit/pgm/BUILD
@@ -4,7 +4,6 @@
junit_tests(
name = "pgm_tests",
srcs = glob(["**/*.java"]),
- tags = ["no_rbe"],
deps = [
"//java/com/google/gerrit/pgm/http/jetty",
"//java/com/google/gerrit/pgm/init/api",
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 87d1729..c694a87 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -27,10 +27,7 @@
),
resource_strip_prefix = "resources",
resources = ["//resources/com/google/gerrit/server"],
- tags = [
- "no_rbe",
- "no_windows",
- ],
+ tags = ["no_windows"],
visibility = ["//visibility:public"],
runtime_deps = [
"//java/com/google/gerrit/lucene",
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java
index 2418d1c..6abe4d1 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java
@@ -118,11 +118,11 @@
+ "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ "\"passingAtoms\":[\"refs/heads/master\"],"
+ "\"failingAtoms\":[]}},"
- + "\"submittabilityExpressionResult\":{"
+ + "\"submittabilityExpressionResult\":{\"value\":{"
+ "\"expression\":{\"expressionString\":\"label:\\\"Code-Review=+2\\\"\"},"
+ "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ "\"passingAtoms\":[\"label:\\\"Code-Review=+2\\\"\"],"
- + "\"failingAtoms\":[]},"
+ + "\"failingAtoms\":[]}},"
+ "\"overrideExpressionResult\":{\"value\":{"
+ "\"expression\":{\"expressionString\":\"label:Override=+1\"},"
+ "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
@@ -196,44 +196,29 @@
}
@Test
- public void submitRequirementResult_deserialize_nonOptionalSubmittabilityExpressionResultField()
+ public void submitRequirementResult_deserialize_optionalSubmittabilityExpressionResultField()
throws Exception {
assertThat(SubmitRequirementResult.typeAdapter(gson).fromJson(srReqResultSerial))
.isEqualTo(srReqResult);
}
@Test
- public void submitRequirementResult_deserialize_optionalSubmittabilityExpressionResultField()
+ public void submitRequirementResult_deserialize_nonOptionalSubmittabilityExpressionResultField()
throws Exception {
- String newFormatSrReqResultSerial =
+ String oldFormatSrReqResultSerial =
srReqResultSerial.replace(
- "\"submittabilityExpressionResult\":{"
- + "\"expression\":{\"expressionString\":\"label:\\\"Code-Review=+2\\\"\"},"
- + "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
- + "\"passingAtoms\":[\"label:\\\"Code-Review=+2\\\"\"],"
- + "\"failingAtoms\":[]},",
"\"submittabilityExpressionResult\":{\"value\":{"
+ "\"expression\":{\"expressionString\":\"label:\\\"Code-Review=+2\\\"\"},"
+ "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ "\"passingAtoms\":[\"label:\\\"Code-Review=+2\\\"\"],"
- + "\"failingAtoms\":[]}},");
- assertThat(SubmitRequirementResult.typeAdapter(gson).fromJson(newFormatSrReqResultSerial))
- .isEqualTo(srReqResult);
- }
-
- @Test
- public void submitRequirementResult_deserialize_emptyOptionalSubmittabilityExpressionResultField()
- throws Exception {
- String newFormatSrReqResultSerial =
- srReqResultSerial.replace(
+ + "\"failingAtoms\":[]}},",
"\"submittabilityExpressionResult\":{"
+ "\"expression\":{\"expressionString\":\"label:\\\"Code-Review=+2\\\"\"},"
+ "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ "\"passingAtoms\":[\"label:\\\"Code-Review=+2\\\"\"],"
- + "\"failingAtoms\":[]},",
- "\"submittabilityExpressionResult\":{\"value\":null},");
- assertThat(SubmitRequirementResult.typeAdapter(gson).fromJson(newFormatSrReqResultSerial))
- .isEqualTo(srReqResult.toBuilder().submittabilityExpressionResult(null).build());
+ + "\"failingAtoms\":[]},");
+ assertThat(SubmitRequirementResult.typeAdapter(gson).fromJson(oldFormatSrReqResultSerial))
+ .isEqualTo(srReqResult);
}
@Test
@@ -243,6 +228,20 @@
}
@Test
+ public void submitRequirementResult_emptySubmittabilityExpressionResultField_roundTrip()
+ throws Exception {
+ SubmitRequirementResult srResult =
+ srReqResult
+ .toBuilder()
+ .submittabilityExpressionResult(Optional.empty())
+ .applicabilityExpressionResult(Optional.empty())
+ .overrideExpressionResult(Optional.empty())
+ .build();
+ TypeAdapter<SubmitRequirementResult> adapter = SubmitRequirementResult.typeAdapter(gson);
+ assertThat(adapter.fromJson(adapter.toJson(srResult))).isEqualTo(srResult);
+ }
+
+ @Test
public void deserializeSubmitRequirementResult_withJGitPatchsetIdFormat() throws Exception {
String srResultSerialJgitFormat =
srReqResultSerial.replace(
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index e557277..c33a87f 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -32,6 +32,11 @@
import org.junit.Before;
import org.junit.Test;
+/**
+ * Tests for {@link ChangeNotesParser}.
+ *
+ * <p>When modifying storage format, please, add tests that both old and new data can be parsed.
+ */
public class ChangeNotesParserTest extends AbstractChangeNotesTest {
private TestRepository<InMemoryRepository> testRepo;
private ChangeNotesRevWalk walk;
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
index 034e7ef..22207cc 100644
--- a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
@@ -371,7 +371,7 @@
assertThat(r.submitRequirement().submittabilityExpression().expressionString())
.isEqualTo(submitExpression);
assertThat(r.status()).isEqualTo(status);
- assertThat(r.submittabilityExpressionResult().status()).isEqualTo(expressionStatus);
+ assertThat(r.submittabilityExpressionResult().get().status()).isEqualTo(expressionStatus);
}
private SubmitRecord createSubmitRecord(
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 957c33d..9e9d750 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -28,6 +28,7 @@
j2objc
jimfs
jruby
+log4j
lucene-analyzers-common
lucene-core
lucene-misc
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index d66c18e..c05fef4 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -22,6 +22,7 @@
"styles",
"types",
"utils",
+ "workers",
]
ts_config(
@@ -285,5 +286,6 @@
"--rules.no-property-visibility-mismatch off",
"--rules.no-incompatible-property-type off",
"--rules.no-incompatible-type-binding off",
+ "--rules.no-unknown-attribute error",
],
)
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
index 1bf4d88..c2820e2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column-requirement/gr-change-list-column-requirement_test.ts
@@ -118,8 +118,12 @@
<gr-vote-chip></gr-vote-chip>
</div>`);
const voteChip = queryAndAssert(element, 'gr-vote-chip');
- expect(voteChip).shadowDom.to.equal(`<span class="container">
+ expect(voteChip).shadowDom.to.equal(`<gr-tooltip-content
+ class="container"
+ has-tooltip=""
+ title="bad"
+ >
<div class="negative vote-chip">-1</div>
- </span>`);
+ </gr-tooltip-content>`);
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index f8b1a2b..11724096 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -73,7 +73,7 @@
export interface ChangeListSection {
countLabel?: string;
- isOutgoing?: boolean;
+ emptyStateSlotName?: string;
name?: string;
query?: string;
results: ChangeInfo[];
@@ -112,7 +112,7 @@
@state() private dynamicHeaderEndpoints?: string[];
- @property({type: Number})
+ @property({type: Number, attribute: 'selected-index'})
selectedIndex?: number;
@property({type: Boolean})
@@ -306,10 +306,8 @@
class="cell"
colspan="${this.computeColspan(changeSection, labelNames)}"
>
- ${this.getSpecialEmptySlot(changeSection)
- ? html`<slot
- name="${this.getSpecialEmptySlot(changeSection)}"
- ></slot>`
+ ${changeSection.emptyStateSlotName
+ ? html`<slot name="${changeSection.emptyStateSlotName}"></slot>`
: 'No changes'}
</td>
</tr>
@@ -672,15 +670,7 @@
if (this.selectedIndex) this.cursor.setCursorAtIndex(this.selectedIndex);
}
- // private but used in test
- getSpecialEmptySlot(section: ChangeListSection) {
- if (section.isOutgoing) return 'empty-outgoing';
- if (section.name === YOUR_TURN.name) return 'empty-your-turn';
- return '';
- }
-
- // private but used in test
- isEmpty(section: ChangeListSection) {
+ private isEmpty(section: ChangeListSection) {
return !section.results?.length;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index 29204b6..88b1357 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -25,7 +25,6 @@
queryAndAssert,
stubFlags,
} from '../../../test/test-utils';
-import {YOUR_TURN} from '../../core/gr-navigation/gr-navigation';
import {Key} from '../../../utils/dom-util';
import {TimeFormat} from '../../../constants/constants';
import {AccountId, NumericChangeId} from '../../../types/common';
@@ -44,6 +43,79 @@
element = basicFixture.instantiate();
});
+ test('renders', async () => {
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: TimeFormat.HHMM_12,
+ change_table: [],
+ };
+ element.account = {_account_id: 1001 as AccountId};
+ element.config = createServerInfo();
+ element.sections = [{results: new Array(1)}, {results: new Array(2)}];
+ element.selectedIndex = 0;
+ element.changes = [
+ {...createChange(), _number: 0 as NumericChangeId},
+ {...createChange(), _number: 1 as NumericChangeId},
+ {...createChange(), _number: 2 as NumericChangeId},
+ ];
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <gr-change-list-item
+ aria-label="Test subject"
+ selected=""
+ >
+ </gr-change-list-item>
+ <gr-change-list-item aria-label="Test subject">
+ </gr-change-list-item>
+ <gr-change-list-item aria-label="Test subject">
+ </gr-change-list-item>
+ <table id="changeList">
+ <tbody class="groupContent">
+ <tr class="groupTitle">
+ <td
+ aria-hidden="true"
+ class="leftPadding"
+ >
+ </td>
+ <td
+ aria-label="Star status column"
+ class="star"
+ hidden=""
+ >
+ </td>
+ <td class="number">
+ #
+ </td>
+ <td class="subject">
+ Subject
+ </td>
+ <td class="status">
+ Status
+ </td>
+ <td class="owner">
+ Owner
+ </td>
+ <td class="reviewers">
+ Reviewers
+ </td>
+ <td class="repo">
+ Repo
+ </td>
+ <td class="branch">
+ Branch
+ </td>
+ <td class="updated">
+ Updated
+ </td>
+ <td class="size">
+ Size
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ `);
+ });
+
suite('test show change number not logged in', () => {
setup(async () => {
element = basicFixture.instantiate();
@@ -251,35 +323,35 @@
assert.equal(noChangesMsg.length, 2);
});
- suite('empty section', () => {
- test('not shown on empty non-outgoing sections', () => {
- const section = {name: 'test', query: 'test', results: []};
- assert.isTrue(element.isEmpty(section));
- assert.equal(element.getSpecialEmptySlot(section), '');
- });
-
- test('shown on empty outgoing sections', () => {
+ suite('empty section slots', () => {
+ test('are shown on empty sections with slot name', async () => {
const section = {
name: 'test',
query: 'test',
results: [],
- isOutgoing: true,
+ emptyStateSlotName: 'test',
};
- assert.isTrue(element.isEmpty(section));
- assert.equal(element.getSpecialEmptySlot(section), 'empty-outgoing');
+ element.sections = [section];
+ await element.updateComplete;
+
+ assert.isEmpty(queryAll(element, 'gr-change-list-item'));
+ queryAndAssert(element, 'slot[name="test"]');
});
- test('shown on empty outgoing sections', () => {
- const section = {name: YOUR_TURN.name, query: 'test', results: []};
- assert.isTrue(element.isEmpty(section));
- assert.equal(element.getSpecialEmptySlot(section), 'empty-your-turn');
+ test('are not shown on empty sections without slot name', async () => {
+ const section = {name: 'test', query: 'test', results: []};
+ element.sections = [section];
+ await element.updateComplete;
+
+ assert.isEmpty(queryAll(element, 'gr-change-list-item'));
+ assert.notExists(query(element, 'slot[name="test"]'));
});
- test('not shown on non-empty outgoing sections', () => {
+ test('are not shown on non-empty sections with slot name', async () => {
const section = {
name: 'test',
query: 'test',
- isOutgoing: true,
+ emptyStateSlotName: 'test',
results: [
{
...createChange(),
@@ -288,7 +360,11 @@
},
],
};
- assert.isFalse(element.isEmpty(section));
+ element.sections = [section];
+ await element.updateComplete;
+
+ assert.isNotEmpty(queryAll(element, 'gr-change-list-item'));
+ assert.notExists(query(element, 'slot[name="test"]'));
});
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index 1b04268..f61518c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -28,6 +28,7 @@
import {htmlTemplate} from './gr-dashboard-view_html';
import {
GerritNav,
+ OUTGOING,
UserDashboard,
YOUR_TURN,
} from '../../core/gr-navigation/gr-navigation';
@@ -56,6 +57,7 @@
import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {GerritView} from '../../../services/router/router-model';
import {RELOAD_DASHBOARD_INTERVAL_MS} from '../../../constants/constants';
+import {ChangeListSection} from '../gr-change-list/gr-change-list';
const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
@@ -68,13 +70,10 @@
};
}
-interface DashboardChange {
- name: string;
- countLabel: string;
- query: string;
- results: ChangeInfo[];
- isOutgoing?: boolean;
-}
+const slotNameBySectionName = new Map<string, string>([
+ [YOUR_TURN.name, 'your-turn-slot'],
+ [OUTGOING.name, 'outgoing-slot'],
+]);
@customElement('gr-dashboard-view')
export class GrDashboardView extends PolymerElement {
@@ -101,7 +100,7 @@
params?: AppElementParams;
@property({type: Array})
- _results?: DashboardChange[];
+ _results?: ChangeListSection[];
@property({type: Boolean})
_loading = true;
@@ -301,7 +300,7 @@
countLabel: this._computeSectionCountLabel(results),
query: res.sections[i].query,
results: this._maybeSortResults(res.sections[i].name, results),
- isOutgoing: res.sections[i].isOutgoing,
+ emptyStateSlotName: slotNameBySectionName.get(res.sections[i].name),
};
})
.filter(
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index b4fa7cc..95829ec 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
@@ -78,12 +78,12 @@
showstar=""
account="[[account]]"
preferences="[[preferences]]"
- selectedindex="[[_selectedChangeIndex]]"
+ selected-index="[[_selectedChangeIndex]]"
sections="[[_results]]"
on-selected-index-changed="_handleSelectedIndexChanged"
on-toggle-star="_handleToggleStar"
>
- <div id="emptyOutgoing" slot="empty-outgoing">
+ <div id="emptyOutgoing" slot="outgoing-slot">
<template is="dom-if" if="[[_showNewUserHelp]]">
<gr-create-change-help
on-create-tap="_handleCreateChangeTap"
@@ -91,7 +91,7 @@
</template>
<template is="dom-if" if="[[!_showNewUserHelp]]"> No changes </template>
</div>
- <div id="emptyYourTurn" slot="empty-your-turn">
+ <div id="emptyYourTurn" slot="your-turn-slot">
<span>No changes need your attention 🎉</span>
</div>
</gr-change-list>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index aa76347..82fffff 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -315,9 +315,9 @@
});
});
- test('preserve isOutgoing sections', () => {
+ test('sets slot name to section name if custom state is requested', () => {
const sections = [
- {name: 'test1', query: 'test1', isOutgoing: true},
+ {name: 'Outgoing reviews', query: 'test1'},
{name: 'test2', query: 'test2'},
];
getChangesStub.restore();
@@ -326,8 +326,8 @@
return element._fetchDashboardChanges({sections}, false).then(() => {
assert.equal(element._results.length, 2);
- assert.isTrue(element._results[0].isOutgoing);
- assert.isNotOk(element._results[1].isOutgoing);
+ assert.equal(element._results[0].emptyStateSlotName, 'outgoing-slot');
+ assert.isNotOk(element._results[1].emptyStateSlotName);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index d52281c..231b9d0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -1343,8 +1343,7 @@
* for each path in order, awaiting the previous render to complete before
* continuing.
*
- * @param initialCount The total number of paths in the pass. This
- * is used to generate log messages.
+ * @param initialCount The total number of paths in the pass.
*/
private async _renderInOrder(
files: PatchSetFile[],
@@ -1376,11 +1375,19 @@
throw new Error('diffPrefs must be set');
}
- const promises: Array<Promise<unknown>> = [diffElem.reload()];
- if (this._loggedIn && !this.diffPrefs.manual_review) {
- promises.push(this._reviewFile(path, true));
+ // When one file is expanded individually then automatically mark as
+ // reviewed, if the user's diff prefs request it. Doing this for
+ // "Expand All" would not be what the user wants, because there is no
+ // control over which diffs were actually seen. And for lots of diffs
+ // that would even be a problem for write QPS quota.
+ if (
+ this._loggedIn &&
+ !this.diffPrefs.manual_review &&
+ initialCount === 1
+ ) {
+ this._reviewFile(path, true);
}
- return Promise.all(promises);
+ return diffElem.reload();
});
this._cancelForEachDiff = undefined;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index bd12eae..991c995 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -1009,24 +1009,6 @@
const reviewStub = sinon.stub(element, '_reviewFile');
let callCount = 0;
const diffs = [{
- path: 'p0',
- style: {},
- prefetchDiff() {},
- reload() {
- assert.equal(reviewStub.callCount, 2);
- assert.equal(callCount++, 2);
- return Promise.resolve();
- },
- }, {
- path: 'p1',
- style: {},
- prefetchDiff() {},
- reload() {
- assert.equal(reviewStub.callCount, 1);
- assert.equal(callCount++, 1);
- return Promise.resolve();
- },
- }, {
path: 'p2',
style: {},
prefetchDiff() {},
@@ -1036,11 +1018,9 @@
return Promise.resolve();
},
}];
- element._renderInOrder([
- {path: 'p2'}, {path: 'p1'}, {path: 'p0'},
- ], diffs, 3);
+ element._renderInOrder([{path: 'p2'}], diffs, 1);
await flush();
- assert.equal(reviewStub.callCount, 3);
+ assert.equal(reviewStub.callCount, 1);
});
test('_renderInOrder respects diffPrefs.manual_review', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
index a6c67d7..77b9d38 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
@@ -119,7 +119,6 @@
</span>
<iron-input
id="filterInput"
- placeholder="Filter"
.bindValue=${this.filterText}
@bind-value-changed=${(e: BindValueChangeEvent) => {
this.filterText = e.detail.value;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 5da6fa8..748729c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -125,19 +125,19 @@
gr-button {
--button-background-color: var(--vote-chip-unselected-color);
}
- gr-button[vote='max'].iron-selected {
+ gr-button[data-vote='max'].iron-selected {
--button-background-color: var(--vote-chip-selected-positive-color);
}
- gr-button[vote='positive'].iron-selected {
+ gr-button[data-vote='positive'].iron-selected {
--button-background-color: var(--vote-chip-selected-positive-color);
}
- gr-button[vote='neutral'].iron-selected {
+ gr-button[data-vote='neutral'].iron-selected {
--button-background-color: var(--vote-chip-selected-neutral-color);
}
- gr-button[vote='negative'].iron-selected {
+ gr-button[data-vote='negative'].iron-selected {
--button-background-color: var(--vote-chip-selected-negative-color);
}
- gr-button[vote='min'].iron-selected {
+ gr-button[data-vote='min'].iron-selected {
--button-background-color: var(--vote-chip-selected-negative-color);
}
gr-button > gr-tooltip-content {
@@ -205,7 +205,7 @@
return html`
<iron-selector
id="labelSelector"
- attr-for-selected="data-value"
+ .attrForSelected=${'data-value'}
?hidden="${!this._computeAnyPermittedLabelValues()}"
selected="${ifDefined(this._computeLabelValue())}"
@selected-item-changed=${this.setSelectedValueText}
@@ -223,12 +223,12 @@
(value, index) => html`
<gr-button
role="radio"
- vote="${this._computeVoteAttribute(
+ title="${ifDefined(this.computeLabelValueTitle(value))}"
+ data-vote="${this._computeVoteAttribute(
Number(value),
index,
items.length
)}"
- title="${ifDefined(this.computeLabelValueTitle(value))}"
data-name="${ifDefined(this.label?.name)}"
data-value="${value}"
aria-label="${value}"
@@ -322,7 +322,7 @@
/**
* Private but used in tests.
* Maps the label value to exactly one of: min, max, positive, negative,
- * neutral. Used for the 'vote' attribute, because we don't want to
+ * neutral. Used for the 'data-vote' attribute, because we don't want to
* interfere with <iron-selector> using the 'class' attribute for setting
* 'iron-selected'.
*/
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts
index 958b11a..e2d05c1 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.ts
@@ -343,7 +343,6 @@
<span class="placeholder" data-label="Verified"></span>
<iron-selector
aria-labelledby="labelName"
- attr-for-selected="data-value"
id="labelSelector"
role="radiogroup"
selected="+1"
@@ -356,7 +355,7 @@
role="radio"
tabindex="0"
title="bad"
- vote="min"
+ data-vote="min"
votechip=""
flatten=""
>
@@ -371,7 +370,7 @@
data-value=" 0"
role="radio"
tabindex="0"
- vote="neutral"
+ data-vote="neutral"
votechip=""
flatten=""
>
@@ -389,7 +388,7 @@
role="radio"
tabindex="0"
title="good"
- vote="max"
+ data-vote="max"
votechip=""
flatten=""
>
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
index f3abe13..50492d2 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
@@ -120,6 +120,7 @@
const labels = this.change?.labels ?? {};
return html`<gr-trigger-vote
.label="${score.label}"
+ .displayValue=${score.value}
.labelInfo="${labels[score.label]}"
.change="${this.change}"
.mutable="${false}"
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
index d9c1db1..74d1114 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores_test.ts
@@ -162,9 +162,14 @@
const triggerChips =
element.shadowRoot?.querySelectorAll('gr-trigger-vote');
assert.equal(triggerChips?.length, 1);
- expect(triggerChips?.[0]).shadowDom.equal(`<div class="container">
+ const triggerChip = triggerChips?.[0];
+ expect(triggerChip).shadowDom.equal(`<div class="container">
<span class="label">Auto-Submit</span>
+ <gr-vote-chip></gr-vote-chip>
</div>`);
+ const voteChips = triggerChip?.shadowRoot?.querySelectorAll('gr-vote-chip');
+ assert.equal(voteChips?.length, 1);
+ expect(voteChips?.[0]).shadowDom.equal('');
const scoreChips = element.shadowRoot?.querySelectorAll('.score');
assert.equal(scoreChips?.length, 1);
expect(scoreChips?.[0]).dom.equal(`<span class="removed score">
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
index a00de22..41964c2 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote/gr-trigger-vote.ts
@@ -45,6 +45,13 @@
@property({type: Object})
account?: AccountInfo;
+ /**
+ * If defined, trigger-vote is shown with this value instead of the latest
+ * vote. This is useful for change log.
+ */
+ @property()
+ displayValue?: string;
+
@property({type: Boolean})
mutable?: boolean;
@@ -115,6 +122,11 @@
private renderVotes() {
const {labelInfo} = this;
if (!labelInfo) return;
+ if (this.displayValue)
+ return html`<gr-vote-chip
+ .displayValue=${this.displayValue}
+ .label=${labelInfo}
+ ></gr-vote-chip>`;
if (isDetailedLabelInfo(labelInfo)) {
const approvals = getAllUniqueApprovals(labelInfo).filter(
approval => !hasNeutralStatus(labelInfo, approval)
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 8541d5d..386591b 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -74,7 +74,6 @@
suffixForDashboard?: string;
selfOnly?: boolean;
hideIfEmpty?: boolean;
- isOutgoing?: boolean;
results?: ChangeInfo[];
}
@@ -115,12 +114,11 @@
hideIfEmpty: true,
suffixForDashboard: 'limit:25',
};
-const OUTGOING: DashboardSection = {
+export const OUTGOING: DashboardSection = {
// Non-WIP open changes owned by viewed user. Filter out changes ignored
// by the viewing user.
name: 'Outgoing reviews',
query: 'is:open owner:${user} -is:wip -is:ignored',
- isOutgoing: true,
suffixForDashboard: 'limit:25',
};
const INCOMING: DashboardSection = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index c5b509c..8e377b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -100,6 +100,7 @@
import {distinctUntilChanged, map} from 'rxjs/operators';
import {deepEqual} from '../../../utils/deep-util';
import {Category} from '../../../api/checks';
+import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
const EMPTY_BLAME = 'No blame information for this diff.';
@@ -293,7 +294,7 @@
private readonly jsAPI = getAppContext().jsApiService;
- private readonly syntaxLayer = new GrSyntaxLayer();
+ private readonly syntaxLayer: GrSyntaxLayer | GrSyntaxLayerWorker;
private checksSubscription?: Subscription;
@@ -301,6 +302,12 @@
constructor() {
super();
+ const useWorker = this.flags.isEnabled(
+ KnownExperimentId.SYNTAX_LAYER_WORKER
+ );
+ this.syntaxLayer = useWorker
+ ? new GrSyntaxLayerWorker()
+ : new GrSyntaxLayer();
this._renderPrefs = {
...this._renderPrefs,
use_lit_components: this.flags.isEnabled(
@@ -701,7 +708,12 @@
/** Cancel any remaining diff builder rendering work. */
cancel() {
this.$.diff.cancel();
- this.syntaxLayer.cancel();
+ // TODO: Remove calling cancel() when switching to GrSyntaxLayerWorker.
+ // The plan is to only start syntax highlighting when a diff is visible on
+ // screen, and HighlightJS itself does not allow cancelling. So there is
+ // unclear benefit from the ability to cancel, which would be non-trivial
+ // to implement correctly and efficiently.
+ if (this.syntaxLayer instanceof GrSyntaxLayer) this.syntaxLayer.cancel();
}
getCursorStops() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
deleted file mode 100644
index 6580ad6..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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.
- */
-/* eslint-disable lit/no-legacy-template-syntax,lit/prefer-static-styles */
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-import {customElement, property} from '@polymer/decorators';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-custom-plugin-header': GrCustomPluginHeader;
- }
-}
-
-@customElement('gr-custom-plugin-header')
-export class GrCustomPluginHeader extends PolymerElement {
- @property({type: String})
- logoUrl = '';
-
- @property({type: String})
- override title = '';
-
- static get template() {
- return html`
- <style>
- img {
- width: 1em;
- height: 1em;
- vertical-align: middle;
- }
- .title {
- margin-left: var(--spacing-xs);
- }
- </style>
- <span>
- <img src="[[logoUrl]]" hidden$="[[!logoUrl]]" />
- <span class="title">[[title]]</span>
- </span>
- `;
- }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
index 64f97ca..cfbb942 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.ts
@@ -58,7 +58,7 @@
@property({type: String})
selectedScheme?: string;
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'show-keyboard-shortcut-tooltips'})
showKeyboardShortcutTooltips = false;
private readonly restApiService = getAppContext().restApiService;
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index b011d23..487145f 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import '../gr-tooltip-content/gr-tooltip-content';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {
@@ -50,6 +51,13 @@
@property({type: Boolean})
more = false;
+ /**
+ * If defined, vote-chip is shown with this value instead of the latest vote.
+ * This is useful for change log.
+ */
+ @property()
+ displayValue?: string;
+
private readonly flagsService = getAppContext().flagsService;
static override get styles() {
@@ -58,6 +66,7 @@
:host([circle-shape]) .vote-chip {
border-radius: 50%;
border: none;
+ padding: 2px;
}
.vote-chip.max {
background-color: var(--vote-color-approved);
@@ -125,17 +134,24 @@
const renderValue = this.renderValue();
if (!renderValue) return;
- return html`<span class="container ${this.more ? 'more' : ''}">
+ return html`<gr-tooltip-content
+ class="container ${this.more ? 'more' : ''}"
+ title="${this.computeTooltip()}"
+ has-tooltip
+ >
<div class="vote-chip ${this.computeClass()}">${renderValue}</div>
${this.more
? html`<div class="chip-angle ${this.computeClass()}">
${renderValue}
</div>`
: ''}
- </span>`;
+ </gr-tooltip-content>`;
}
private renderValue() {
+ if (this.displayValue) {
+ return this.displayValue;
+ }
if (!this.label) {
return '';
} else if (isDetailedLabelInfo(this.label)) {
@@ -157,9 +173,19 @@
private computeClass() {
if (!this.label) {
return '';
+ } else if (this.displayValue) {
+ const status = getLabelStatus(this.label, Number(this.displayValue));
+ return classForLabelStatus(status);
} else {
const status = getLabelStatus(this.label, this.vote?.value);
return classForLabelStatus(status);
}
}
+
+ private computeTooltip() {
+ if (!this.label || !isDetailedLabelInfo(this.label)) {
+ return '';
+ }
+ return this.label.values?.[valueString(this.vote?.value)] ?? '';
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
index d5c5df8..de8f5ae 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
@@ -48,9 +48,13 @@
});
test('renders', () => {
- expect(element).shadowDom.to.equal(`<span class="container">
+ expect(element).shadowDom.to.equal(`<gr-tooltip-content
+ class="container"
+ has-tooltip=""
+ title=""
+ >
<div class="max vote-chip">👍</div>
- </span>`);
+ </gr-tooltip-content>`);
});
});
@@ -69,11 +73,15 @@
});
test('renders', () => {
- expect(element).shadowDom.to.equal(`<span class="container">
+ expect(element).shadowDom.to.equal(`<gr-tooltip-content
+ class="container"
+ has-tooltip=""
+ title=""
+ >
<div class="positive vote-chip">
+2
</div>
- </span>`);
+ </gr-tooltip-content>`);
});
test('renders negative vote', async () => {
@@ -84,11 +92,15 @@
element = await fixture<GrVoteChip>(
html`<gr-vote-chip .label=${labelInfo} .vote=${vote}></gr-vote-chip>`
);
- expect(element).shadowDom.to.equal(`<span class="container">
+ expect(element).shadowDom.to.equal(`<gr-tooltip-content
+ class="container"
+ has-tooltip=""
+ title="Wrong Style or Formatting"
+ >
<div class="min vote-chip">
-1
</div>
- </span>`);
+ </gr-tooltip-content>`);
});
test('renders for more than 1 vote', async () => {
@@ -99,10 +111,14 @@
more
></gr-vote-chip>`
);
- expect(element).shadowDom.to.equal(`<span class="container more">
+ expect(element).shadowDom.to.equal(`<gr-tooltip-content
+ class="container more"
+ has-tooltip=""
+ title=""
+ >
<div class="positive vote-chip">+2</div>
<div class="chip-angle positive">+2</div>
- </span>`);
+ </gr-tooltip-content>`);
});
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
new file mode 100644
index 0000000..ba64f27
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -0,0 +1,198 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {FILE, GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {DiffFileMetaInfo, DiffInfo} from '../../../types/diff';
+import {DiffLayer, DiffLayerListener} from '../../../types/types';
+import {Side} from '../../../constants/constants';
+import {LANGUAGE_MAP} from './gr-syntax-layer';
+import {getAppContext} from '../../../services/app-context';
+import {SyntaxLayerLine} from '../../../types/syntax-worker-api';
+
+const CLASS_PREFIX = 'gr-diff gr-syntax gr-syntax-';
+
+const CLASS_SAFELIST = new Set<string>([
+ 'attr',
+ 'attribute',
+ 'built_in',
+ 'comment',
+ 'doctag',
+ 'function',
+ 'keyword',
+ 'link',
+ 'literal',
+ 'meta',
+ 'meta-keyword',
+ 'name',
+ 'number',
+ 'params',
+ 'property',
+ 'regexp',
+ 'selector-attr',
+ 'selector-class',
+ 'selector-id',
+ 'selector-pseudo',
+ 'selector-tag',
+ 'string',
+ 'tag',
+ 'template-tag',
+ 'template-variable',
+ 'title',
+ 'type',
+ 'variable',
+]);
+
+export class GrSyntaxLayerWorker implements DiffLayer {
+ diff?: DiffInfo;
+
+ enabled = true;
+
+ private leftRanges: SyntaxLayerLine[] = [];
+
+ private rightRanges: SyntaxLayerLine[] = [];
+
+ private listeners: DiffLayerListener[] = [];
+
+ private readonly highlightService = getAppContext().highlightService;
+
+ init(diff?: DiffInfo) {
+ this.leftRanges = [];
+ this.rightRanges = [];
+ this.diff = diff;
+ }
+
+ setEnabled(enabled: boolean) {
+ this.enabled = enabled;
+ }
+
+ addListener(listener: DiffLayerListener) {
+ this.listeners.push(listener);
+ }
+
+ removeListener(listener: DiffLayerListener) {
+ this.listeners = this.listeners.filter(f => f !== listener);
+ }
+
+ annotate(el: HTMLElement, _: HTMLElement, line: GrDiffLine) {
+ if (!this.enabled) return;
+ if (line.beforeNumber === FILE || line.afterNumber === FILE) return;
+ if (line.beforeNumber === 'LOST' || line.afterNumber === 'LOST') return;
+
+ let side: Side | undefined;
+ if (
+ line.type === GrDiffLineType.REMOVE ||
+ (line.type === GrDiffLineType.BOTH &&
+ el.getAttribute('data-side') !== Side.RIGHT)
+ ) {
+ side = Side.LEFT;
+ } else if (
+ line.type === GrDiffLineType.ADD ||
+ el.getAttribute('data-side') !== Side.LEFT
+ ) {
+ side = Side.RIGHT;
+ }
+ if (!side) return;
+
+ const isLeft = side === Side.LEFT;
+ const lineNumber = isLeft ? line.beforeNumber : line.afterNumber;
+ const rangesPerLine = isLeft ? this.leftRanges : this.rightRanges;
+ const ranges = rangesPerLine[lineNumber - 1]?.ranges ?? [];
+
+ for (const range of ranges) {
+ if (!CLASS_SAFELIST.has(range.className)) continue;
+ GrAnnotation.annotateElement(
+ el,
+ range.start,
+ range.length,
+ CLASS_PREFIX + range.className
+ );
+ }
+ }
+
+ _getLanguage(metaInfo?: DiffFileMetaInfo) {
+ if (!metaInfo) return undefined;
+ // The Gerrit API provides only content-type, but for other users of
+ // gr-diff it may be more convenient to specify the language directly.
+ return metaInfo.language ?? LANGUAGE_MAP.get(metaInfo.content_type);
+ }
+
+ /**
+ * Computes SyntaxLayerLines asynchronously, which can then later be applied,
+ * when the annotate() method of the layer API is called.
+ *
+ * For larger files this is an expensive operation, but is offloaded to a web
+ * worker. We are using the HighlightJS lib for doing the actual highlighting.
+ *
+ * `init()` must have been called before. There is actually no good reason for
+ * init() and process() to be separated. The diff host typically allows the
+ * diff builder to render first and only then calls process(), but as soon as
+ * the diff is known the highlighting process can be kicked off.
+ * TODO(brohlfs): Call process() directly after init().
+ *
+ * annotate() will only be able to apply highlighting after process() has
+ * completed, but that will likely happen later. That is why layer can have
+ * listeners. When process() completes, the listeners will be notified, which
+ * tells the diff renderer that another call to annotate() is needed.
+ */
+ async process() {
+ this.leftRanges = [];
+ this.rightRanges = [];
+ if (!this.enabled || !this.diff) return;
+
+ const leftLanguage = this._getLanguage(this.diff.meta_a);
+ const rightLanguage = this._getLanguage(this.diff.meta_b);
+
+ let leftContent = '';
+ let rightContent = '';
+ for (const chunk of this.diff.content) {
+ const a = [...(chunk.a ?? []), ...(chunk.ab ?? [])];
+ const b = [...(chunk.b ?? []), ...(chunk.ab ?? [])];
+ for (const line of a) {
+ leftContent += line + '\n';
+ }
+ for (const line of b) {
+ rightContent += line + '\n';
+ }
+ }
+
+ const leftPromise = this.highlight(leftLanguage, leftContent);
+ const rightPromise = this.highlight(rightLanguage, rightContent);
+ this.leftRanges = await leftPromise;
+ this.rightRanges = await rightPromise;
+ this.notify();
+ }
+
+ async highlight(language?: string, code?: string) {
+ if (!language || !code) return [];
+ return this.highlightService.highlight(language, code);
+ }
+
+ notify() {
+ // We don't want to notify for lines that don't have any SyntaxLayerRange.
+ // So for both sides we are looking for the first and the last occurrence
+ // of a line with at least one SyntaxLayerRange.
+ const leftRangesReversed = [...this.leftRanges].reverse();
+ const leftStart = this.leftRanges.findIndex(line => line.ranges.length > 0);
+ const leftEnd =
+ this.leftRanges.length -
+ 1 -
+ leftRangesReversed.findIndex(line => line.ranges.length > 0);
+
+ const rightRangesReversed = [...this.rightRanges].reverse();
+ const rightStart = this.rightRanges.findIndex(
+ line => line.ranges.length > 0
+ );
+ const rightEnd =
+ this.rightRanges.length -
+ 1 -
+ rightRangesReversed.findIndex(line => line.ranges.length > 0);
+
+ for (const listener of this.listeners) {
+ if (leftStart > -1) listener(leftStart + 1, leftEnd + 1, Side.LEFT);
+ if (rightStart > -1) listener(rightStart + 1, rightEnd + 1, Side.RIGHT);
+ }
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
new file mode 100644
index 0000000..9c2cc9d
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker_test.ts
@@ -0,0 +1,115 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {DiffInfo, GrDiffLineType, Side} from '../../../api/diff';
+import '../../../test/common-test-setup-karma';
+import {SyntaxLayerLine} from '../../../types/syntax-worker-api';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+import {GrSyntaxLayerWorker} from './gr-syntax-layer-worker';
+
+const diff: DiffInfo = {
+ meta_a: {
+ name: 'somepath/somefile.js',
+ content_type: 'text/javascript',
+ lines: 3,
+ language: 'lang-left',
+ },
+ meta_b: {
+ name: 'somepath/somefile.js',
+ content_type: 'text/javascript',
+ lines: 4,
+ language: 'lang-right',
+ },
+ change_type: 'MODIFIED',
+ intraline_status: 'OK',
+ content: [
+ {
+ ab: ['import it;'],
+ },
+ {
+ b: ['b only'],
+ },
+ {
+ ab: [' public static final {', 'ab3'],
+ },
+ ],
+};
+
+const leftRanges: SyntaxLayerLine[] = [
+ {ranges: [{start: 0, length: 6, className: 'literal'}]},
+ {ranges: []},
+ {ranges: []},
+];
+
+const rightRanges: SyntaxLayerLine[] = [
+ {ranges: []},
+ {ranges: []},
+ {
+ ranges: [
+ {start: 0, length: 2, className: 'not-safe'},
+ {start: 2, length: 6, className: 'literal'},
+ {start: 9, length: 6, className: 'keyword'},
+ {start: 16, length: 5, className: 'name'},
+ ],
+ },
+ {ranges: []},
+];
+
+suite('gr-syntax-layer-worker tests', () => {
+ let layer: GrSyntaxLayerWorker;
+ let listener: sinon.SinonStub;
+
+ const annotate = (side: Side, lineNumber: number, text: string) => {
+ const el = document.createElement('div');
+ const lineNumberEl = document.createElement('td');
+ el.setAttribute('data-side', side);
+ el.innerText = text;
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ if (side === Side.LEFT) line.beforeNumber = lineNumber;
+ if (side === Side.RIGHT) line.afterNumber = lineNumber;
+ layer.annotate(el, lineNumberEl, line);
+ return el;
+ };
+
+ setup(() => {
+ layer = new GrSyntaxLayerWorker();
+ listener = sinon.stub();
+ layer.addListener(listener);
+ sinon.stub(layer, 'highlight').callsFake((lang?: string) => {
+ if (lang === 'lang-left') return Promise.resolve(leftRanges);
+ if (lang === 'lang-right') return Promise.resolve(rightRanges);
+ return Promise.resolve([]);
+ });
+ layer.init(diff);
+ });
+
+ test('process and annotate line 2 LEFT', async () => {
+ await layer.process();
+ const el = annotate(Side.LEFT, 1, 'import it;');
+ assert.equal(
+ el.innerHTML,
+ '<hl class="gr-diff gr-syntax gr-syntax-literal">import</hl> it;'
+ );
+ assert.equal(listener.callCount, 2);
+ assert.equal(listener.getCall(0).args[0], 1);
+ assert.equal(listener.getCall(0).args[1], 1);
+ assert.equal(listener.getCall(0).args[2], Side.LEFT);
+ assert.equal(listener.getCall(1).args[0], 3);
+ assert.equal(listener.getCall(1).args[1], 3);
+ assert.equal(listener.getCall(1).args[2], Side.RIGHT);
+ });
+
+ test('process and annotate line 3 RIGHT', async () => {
+ await layer.process();
+ const el = annotate(Side.RIGHT, 3, ' public static final {');
+ assert.equal(
+ el.innerHTML,
+ ' <hl class="gr-diff gr-syntax gr-syntax-literal">public</hl> ' +
+ '<hl class="gr-diff gr-syntax gr-syntax-keyword">static</hl> ' +
+ '<hl class="gr-diff gr-syntax gr-syntax-name">final</hl> {'
+ );
+ assert.equal(listener.callCount, 2);
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer.ts
index b3d6605..bd34034 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer.ts
@@ -23,7 +23,7 @@
import {HLJS_LIBRARY_CONFIG} from '../../../elements/shared/gr-lib-loader/highlightjs_config';
import {Side} from '../../../constants/constants';
-const LANGUAGE_MAP = new Map<string, string>([
+export const LANGUAGE_MAP = new Map<string, string>([
['application/dart', 'dart'],
['application/json', 'json'],
['application/x-powershell', 'powershell'],
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 53ed193..7d2d742 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -93,6 +93,9 @@
pluginsModel: (_ctx: Partial<AppContext>) => {
throw new Error('pluginsModel is not implemented');
},
+ highlightService: (_ctx: Partial<AppContext>) => {
+ throw new Error('highlightService is not implemented');
+ },
};
return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 401c0c3..01bb31f 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -32,6 +32,18 @@
],
)
+ rollup_bundle(
+ name = "syntax-worker",
+ srcs = [app_name + "-full-src"],
+ config_file = ":rollup.config.js",
+ entry_point = "_pg_ts_out/workers/syntax-worker.js",
+ rollup_bin = "//tools/node_tools:rollup-bin",
+ sourcemap = "hidden",
+ deps = [
+ "@tools_npm//rollup-plugin-node-resolve",
+ ],
+ )
+
native.filegroup(
name = name + "_app_sources",
srcs = [
@@ -45,6 +57,13 @@
)
native.filegroup(
+ name = name + "_worker_sources",
+ srcs = [
+ "syntax-worker.js",
+ ],
+ )
+
+ native.filegroup(
name = name + "_top_sources",
srcs = [
"favicon.ico",
@@ -59,6 +78,7 @@
name + "_app_sources",
name + "_css_sources",
name + "_top_sources",
+ name + "_worker_sources",
"//lib/fonts:robotofonts",
"//lib/js:highlightjs__files",
"@ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js",
@@ -70,11 +90,12 @@
outs = outs,
cmd = " && ".join([
"FONT_DIR=$$(dirname $(location @ui_npm//:node_modules/@polymer/font-roboto-local/package.json))/fonts",
- "mkdir -p $$TMP/polygerrit_ui/{styles/themes,fonts/{roboto,robotomono},bower_components/{highlightjs,webcomponentsjs,resemblejs},elements}",
+ "mkdir -p $$TMP/polygerrit_ui/{workers,styles/themes,fonts/{roboto,robotomono},bower_components/{highlightjs,webcomponentsjs,resemblejs},elements}",
"for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + app_name + ".$$ext; done",
"cp $(locations //lib/fonts:robotofonts) $$TMP/polygerrit_ui/fonts/",
"for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
"for f in $(locations " + name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
+ "for f in $(locations " + name + "_worker_sources); do cp $$f $$TMP/polygerrit_ui/workers; done",
"for f in $(locations //lib/js:highlightjs__files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
"cp $(location @ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js) $$TMP/polygerrit_ui/bower_components/webcomponentsjs/webcomponents-lite.js",
"cp $(location @ui_npm//:node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js.map) $$TMP/polygerrit_ui/bower_components/webcomponentsjs/webcomponents-lite.js.map",
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 2d2400b..10aa266 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -37,6 +37,7 @@
import {ConfigModel, configModelToken} from '../models/config/config-model';
import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
import {PluginsModel} from '../models/plugins/plugins-model';
+import {HighlightService} from './highlight/highlight-service';
/**
* The AppContext lazy initializator for all services
@@ -75,6 +76,7 @@
return new ShortcutsService(ctx.userModel, ctx.reportingService!);
},
pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
+ highlightService: (_ctx: Partial<AppContext>) => new HighlightService(),
};
return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/services/app-context-init_test.ts b/polygerrit-ui/app/services/app-context-init_test.ts
index efe6aac..626c571 100644
--- a/polygerrit-ui/app/services/app-context-init_test.ts
+++ b/polygerrit-ui/app/services/app-context-init_test.ts
@@ -17,13 +17,13 @@
import '../test/common-test-setup-karma.js';
import {AppContext} from './app-context.js';
-import {createAppContext} from './app-context-init.js';
import {Finalizable} from './registry';
+import {createTestAppContext} from '../test/test-app-context-init.js';
suite('app context initializer tests', () => {
let appContext: AppContext & Finalizable;
setup(() => {
- appContext = createAppContext();
+ appContext = createTestAppContext();
});
teardown(() => {
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 965146a..ba21734 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -26,6 +26,7 @@
import {RouterModel} from './router/router-model';
import {ShortcutsService} from './shortcuts/shortcuts-service';
import {PluginsModel} from '../models/plugins/plugins-model';
+import {HighlightService} from './highlight/highlight-service';
export interface AppContext {
routerModel: RouterModel;
@@ -39,6 +40,7 @@
userModel: UserModel;
shortcutsService: ShortcutsService;
pluginsModel: PluginsModel;
+ highlightService: HighlightService;
}
/**
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 515dab8..0edc13d 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -32,4 +32,5 @@
BULK_ACTIONS = 'UiFeature__bulk_actions_dashboard',
CHECK_RESULTS_IN_DIFFS = 'UiFeature__check_results_in_diffs',
DIFF_RENDERING_LIT = 'UiFeature__diff_rendering_lit',
+ SYNTAX_LAYER_WORKER = 'UiFeature__syntax_layer_worker',
}
diff --git a/polygerrit-ui/app/services/highlight/highlight-service.ts b/polygerrit-ui/app/services/highlight/highlight-service.ts
new file mode 100644
index 0000000..2711ba5
--- /dev/null
+++ b/polygerrit-ui/app/services/highlight/highlight-service.ts
@@ -0,0 +1,136 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {
+ SyntaxWorkerRequest,
+ SyntaxWorkerInit,
+ SyntaxWorkerResult,
+ SyntaxWorkerMessageType,
+ SyntaxLayerLine,
+} from '../../types/syntax-worker-api';
+import {createWorker} from '../../utils/worker-util';
+import {Finalizable} from '../registry';
+
+const hljsLibUrl = `${window.STATIC_RESOURCE_PATH}/bower_components/highlightjs/highlight.min.js`;
+
+const syntaxWorkerUrl = `${window.STATIC_RESOURCE_PATH}/workers/syntax-worker.js`;
+
+/**
+ * It is unlikely that a pool size greater than 3 will gain anything, because
+ * the app also needs the resources to process the results.
+ */
+const WORKER_POOL_SIZE = 3;
+
+/**
+ * Service for syntax highlighting. Maintains some HighlightJS workers doing
+ * their job in the background.
+ */
+export class HighlightService implements Finalizable {
+ // visible for testing
+ poolIdle: Set<Worker> = new Set();
+
+ // visible for testing
+ poolBusy: Set<Worker> = new Set();
+
+ // visible for testing
+ /** Queue for waiting that a worker becomes available. */
+ queueForWorker: Array<() => void> = [];
+
+ // visible for testing
+ /** Queue for waiting on the results of a worker. */
+ queueForResult: Map<Worker, (r: SyntaxLayerLine[]) => void> = new Map();
+
+ constructor() {
+ for (let i = 0; i < WORKER_POOL_SIZE; i++) {
+ this.addWorker();
+ }
+ }
+
+ /** Allows tests to produce fake workers. */
+ protected createWorker() {
+ return createWorker(syntaxWorkerUrl);
+ }
+
+ /** Creates, initializes and then moves a worker to the idle pool. */
+ private addWorker() {
+ const worker = this.createWorker();
+ // Will move to the idle pool after being initialized.
+ this.poolBusy.add(worker);
+ worker.onmessage = (e: MessageEvent<SyntaxWorkerResult>) => {
+ this.handleResult(worker, e.data);
+ };
+ const initMsg: SyntaxWorkerInit = {
+ type: SyntaxWorkerMessageType.INIT,
+ url: hljsLibUrl,
+ };
+ worker.postMessage(initMsg);
+ }
+
+ private moveIdleToBusy() {
+ const worker = this.poolIdle.values().next().value;
+ this.poolIdle.delete(worker);
+ this.poolBusy.add(worker);
+ return worker;
+ }
+
+ private moveBusyToIdle(worker: Worker) {
+ this.poolBusy.delete(worker);
+ this.poolIdle.add(worker);
+ const resolver = this.queueForWorker.shift();
+ if (resolver) resolver();
+ }
+
+ /**
+ * If there is worker in the idle pool, then return it. Otherwise wait for a
+ * worker to become a available.
+ */
+ private async requestWorker(): Promise<Worker> {
+ if (this.poolIdle.size > 0) {
+ const worker = this.moveIdleToBusy();
+ return Promise.resolve(worker);
+ }
+ await new Promise<void>(r => this.queueForWorker.push(r));
+ return this.requestWorker();
+ }
+
+ /**
+ * A worker is done with its job. Move it back to the idle pool and notify the
+ * resolver that is waiting for the results.
+ */
+ private handleResult(worker: Worker, result: SyntaxWorkerResult) {
+ this.moveBusyToIdle(worker);
+ if (result.error) console.error(`syntax worker failed: ${result.error}`);
+ const resolver = this.queueForResult.get(worker);
+ this.queueForResult.delete(worker);
+ if (resolver) resolver(result.ranges ?? []);
+ }
+
+ async highlight(language: string, code: string): Promise<SyntaxLayerLine[]> {
+ const worker = await this.requestWorker();
+ const message: SyntaxWorkerRequest = {
+ type: SyntaxWorkerMessageType.REQUEST,
+ language,
+ code,
+ };
+ const promise = new Promise<SyntaxLayerLine[]>(r => {
+ this.queueForResult.set(worker, r);
+ });
+ worker.postMessage(message);
+ return await promise;
+ }
+
+ finalize() {
+ for (const worker of this.poolIdle) {
+ worker.terminate();
+ }
+ this.poolIdle.clear();
+ for (const worker of this.poolBusy) {
+ worker.terminate();
+ }
+ this.poolBusy.clear();
+ this.queueForResult.clear();
+ this.queueForWorker.length = 0;
+ }
+}
diff --git a/polygerrit-ui/app/services/highlight/highlight-service_test.ts b/polygerrit-ui/app/services/highlight/highlight-service_test.ts
new file mode 100644
index 0000000..dc0c2f7
--- /dev/null
+++ b/polygerrit-ui/app/services/highlight/highlight-service_test.ts
@@ -0,0 +1,165 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../test/common-test-setup-karma';
+import {waitUntil} from '../../test/test-utils';
+import {
+ SyntaxWorkerMessage,
+ SyntaxWorkerResult,
+} from '../../types/syntax-worker-api';
+import {HighlightService} from './highlight-service';
+
+class FakeWorker implements Worker {
+ messages: SyntaxWorkerMessage[] = [];
+
+ constructor(private readonly autoRespond = true) {}
+
+ postMessage(message: SyntaxWorkerMessage) {
+ this.messages.push(message);
+ if (this.autoRespond) this.sendResult({ranges: []});
+ }
+
+ sendResult(result: SyntaxWorkerResult) {
+ if (this.onmessage)
+ this.onmessage({data: result} as MessageEvent<SyntaxWorkerResult>);
+ }
+
+ onmessage: ((e: MessageEvent<SyntaxWorkerResult>) => any) | null = null;
+
+ onmessageerror = null;
+
+ onerror = null;
+
+ terminate(): void {}
+
+ addEventListener(): void {}
+
+ removeEventListener(): void {}
+
+ dispatchEvent(): boolean {
+ return true;
+ }
+}
+
+export class MockHighlightService extends HighlightService {
+ idle = this.poolIdle as Set<FakeWorker>;
+
+ busy = this.poolBusy as Set<FakeWorker>;
+
+ override createWorker(): Worker {
+ return new FakeWorker();
+ }
+
+ countAllMessages() {
+ let count = 0;
+ for (const worker of [...this.idle, ...this.busy]) {
+ count += worker.messages.length;
+ }
+ return count;
+ }
+
+ sendToAll(result: SyntaxWorkerResult) {
+ for (const worker of this.busy) {
+ worker.sendResult(result);
+ }
+ }
+}
+
+export class MockHighlightServiceManual extends MockHighlightService {
+ override createWorker(): Worker {
+ return new FakeWorker(false);
+ }
+}
+
+suite('highlight-service tests', () => {
+ let service: MockHighlightServiceManual;
+
+ setup(() => {
+ service = new MockHighlightServiceManual();
+ });
+
+ test('initial state', () => {
+ assert.equal(service.poolBusy.size, 3);
+ assert.equal(service.poolIdle.size, 0);
+ assert.equal(service.queueForResult.size, 0);
+ assert.equal(service.queueForWorker.length, 0);
+ });
+
+ test('initialized workers move to idle pool', () => {
+ service.sendToAll({});
+ assert.equal(service.countAllMessages(), 3);
+
+ assert.equal(service.poolBusy.size, 0);
+ assert.equal(service.poolIdle.size, 3);
+ });
+
+ test('highlight 1', async () => {
+ service.sendToAll({});
+ assert.equal(service.countAllMessages(), 3);
+
+ const p = service.highlight('asdf', 'qwer');
+ assert.equal(service.poolBusy.size, 1);
+ assert.equal(service.poolIdle.size, 2);
+ await waitUntil(() => service.queueForResult.size > 0);
+ assert.equal(service.queueForResult.size, 1);
+ assert.equal(service.queueForWorker.length, 0);
+
+ service.sendToAll({ranges: []});
+ assert.equal(service.countAllMessages(), 4);
+ const ranges = await p;
+ assert.equal(ranges.length, 0);
+
+ await waitUntil(() => service.queueForResult.size === 0);
+ assert.equal(service.poolBusy.size, 0);
+ assert.equal(service.poolIdle.size, 3);
+ assert.equal(service.queueForResult.size, 0);
+ assert.equal(service.queueForWorker.length, 0);
+ });
+
+ test('highlight 5', async () => {
+ service.sendToAll({});
+ assert.equal(service.countAllMessages(), 3);
+
+ const p1 = service.highlight('asdf1', 'qwer1');
+ const p2 = service.highlight('asdf2', 'qwer2');
+ const p3 = service.highlight('asdf3', 'qwer3');
+ const p4 = service.highlight('asdf4', 'qwer4');
+ const p5 = service.highlight('asdf5', 'qwer5');
+
+ assert.equal(service.poolBusy.size, 3);
+ assert.equal(service.poolIdle.size, 0);
+ await waitUntil(() => service.queueForResult.size > 0);
+ assert.equal(service.queueForResult.size, 3);
+ assert.equal(service.queueForWorker.length, 2);
+
+ service.sendToAll({ranges: []});
+ assert.equal(service.countAllMessages(), 6);
+ const ranges1 = await p1;
+ const ranges2 = await p2;
+ const ranges3 = await p3;
+ assert.equal(ranges1.length, 0);
+ assert.equal(ranges2.length, 0);
+ assert.equal(ranges3.length, 0);
+
+ await waitUntil(() => service.queueForResult.size === 2);
+ assert.equal(service.poolBusy.size, 2);
+ assert.equal(service.poolIdle.size, 1);
+ assert.equal(service.queueForResult.size, 2);
+ assert.equal(service.queueForWorker.length, 0);
+
+ service.sendToAll({ranges: []});
+ assert.equal(service.countAllMessages(), 8);
+ const ranges4 = await p4;
+ const ranges5 = await p5;
+ assert.equal(ranges4.length, 0);
+ assert.equal(ranges5.length, 0);
+
+ await waitUntil(() => service.queueForResult.size === 0);
+ assert.equal(service.poolBusy.size, 0);
+ assert.equal(service.poolIdle.size, 3);
+ assert.equal(service.queueForResult.size, 0);
+ assert.equal(service.queueForWorker.length, 0);
+ });
+});
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 9608239..7a9eee9 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -39,6 +39,7 @@
import {ConfigModel, configModelToken} from '../models/config/config-model';
import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
import {PluginsModel} from '../models/plugins/plugins-model';
+import {MockHighlightService} from '../services/highlight/highlight-service_test';
export function createTestAppContext(): AppContext & Finalizable {
const appRegistry: Registry<AppContext> = {
@@ -67,6 +68,7 @@
return new ShortcutsService(ctx.userModel!, ctx.reportingService!);
},
pluginsModel: (_ctx: Partial<AppContext>) => new PluginsModel(),
+ highlightService: (_ctx: Partial<AppContext>) => new MockHighlightService(),
};
return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index a335926..ac26c6d 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -95,6 +95,7 @@
"types/**/*",
"utils/**/*",
"test/**/*",
+ "workers/**/*",
"tmpl_out/**/*" //Created by template checker in dev-mode
]
}
diff --git a/polygerrit-ui/app/tsconfig_bazel.json b/polygerrit-ui/app/tsconfig_bazel.json
index cd83fc0..dba3060 100644
--- a/polygerrit-ui/app/tsconfig_bazel.json
+++ b/polygerrit-ui/app/tsconfig_bazel.json
@@ -22,10 +22,11 @@
"services/**/*",
"styles/**/*",
"types/**/*",
- "utils/**/*"
+ "utils/**/*",
+ "workers/**/*"
],
"exclude": [
"**/*_test.ts",
"**/*_test.js"
]
-}
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/tsconfig_bazel_test.json b/polygerrit-ui/app/tsconfig_bazel_test.json
index be3c934..78bb350 100644
--- a/polygerrit-ui/app/tsconfig_bazel_test.json
+++ b/polygerrit-ui/app/tsconfig_bazel_test.json
@@ -7,7 +7,9 @@
"../../external/ui_dev_npm/node_modules/@types"
],
"paths": {
- "@polymer/iron-test-helpers/*": ["../../ui_dev_npm/node_modules/@polymer/iron-test-helpers/*"]
+ "@polymer/iron-test-helpers/*": [
+ "../../ui_dev_npm/node_modules/@polymer/iron-test-helpers/*"
+ ]
}
},
"include": [
@@ -25,9 +27,10 @@
"scripts/**/*",
"services/**/*",
"styles/**/*",
+ "test/**/*",
"types/**/*",
"utils/**/*",
- "test/**/*"
+ "workers/**/*"
],
"exclude": []
-}
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/types/syntax-worker-api.ts b/polygerrit-ui/app/types/syntax-worker-api.ts
new file mode 100644
index 0000000..5dd8a36
--- /dev/null
+++ b/polygerrit-ui/app/types/syntax-worker-api.ts
@@ -0,0 +1,90 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * This file defines the API of syntax-worker, which is a web worker for syntax
+ * highlighting based on the HighlightJS library.
+ *
+ * Workers communicate via `postMessage(e)` and `onMessage(e)` where `e` is a
+ * MessageEvent.
+ *
+ * SyntaxWorker expects incoming messages to be of type
+ * `MessageEvent<SyntaxWorkerMessage>`. And outgoing messages will be of type
+ * `MessageEvent<SyntaxWorkerResult>`.
+ */
+
+/** Type of incoming messages for SyntaxWorker. */
+export enum SyntaxWorkerMessageType {
+ INIT,
+ REQUEST,
+}
+
+/** Incoming message for SyntaxWorker. */
+export interface SyntaxWorkerMessage {
+ type: SyntaxWorkerMessageType;
+}
+
+/**
+ * Requests the worker to import the HighlightJS lib from the given URL and
+ * initializes and configures it. Has to be called once before you can send
+ * a SyntaxWorkerRequest message.
+ */
+export interface SyntaxWorkerInit extends SyntaxWorkerMessage {
+ type: SyntaxWorkerMessageType.INIT;
+ url: string;
+}
+
+export function isInit(
+ x: SyntaxWorkerMessage | undefined
+): x is SyntaxWorkerInit {
+ return !!x && x.type === SyntaxWorkerMessageType.INIT;
+}
+
+/**
+ * Requests the worker to highlight the given code. The worker must have been
+ * initialized before.
+ */
+export interface SyntaxWorkerRequest extends SyntaxWorkerMessage {
+ type: SyntaxWorkerMessageType.REQUEST;
+ language: string;
+ code: string;
+}
+
+export function isRequest(
+ x: SyntaxWorkerMessage | undefined
+): x is SyntaxWorkerRequest {
+ return !!x && x.type === SyntaxWorkerMessageType.REQUEST;
+}
+
+/** Type of outgoing messages of SyntaxWorker. */
+export interface SyntaxWorkerResult {
+ /** Unset or undefined means "success". */
+ error?: string;
+ /**
+ * Returned by SyntaxWorkerRequest calls. Every line gets its own array of
+ * ranges. `ranges[0]` are the ranges for line 1. Every line has an array,
+ * which may be empty. All ranges are guaranteed to be closed.
+ */
+ ranges?: SyntaxLayerLine[];
+}
+
+/** Ranges for one line. */
+export interface SyntaxLayerLine {
+ ranges: SyntaxLayerRange[];
+}
+
+/** Can be used for `length` in SyntaxLayerRange. */
+export const UNCLOSED = -1;
+
+/** Range of characters in a line to be syntax highlighted. */
+export interface SyntaxLayerRange {
+ /** 0-based inclusive. */
+ start: number;
+ /** Can only be UNCLOSED during processing. */
+ length: number;
+ /** HighlightJS specific names, e.g. 'literal'. */
+ className: string;
+}
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index c6137eb..ab78015 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -166,7 +166,7 @@
languageName: string,
code: string,
ignore_illegals: boolean,
- continuation: unknown
+ continuation?: unknown
): HighlightJSResult;
}
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index 411421e..605241a 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -45,7 +45,7 @@
const bFile = b.substr(0, bLastDotIndex) || b;
// Sort header files above others with the same base name.
- const headerExts = ['h', 'hxx', 'hpp'];
+ const headerExts = ['h', 'hh', 'hxx', 'hpp'];
if (aFile.length > 0 && aFile === bFile) {
if (headerExts.includes(aExt) && headerExts.includes(bExt)) {
return a.localeCompare(b);
diff --git a/polygerrit-ui/app/utils/path-list-util_test.ts b/polygerrit-ui/app/utils/path-list-util_test.ts
index 79b5f09..eb071a5 100644
--- a/polygerrit-ui/app/utils/path-list-util_test.ts
+++ b/polygerrit-ui/app/utils/path-list-util_test.ts
@@ -79,6 +79,12 @@
['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']
);
+ // Regression test for Issue 15635
+ assert.deepEqual(
+ ['manager.cc', 'manager.hh'].sort(specialFilePathCompare),
+ ['manager.hh', 'manager.cc']
+ );
+
// Regression test for Issue 4448.
assert.deepEqual(
[
diff --git a/polygerrit-ui/app/utils/hljs-util.ts b/polygerrit-ui/app/utils/syntax-util.ts
similarity index 88%
rename from polygerrit-ui/app/utils/hljs-util.ts
rename to polygerrit-ui/app/utils/syntax-util.ts
index 1bd2072..318d46a 100644
--- a/polygerrit-ui/app/utils/hljs-util.ts
+++ b/polygerrit-ui/app/utils/syntax-util.ts
@@ -3,11 +3,16 @@
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+import {
+ SyntaxLayerLine,
+ SyntaxLayerRange,
+ UNCLOSED,
+} from '../types/syntax-worker-api';
/**
* Utilities related to working with the HighlightJS syntax highlighting lib.
*
- * Note that this utility is mostly used by the hljs-worker, which is a Web
+ * Note that this utility is mostly used by the syntax-worker, which is a Web
* Worker and can thus not depend on document, the DOM or any related
* functionality.
*/
@@ -20,19 +25,6 @@
const openingSpan = new RegExp('<span class="(.*?)">');
const closingSpan = new RegExp('</span>');
-/** Can be used for `length` in SyntaxLayerRange. */
-const UNCLOSED = -1;
-
-/** Range of characters in a line to be syntax highlighted. */
-export interface SyntaxLayerRange {
- /** 1-based inclusive. */
- start: number;
- /** Can only be UNCLOSED during processing. */
- length: number;
- /** HighlightJS specific names, e.g. 'literal'. */
- className: string;
-}
-
/**
* HighlightJS produces one long HTML string with HTML elements spanning
* multiple lines. <gr-diff> is line based, needs all elements closed at the end
@@ -44,16 +36,16 @@
*/
export function highlightedStringToRanges(
highlightedCode: string
-): SyntaxLayerRange[][] {
+): SyntaxLayerLine[] {
// What the function eventually returns.
- const rangesPerLine: SyntaxLayerRange[][] = [];
+ const rangesPerLine: SyntaxLayerLine[] = [];
// The unclosed ranges that are carried over from one line to the next.
let carryOverRanges: SyntaxLayerRange[] = [];
for (let line of highlightedCode.split('\n')) {
const ranges: SyntaxLayerRange[] = [...carryOverRanges];
carryOverRanges = [];
- rangesPerLine.push(ranges);
+ rangesPerLine.push({ranges});
// Remove all span tags one after another from left to right.
// For each opening <span ...> push a new (unclosed) range.
diff --git a/polygerrit-ui/app/utils/hljs-util_test.ts b/polygerrit-ui/app/utils/syntax-util_test.ts
similarity index 62%
rename from polygerrit-ui/app/utils/hljs-util_test.ts
rename to polygerrit-ui/app/utils/syntax-util_test.ts
index 3c577ca..fef908a 100644
--- a/polygerrit-ui/app/utils/hljs-util_test.ts
+++ b/polygerrit-ui/app/utils/syntax-util_test.ts
@@ -4,14 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../test/common-test-setup-karma';
-import './hljs-util';
+import './syntax-util';
import {
highlightedStringToRanges,
removeFirstSpan,
SpanType,
-} from './hljs-util';
+} from './syntax-util';
-suite('file hljs-util', () => {
+suite('file syntax-util', () => {
suite('function removeFirstSpan()', () => {
test('no matches', async () => {
assert.isUndefined(removeFirstSpan(''));
@@ -44,23 +44,26 @@
suite('function highlightedStringToRanges()', () => {
test('no ranges', async () => {
- assert.deepEqual(highlightedStringToRanges(''), [[]]);
- assert.deepEqual(highlightedStringToRanges('\n'), [[], []]);
+ assert.deepEqual(highlightedStringToRanges(''), [{ranges: []}]);
+ assert.deepEqual(highlightedStringToRanges('\n'), [
+ {ranges: []},
+ {ranges: []},
+ ]);
assert.deepEqual(highlightedStringToRanges('asdf\nasdf\nasdf'), [
- [],
- [],
- [],
+ {ranges: []},
+ {ranges: []},
+ {ranges: []},
]);
});
test('one line, one span', async () => {
assert.deepEqual(
highlightedStringToRanges('asdf<span class="c">qwer</span>asdf'),
- [[{start: 4, length: 4, className: 'c'}]]
+ [{ranges: [{start: 4, length: 4, className: 'c'}]}]
);
assert.deepEqual(
highlightedStringToRanges('<span class="d">asdfqwer</span>'),
- [[{start: 0, length: 8, className: 'd'}]]
+ [{ranges: [{start: 0, length: 8, className: 'd'}]}]
);
});
@@ -70,10 +73,12 @@
'asdf<span class="c">qwer</span>zxcv<span class="d">qwer</span>asdf'
),
[
- [
- {start: 4, length: 4, className: 'c'},
- {start: 12, length: 4, className: 'd'},
- ],
+ {
+ ranges: [
+ {start: 4, length: 4, className: 'c'},
+ {start: 12, length: 4, className: 'd'},
+ ],
+ },
]
);
});
@@ -84,10 +89,12 @@
'asdf<span class="c">qwer<span class="d">zxcv</span>qwer</span>asdf'
),
[
- [
- {start: 4, length: 12, className: 'c'},
- {start: 8, length: 4, className: 'd'},
- ],
+ {
+ ranges: [
+ {start: 4, length: 12, className: 'c'},
+ {start: 8, length: 4, className: 'd'},
+ ],
+ },
]
);
});
@@ -99,8 +106,8 @@
'asd<span class="d">qwe</span>asd'
),
[
- [{start: 4, length: 4, className: 'c'}],
- [{start: 3, length: 3, className: 'd'}],
+ {ranges: [{start: 4, length: 4, className: 'c'}]},
+ {ranges: [{start: 3, length: 3, className: 'd'}]},
]
);
});
@@ -111,8 +118,8 @@
'asdf<span class="c">qwer\n' + 'asdf</span>qwer'
),
[
- [{start: 4, length: 4, className: 'c'}],
- [{start: 0, length: 4, className: 'c'}],
+ {ranges: [{start: 4, length: 4, className: 'c'}]},
+ {ranges: [{start: 0, length: 4, className: 'c'}]},
]
);
});
@@ -124,14 +131,18 @@
'asdf</span>qwer</span>zxcv'
),
[
- [
- {start: 4, length: 8, className: 'c'},
- {start: 8, length: 4, className: 'd'},
- ],
- [
- {start: 0, length: 8, className: 'c'},
- {start: 0, length: 4, className: 'd'},
- ],
+ {
+ ranges: [
+ {start: 4, length: 8, className: 'c'},
+ {start: 8, length: 4, className: 'd'},
+ ],
+ },
+ {
+ ranges: [
+ {start: 0, length: 8, className: 'c'},
+ {start: 0, length: 4, className: 'd'},
+ ],
+ },
]
);
});
@@ -145,16 +156,20 @@
'asdf</span>qwer'
),
[
- [{start: 4, length: 4, className: 'c'}],
- [
- {start: 0, length: 8, className: 'c'},
- {start: 4, length: 4, className: 'd'},
- ],
- [
- {start: 0, length: 8, className: 'c'},
- {start: 0, length: 4, className: 'd'},
- ],
- [{start: 0, length: 4, className: 'c'}],
+ {ranges: [{start: 4, length: 4, className: 'c'}]},
+ {
+ ranges: [
+ {start: 0, length: 8, className: 'c'},
+ {start: 4, length: 4, className: 'd'},
+ ],
+ },
+ {
+ ranges: [
+ {start: 0, length: 8, className: 'c'},
+ {start: 0, length: 4, className: 'd'},
+ ],
+ },
+ {ranges: [{start: 0, length: 4, className: 'c'}]},
]
);
});
diff --git a/polygerrit-ui/app/utils/worker-util.ts b/polygerrit-ui/app/utils/worker-util.ts
new file mode 100644
index 0000000..e4f5d2e
--- /dev/null
+++ b/polygerrit-ui/app/utils/worker-util.ts
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * We cannot import the worker script from cdn directly, because that is
+ * creating cross-origin issues. Instead we have to create a worker script on
+ * the fly and pull the actual worker via `importScripts()`. Apparently that
+ * is a well established pattern.
+ */
+function wrapUrl(url: string) {
+ const content = `importScripts("${url}");`;
+ return URL.createObjectURL(new Blob([content], {type: 'text/javascript'}));
+}
+
+export function createWorker(workerUrl: string): Worker {
+ return new Worker(wrapUrl(workerUrl));
+}
diff --git a/polygerrit-ui/app/workers/syntax-worker.ts b/polygerrit-ui/app/workers/syntax-worker.ts
new file mode 100644
index 0000000..85a0fb4
--- /dev/null
+++ b/polygerrit-ui/app/workers/syntax-worker.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {HighlightJS} from '../types/types';
+import {
+ SyntaxWorkerMessage,
+ SyntaxWorkerResult,
+ isRequest,
+ isInit,
+} from '../types/syntax-worker-api';
+import {highlightedStringToRanges} from '../utils/syntax-util';
+
+// This is an entry point file of a bundle. Keep free of exports!
+
+/**
+ * This is a web worker for calling the HighlightJS library for syntax
+ * highlighting. Files can be large and highlighting does not require
+ * the `document` or the `DOM`, so it is a perfect fit for a web worker.
+ *
+ * This file is a just a hub hooking into the web worker API. The message
+ * events for communicating with the main app are defined in the file
+ * `types/worker-api.ts`. And the `meat` of the computation is done in the
+ * file `syntax-util.ts`.
+ */
+
+/**
+ * `self` is for a worker what `window` is for the web app. It is called
+ * the `DedicatedWorkerGlobalScope`, see
+ * https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope
+ *
+ * Once imported the HighlightJS lib exposes its functionality via the global
+ * `hljs` variable.
+ */
+type Context = Worker & {hljs?: HighlightJS};
+const ctx: Context = self as unknown as Context;
+
+/**
+ * We are encapsulating the web worker API here, so this is the only place
+ * where you need to know about it and the MessageEvents in this file.
+ */
+ctx.onmessage = function (e: MessageEvent<SyntaxWorkerMessage>) {
+ try {
+ const message = e.data;
+ if (isInit(message)) {
+ worker.init(message.url);
+ const result: SyntaxWorkerResult = {ranges: []};
+ ctx.postMessage(result);
+ }
+ if (isRequest(message)) {
+ const ranges = worker.highlight(message.language, message.code);
+ const result: SyntaxWorkerResult = {ranges};
+ ctx.postMessage(result);
+ }
+ } catch (err) {
+ let error = 'syntax worker error';
+ if (err instanceof Error) error = err.message;
+ const result: SyntaxWorkerResult = {error, ranges: []};
+ ctx.postMessage(result);
+ }
+};
+
+class SyntaxWorker {
+ private highlightJsLib?: HighlightJS;
+
+ init(highlightJsLibUrl: string) {
+ importScripts(highlightJsLibUrl);
+ if (!ctx.hljs) {
+ throw new Error('HighlightJS lib not available after import');
+ }
+ this.highlightJsLib = ctx.hljs;
+ this.highlightJsLib.configure({classPrefix: ''});
+ }
+
+ highlight(language: string, code: string) {
+ if (!this.highlightJsLib) throw new Error('worker not initialized');
+ const highlight = this.highlightJsLib.highlight(language, code, true);
+ return highlightedStringToRanges(highlight.value);
+ }
+}
+
+/** Singleton instance being referenced in `onmessage` function above. */
+const worker = new SyntaxWorker();
diff --git a/tools/deps.bzl b/tools/deps.bzl
index abb90b3..62049e7 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -3,13 +3,13 @@
CAFFEINE_VERS = "2.9.2"
ANTLR_VERS = "3.5.2"
-SLF4J_VERS = "1.7.26"
+SLF4J_VERS = "1.7.33"
COMMONMARK_VERS = "0.10.0"
-FLEXMARK_VERS = "0.50.42"
+FLEXMARK_VERS = "0.50.50"
GREENMAIL_VERS = "1.5.5"
MAIL_VERS = "1.6.0"
MIME4J_VERS = "0.8.1"
-OW2_VERS = "9.0"
+OW2_VERS = "9.2"
AUTO_VALUE_VERSION = "1.7.4"
AUTO_VALUE_GSON_VERSION = "1.3.1"
PROLOG_VERS = "1.4.4"
@@ -114,25 +114,19 @@
maven_jar(
name = "log-api",
artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
- sha1 = "77100a62c2e6f04b53977b9f541044d7d722693d",
+ sha1 = "d375aa1b98d34d5ddf73a3f19eaad66e98975b12",
)
maven_jar(
name = "log-ext",
artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
- sha1 = "31cdf122e000322e9efcb38913e9ab07825b17ef",
+ sha1 = "00da03640ae1ad57f964dcaa542fb5d804dce8a6",
)
maven_jar(
name = "jcl-over-slf4j",
artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
- sha1 = "33fbc2d93de829fa5e263c5ce97f5eab8f57d53e",
- )
-
- maven_jar(
- name = "log4j",
- artifact = "log4j:log4j:1.2.17",
- sha1 = "5af35056b4d257e4b64b9e8069c0746e8b08629f",
+ sha1 = "28c441128bc81b6d95cc2857ae5bb46ae5bf658b",
)
maven_jar(
@@ -238,151 +232,151 @@
maven_jar(
name = "flexmark",
artifact = "com.vladsch.flexmark:flexmark:" + FLEXMARK_VERS,
- sha1 = "ed537d7bc31883b008cc17d243a691c7efd12a72",
+ sha1 = "7f61e190cff7e1bea64906408ccfa583b5ac9fc2",
)
maven_jar(
name = "flexmark-ext-abbreviation",
artifact = "com.vladsch.flexmark:flexmark-ext-abbreviation:" + FLEXMARK_VERS,
- sha1 = "dc27c3e7abbc8d2cfb154f41c68645c365bb9d22",
+ sha1 = "8f62f49cfaf8d33391e48a3b79e941d94e444e50",
)
maven_jar(
name = "flexmark-ext-anchorlink",
artifact = "com.vladsch.flexmark:flexmark-ext-anchorlink:" + FLEXMARK_VERS,
- sha1 = "6a8edb0165f695c9c19b7143a7fbd78c25c3b99c",
+ sha1 = "1d530e44b4439abf53ce9dcc784ffa5b9fd6ce89",
)
maven_jar(
name = "flexmark-ext-autolink",
artifact = "com.vladsch.flexmark:flexmark-ext-autolink:" + FLEXMARK_VERS,
- sha1 = "5da7a4d009ea08ef2d8714cc73e54a992c6d2d9a",
+ sha1 = "4026aa60fd6e146c2d4931acb19b2409c6a79b8a",
)
maven_jar(
name = "flexmark-ext-definition",
artifact = "com.vladsch.flexmark:flexmark-ext-definition:" + FLEXMARK_VERS,
- sha1 = "862d17812654624ed81ce8fc89c5ef819ff45f87",
+ sha1 = "bb74d36459e8e34653761aaa0095220b8b7b6cb6",
)
maven_jar(
name = "flexmark-ext-emoji",
artifact = "com.vladsch.flexmark:flexmark-ext-emoji:" + FLEXMARK_VERS,
- sha1 = "f0d7db64cb546798742b1ffc6db316a33f6acd76",
+ sha1 = "36d36cb227f831b81b636d3f901f07db06b8d84d",
)
maven_jar(
name = "flexmark-ext-escaped-character",
artifact = "com.vladsch.flexmark:flexmark-ext-escaped-character:" + FLEXMARK_VERS,
- sha1 = "6fd9ab77619df417df949721cb29c45914b326f8",
+ sha1 = "09f0cef3260b6f6371d6066cf32ce6a7dc2a7922",
)
maven_jar(
name = "flexmark-ext-footnotes",
artifact = "com.vladsch.flexmark:flexmark-ext-footnotes:" + FLEXMARK_VERS,
- sha1 = "e36bd69e43147cc6e19c3f55e4b27c0fc5a3d88c",
+ sha1 = "cdf0b79f026b09c6b87d91e61eb29ada1577aa5c",
)
maven_jar(
name = "flexmark-ext-gfm-issues",
artifact = "com.vladsch.flexmark:flexmark-ext-gfm-issues:" + FLEXMARK_VERS,
- sha1 = "5c825dd4e4fa4f7ccbe30dc92d7e35cdcb8a8c24",
+ sha1 = "e40d347e242e35d4344553120cb9e69c1c975f41",
)
maven_jar(
name = "flexmark-ext-gfm-strikethrough",
artifact = "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:" + FLEXMARK_VERS,
- sha1 = "3256735fd77e7228bf40f7888b4d3dc56787add4",
+ sha1 = "a6948f6e4fc2ec1059d6b53e73d4a30c24f6e05d",
)
maven_jar(
name = "flexmark-ext-gfm-tables",
artifact = "com.vladsch.flexmark:flexmark-ext-gfm-tables:" + FLEXMARK_VERS,
- sha1 = "62f0efcfb974756940ebe749fd4eb01323babc29",
+ sha1 = "92d3eb0c5dc79924448c186d89717f1df853aaa0",
)
maven_jar(
name = "flexmark-ext-gfm-tasklist",
artifact = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:" + FLEXMARK_VERS,
- sha1 = "76d4971ad9ce02f0e70351ab6bd06ad8e405e40d",
+ sha1 = "e6fb4d9e46c96e61a07fbb40cf653db58ed95a95",
)
maven_jar(
name = "flexmark-ext-gfm-users",
artifact = "com.vladsch.flexmark:flexmark-ext-gfm-users:" + FLEXMARK_VERS,
- sha1 = "7b0fc7e42e4da508da167fcf8e1cbf9ba7e21147",
+ sha1 = "44c83bdccfd41a399f3f7b11ba72728382dd3f5a",
)
maven_jar(
name = "flexmark-ext-ins",
artifact = "com.vladsch.flexmark:flexmark-ext-ins:" + FLEXMARK_VERS,
- sha1 = "9e51809867b9c4db0fb1c29599b4574e3d2a78e9",
+ sha1 = "b0d174d86ac2348420993dc2c997fad5f02c68fa",
)
maven_jar(
name = "flexmark-ext-jekyll-front-matter",
artifact = "com.vladsch.flexmark:flexmark-ext-jekyll-front-matter:" + FLEXMARK_VERS,
- sha1 = "44eb6dbb33b3831d3b40af938ddcd99c9c16a654",
+ sha1 = "2bfb31c67fd10af058b90f1b364f881f6276de80",
)
maven_jar(
name = "flexmark-ext-superscript",
artifact = "com.vladsch.flexmark:flexmark-ext-superscript:" + FLEXMARK_VERS,
- sha1 = "35815b8cb91000344d1fe5df21cacde8553d2994",
+ sha1 = "d210b007b46339082f79b6caf632b19ac8efc341",
)
maven_jar(
name = "flexmark-ext-tables",
artifact = "com.vladsch.flexmark:flexmark-ext-tables:" + FLEXMARK_VERS,
- sha1 = "f6768e98c7210b79d5e8bab76fff27eec6db51e6",
+ sha1 = "be77790470aa9bd90011067221504162a1d7b083",
)
maven_jar(
name = "flexmark-ext-toc",
artifact = "com.vladsch.flexmark:flexmark-ext-toc:" + FLEXMARK_VERS,
- sha1 = "1968d038fc6c8156f244f5a7eecb34e7e2f33705",
+ sha1 = "91d6d63ff5b70c3ebae98867bd7a346d71cb0ce1",
)
maven_jar(
name = "flexmark-ext-typographic",
artifact = "com.vladsch.flexmark:flexmark-ext-typographic:" + FLEXMARK_VERS,
- sha1 = "6549b9862b61c4434a855a733237103df9162849",
+ sha1 = "f51e247df80628df509a3af92a1efa1efb83d746",
)
maven_jar(
name = "flexmark-ext-wikilink",
artifact = "com.vladsch.flexmark:flexmark-ext-wikilink:" + FLEXMARK_VERS,
- sha1 = "e105b09dd35aab6e6f5c54dfe062ee59bd6f786a",
+ sha1 = "d14aeb078dbaf3e166621ae9499595a4a57b22ab",
)
maven_jar(
name = "flexmark-ext-yaml-front-matter",
artifact = "com.vladsch.flexmark:flexmark-ext-yaml-front-matter:" + FLEXMARK_VERS,
- sha1 = "b2d3a1e7f3985841062e8d3203617e29c6c21b52",
+ sha1 = "aba328b70e15f2553aac0a74e391edab37bf7b30",
)
maven_jar(
name = "flexmark-formatter",
artifact = "com.vladsch.flexmark:flexmark-formatter:" + FLEXMARK_VERS,
- sha1 = "a50c6cb10f6d623fc4354a572c583de1372d217f",
+ sha1 = "3e4ad3b1be29d41e8c35cadb70761768505f2164",
)
maven_jar(
name = "flexmark-html-parser",
artifact = "com.vladsch.flexmark:flexmark-html-parser:" + FLEXMARK_VERS,
- sha1 = "46c075f30017e131c1ada8538f1d8eacf652b044",
+ sha1 = "3c59b0061c50c9a8a48c46d4f4498d8eba249921",
)
maven_jar(
name = "flexmark-profile-pegdown",
artifact = "com.vladsch.flexmark:flexmark-profile-pegdown:" + FLEXMARK_VERS,
- sha1 = "d9aafd47629959cbeddd731f327ae090fc92b60f",
+ sha1 = "c01a2c2eebe06230858956d45847cee233790d7c",
)
maven_jar(
name = "flexmark-util",
artifact = "com.vladsch.flexmark:flexmark-util:" + FLEXMARK_VERS,
- sha1 = "417a9821d5d80ddacbfecadc6843ae7b259d5112",
+ sha1 = "fdfce72f5eb9ec53085804fd0c8d15f3680ae359",
)
# Transitive dependency of flexmark and gitiles
@@ -425,31 +419,31 @@
maven_jar(
name = "ow2-asm",
artifact = "org.ow2.asm:asm:" + OW2_VERS,
- sha1 = "af582ff60bc567c42d931500c3fdc20e0141ddf9",
+ sha1 = "81a03f76019c67362299c40e0ba13405f5467bff",
)
maven_jar(
name = "ow2-asm-analysis",
artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS,
- sha1 = "4630afefbb43939c739445dde0af1a5729a0fb4e",
+ sha1 = "7487dd756daf96cab9986e44b9d7bcb796a61c10",
)
maven_jar(
name = "ow2-asm-commons",
artifact = "org.ow2.asm:asm-commons:" + OW2_VERS,
- sha1 = "5a34a3a9ac44f362f35d1b27932380b0031a3334",
+ sha1 = "f4d7f0fc9054386f2893b602454d48e07d4fbead",
)
maven_jar(
name = "ow2-asm-tree",
artifact = "org.ow2.asm:asm-tree:" + OW2_VERS,
- sha1 = "9df939f25c556b0c7efe00701d47e77a49837f24",
+ sha1 = "d96c99a30f5e1a19b0e609dbb19a44d8518ac01e",
)
maven_jar(
name = "ow2-asm-util",
artifact = "org.ow2.asm:asm-util:" + OW2_VERS,
- sha1 = "7c059a94ab5eed3347bf954e27fab58e52968848",
+ sha1 = "fbc178fc5ba3dab50fd7e8a5317b8b647c8e8946",
)
maven_jar(
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 272f822..612d897 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -17,6 +17,12 @@
"""
maven_jar(
+ name = "log4j",
+ artifact = "ch.qos.reload4j:reload4j:1.2.18.1",
+ sha1 = "7075022a11e18c1ad230de5be074e0c691fed17b",
+ )
+
+ maven_jar(
name = "j2objc",
artifact = "com.google.j2objc:j2objc-annotations:1.1",
sha1 = "ed28ded51a8b1c6b112568def5f4b455e6809019",
diff --git a/tools/platforms/Dockerfile b/tools/platforms/Dockerfile
new file mode 100644
index 0000000..157529c
--- /dev/null
+++ b/tools/platforms/Dockerfile
@@ -0,0 +1,21 @@
+# 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.
+
+FROM gcr.io/cloud-marketplace/google/rbe-ubuntu18-04:latest
+
+# Install Git >=2.18.0
+RUN add-apt-repository ppa:git-core/ppa && \
+ apt-get -y update && \
+ apt-get -y install git && \
+ apt-get clean