Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Fix bazel build on Mac M1 (aarch64)
  Update git submodules
  Fix threading issue in Diff cache
  Use original servlet-api 3.1.0 artefact instead of tomcat's copy
  Cache change /meta ref SHA1 for each REST API request
  Validation on Invalid Filter Expression in User-Set Notifications
  Update git submodules
  dev-release: Correct link to public-keys.md
  Doc: Remove reference to "extra" keyserver keyserver.ubuntu.com
  Update existing change on cherry-pick with CommitApi
  Bazel: Bump rules_nodejs version to 5.1.0
  Bump rules_nodejs to version 3.0.0

Change-Id: Icf6a244628ed49db5eeac6ffa9919ed3b86c7af3
Release-Notes: skip
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index a4ccccf..db08da5 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -44,15 +44,13 @@
 +
 Generate and publish a PGP key as described in
 link:http://central.sonatype.org/pages/working-with-pgp-signatures.html[
-Working with PGP Signatures,role=external,window=_blank]. In addition to the keyserver mentioned
-there it is recommended to also publish the key to the
-link:https://keyserver.ubuntu.com/[Ubuntu key server].
+Working with PGP Signatures,role=external,window=_blank].
 +
 Please be aware that after publishing your public key it may take a
 while until it is visible to the Sonatype server.
 +
 Add an entry for the public key in the
-link:https://gerrit.googlesource.com/homepage/+/md-pages/releases/public-keys.md[key list,role=external,window=_blank]
+link:https://gerrit.googlesource.com/homepage/+/master/pages/site/releases/public-keys.md[key list,role=external,window=_blank]
 on the homepage.
 +
 The PGP passphrase can be put in `~/.m2/settings.xml`:
diff --git a/WORKSPACE b/WORKSPACE
index 51068e0..fe6b94e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -34,6 +34,15 @@
 load("//tools:deps.bzl", "CAFFEINE_VERS", "java_dependencies")
 
 http_archive(
+    name = "platforms",
+    sha256 = "379113459b0feaf6bfbb584a91874c065078aa673222846ac765f86661c27407",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.5/platforms-0.0.5.tar.gz",
+        "https://github.com/bazelbuild/platforms/releases/download/0.0.5/platforms-0.0.5.tar.gz",
+    ],
+)
+
+http_archive(
     name = "rbe_jdk11",
     sha256 = "5939e2a4e56d1fc53b6c44c6db97ee068c9f4bd18e86c762f6ab8b4fff5e294b",
     strip_prefix = "rbe_autoconfig-3.0.0",
diff --git a/antlr3/BUILD b/antlr3/BUILD
index 549946a..23641e3 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -22,6 +22,7 @@
     srcs = [":query"],
     visibility = [
         "//java/com/google/gerrit/index:__subpackages__",
+        "//java/com/google/gerrit/server:__subpackages__",
         "//javatests/com/google/gerrit:__subpackages__",
         "//javatests/com/google/gerrit/index:__pkg__",
         "//plugins:__pkg__",
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index b7dd2f4..f38653d 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -361,9 +361,9 @@
     try (TraceContext traceContext = enableTracing(req, res)) {
       String requestUri = requestUri(req);
 
-      try (PerThreadCache ignored = PerThreadCache.create()) {
+      try (PerThreadCache ignored = PerThreadCache.create(req)) {
         List<IdString> path = splitPath(req);
-        RequestInfo requestInfo = createRequestInfo(traceContext, requestUri, path);
+        RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
         globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
 
         // It's important that the PerformanceLogContext is closed before the response is sent to
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index ceec55c..a190ebf 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -301,7 +301,7 @@
     move(jars, "javax.inject-1.jar", extapi);
     move(jars, "aopalliance-1.0.jar", extapi);
     move(jars, "guice-servlet-", extapi);
-    move(jars, "tomcat-servlet-api-", extapi);
+    move(jars, "servlet-api-", extapi);
 
     ClassLoader parent = ClassLoader.getSystemClassLoader();
     if (!extapi.isEmpty()) {
diff --git a/java/com/google/gerrit/server/cache/PerThreadCache.java b/java/com/google/gerrit/server/cache/PerThreadCache.java
index ef00b80..4270d1e 100644
--- a/java/com/google/gerrit/server/cache/PerThreadCache.java
+++ b/java/com/google/gerrit/server/cache/PerThreadCache.java
@@ -21,7 +21,9 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Supplier;
+import javax.servlet.http.HttpServletRequest;
 
 /**
  * Caches object instances for a request as {@link ThreadLocal} in the serving thread.
@@ -58,6 +60,12 @@
   private static final int PER_THREAD_CACHE_SIZE = 25;
 
   /**
+   * Optional HTTP request associated with the per-thread cache, should the thread be associated
+   * with the incoming HTTP thread pool.
+   */
+  private final Optional<HttpServletRequest> httpRequest;
+
+  /**
    * Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's
    * class and a list of identifiers that in combination uniquely set the object apart form others
    * of the same class.
@@ -102,9 +110,9 @@
     }
   }
 
-  public static PerThreadCache create() {
+  public static PerThreadCache create(@Nullable HttpServletRequest httpRequest) {
     checkState(CACHE.get() == null, "called create() twice on the same request");
-    PerThreadCache cache = new PerThreadCache();
+    PerThreadCache cache = new PerThreadCache(httpRequest);
     CACHE.set(cache);
     return cache;
   }
@@ -121,7 +129,9 @@
 
   private final Map<Key<?>, Object> cache = Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE);
 
-  private PerThreadCache() {}
+  private PerThreadCache(@Nullable HttpServletRequest req) {
+    httpRequest = Optional.ofNullable(req);
+  }
 
   /**
    * Returns an instance of {@code T} that was either loaded from the cache or obtained from the
@@ -139,6 +149,19 @@
     return value;
   }
 
+  /** Returns the optional HTTP request associated with the local thread cache. */
+  public Optional<HttpServletRequest> getHttpRequest() {
+    return httpRequest;
+  }
+
+  /** Returns true if there is an HTTP request associated and is a GET or HEAD */
+  public boolean hasReadonlyRequest() {
+    return httpRequest
+        .map(HttpServletRequest::getMethod)
+        .filter(m -> m.equalsIgnoreCase("GET") || m.equalsIgnoreCase("HEAD"))
+        .isPresent();
+  }
+
   @Override
   public void close() {
     CACHE.remove();
diff --git a/java/com/google/gerrit/server/git/RepoRefCache.java b/java/com/google/gerrit/server/git/RepoRefCache.java
index c69f9a6..7f22111 100644
--- a/java/com/google/gerrit/server/git/RepoRefCache.java
+++ b/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.server.cache.PerThreadCache;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
@@ -29,6 +30,17 @@
   private final RefDatabase refdb;
   private final Map<String, Optional<ObjectId>> ids;
 
+  public static Optional<RefCache> getOptional(Repository repo) {
+    PerThreadCache cache = PerThreadCache.get();
+    if (cache != null && cache.hasReadonlyRequest()) {
+      return Optional.of(
+          cache.get(
+              PerThreadCache.Key.create(RepoRefCache.class, repo), () -> new RepoRefCache(repo)));
+    }
+
+    return Optional.empty();
+  }
+
   public RepoRefCache(Repository repo) {
     this.refdb = repo.getRefDatabase();
     this.ids = new HashMap<>();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index ca636e8..53a5739 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -57,6 +57,7 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.RefCache;
+import com.google.gerrit.server.git.RepoRefCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -702,6 +703,10 @@
 
   @Override
   protected ObjectId readRef(Repository repo) throws IOException {
-    return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo);
+    Optional<RefCache> refsCache =
+        Optional.ofNullable(refs).map(Optional::of).orElse(RepoRefCache.getOptional(repo));
+    return refsCache.isPresent()
+        ? refsCache.get().get(getRefName()).orElse(null)
+        : super.readRef(repo);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 2864391..2566b72 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -8,6 +8,7 @@
     name = "restapi",
     srcs = glob(["**/*.java"]),
     deps = [
+        "//antlr3:query_parser",
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
@@ -29,6 +30,7 @@
         "//lib:guava",
         "//lib:jgit",
         "//lib:servlet-api",
+        "//lib/antlr:java-runtime",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/commons:compress",
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index 5979b2a..2131070 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.QueryParser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountResource;
@@ -95,6 +98,17 @@
         throw new BadRequestException("project name must be specified");
       }
 
+      if (!Strings.isNullOrEmpty(info.filter)) {
+        try {
+          QueryParser.parse(info.filter);
+        } catch (QueryParseException e) {
+          throw new BadRequestException(
+              String.format(
+                  "invalid filter expression for project %s: %s", info.project, e.getMessage()),
+              e);
+        }
+      }
+
       ProjectWatchKey key =
           ProjectWatchKey.create(projectsCollection.parse(info.project).getNameKey(), info.filter);
       if (m.containsKey(key)) {
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index b3b2f5a..c708e09 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -7,8 +7,11 @@
     deps = [
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//javatests/com/google/gerrit/util/http/testutil",
         "//lib:junit",
         "//lib/truth",
+        "//lib/truth:truth-java8-extension",
+        "@servlet-api//jar",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
index 5d420d3..c04deb4 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -15,9 +15,13 @@
 package com.google.gerrit.server.cache;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import java.util.function.Supplier;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
 import org.junit.Test;
 
 public class PerThreadCacheTest {
@@ -43,7 +47,7 @@
 
   @Test
   public void endToEndCache() {
-    try (PerThreadCache ignored = PerThreadCache.create()) {
+    try (PerThreadCache ignored = PerThreadCache.create(null)) {
       PerThreadCache cache = PerThreadCache.get();
       PerThreadCache.Key<String> key1 = PerThreadCache.Key.create(String.class);
 
@@ -61,7 +65,7 @@
   @Test
   public void cleanUp() {
     PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class);
-    try (PerThreadCache ignored = PerThreadCache.create()) {
+    try (PerThreadCache ignored = PerThreadCache.create(null)) {
       PerThreadCache cache = PerThreadCache.get();
       String value1 = cache.get(key, () -> "value1");
       assertThat(value1).isEqualTo("value1");
@@ -69,7 +73,7 @@
 
     // Create a second cache and assert that it is not connected to the first one.
     // This ensures that the cleanup is actually working.
-    try (PerThreadCache ignored = PerThreadCache.create()) {
+    try (PerThreadCache ignored = PerThreadCache.create(null)) {
       PerThreadCache cache = PerThreadCache.get();
       String value1 = cache.get(key, () -> "value2");
       assertThat(value1).isEqualTo("value2");
@@ -78,16 +82,48 @@
 
   @Test
   public void doubleInstantiationFails() {
-    try (PerThreadCache ignored = PerThreadCache.create()) {
+    try (PerThreadCache ignored = PerThreadCache.create(null)) {
       IllegalStateException thrown =
-          assertThrows(IllegalStateException.class, () -> PerThreadCache.create());
+          assertThrows(IllegalStateException.class, () -> PerThreadCache.create(null));
       assertThat(thrown).hasMessageThat().contains("called create() twice on the same request");
     }
   }
 
   @Test
+  public void isAssociatedWithHttpReadonlyRequest() {
+    HttpServletRequest getRequest = new FakeHttpServletRequest();
+    try (PerThreadCache cache = PerThreadCache.create(getRequest)) {
+      assertThat(cache.getHttpRequest()).hasValue(getRequest);
+      assertThat(cache.hasReadonlyRequest()).isTrue();
+    }
+  }
+
+  @Test
+  public void isAssociatedWithHttpWriteRequest() {
+    HttpServletRequest putRequest =
+        new HttpServletRequestWrapper(new FakeHttpServletRequest()) {
+          @Override
+          public String getMethod() {
+            return "PUT";
+          }
+        };
+    try (PerThreadCache cache = PerThreadCache.create(putRequest)) {
+      assertThat(cache.getHttpRequest()).hasValue(putRequest);
+      assertThat(cache.hasReadonlyRequest()).isFalse();
+    }
+  }
+
+  @Test
+  public void isNotAssociatedWithHttpRequest() {
+    try (PerThreadCache cache = PerThreadCache.create(null)) {
+      assertThat(cache.getHttpRequest()).isEmpty();
+      assertThat(cache.hasReadonlyRequest()).isFalse();
+    }
+  }
+
+  @Test
   public void enforceMaxSize() {
-    try (PerThreadCache cache = PerThreadCache.create()) {
+    try (PerThreadCache cache = PerThreadCache.create(null)) {
       // Fill the cache
       for (int i = 0; i < 50; i++) {
         PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i);
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index e298c65..084befa 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -246,6 +246,7 @@
     ],
     ignore = ".eslintignore",
     plugins = [
+        "@npm//@typescript-eslint/eslint-plugin",
         "@npm//eslint-config-google",
         "@npm//eslint-plugin-html",
         "@npm//eslint-plugin-import",
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 81708da..df13f73 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -62,8 +62,8 @@
 
     maven_jar(
         name = "servlet-api",
-        artifact = "org.apache.tomcat:tomcat-servlet-api:8.5.23",
-        sha1 = "021a212688ec94fe77aff74ab34cc74f6f940e60",
+        artifact = "javax.servlet:javax.servlet-api:3.1.0",
+        sha1 = "3cd63d075497751784b2fa84be59432f4905bf7c",
     )
 
     # JGit's transitive dependencies
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 669ab8d..3f9d3a4 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -2385,7 +2385,7 @@
   resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
   integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
 
-cacheable-request@^7.0.1:
+cacheable-request@^7.0.2:
   version "7.0.2"
   resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27"
   integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==
@@ -4309,16 +4309,16 @@
   integrity sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg==
 
 got@^11.8.2:
-  version "11.8.2"
-  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
-  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
+  version "11.8.3"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770"
+  integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==
   dependencies:
     "@sindresorhus/is" "^4.0.0"
     "@szmarczak/http-timer" "^4.0.5"
     "@types/cacheable-request" "^6.0.1"
     "@types/responselike" "^1.0.0"
     cacheable-lookup "^5.0.3"
-    cacheable-request "^7.0.1"
+    cacheable-request "^7.0.2"
     decompress-response "^6.0.0"
     http2-wrapper "^1.0.0-beta.5.2"
     lowercase-keys "^2.0.0"
@@ -8597,11 +8597,16 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
-tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.8.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
+tslib@^1.9.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
+  integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
+
 tsutils@3.21.0:
   version "3.21.0"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
diff --git a/yarn.lock b/yarn.lock
index e6fa177..406f567 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -38,9 +38,9 @@
     regenerator-runtime "^0.13.4"
 
 "@bazel/concatjs@^5.1.0":
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/@bazel/concatjs/-/concatjs-5.1.0.tgz#f4321dec4a225c3ceac41b2dc7ec7c3dd3dd5e21"
-  integrity sha512-sj+vxHVB/swh7awOfQ37h3p/gxSPgLSnUkDt6POrj26qkfi7HrLB1ZkWAPFIIxjEhsBp1LchoHiezjw2GylZQg==
+  version "5.4.0"
+  resolved "https://registry.yarnpkg.com/@bazel/concatjs/-/concatjs-5.4.0.tgz#04e752a6ea3e684f00879e6683657c4ede72df6e"
+  integrity sha512-jlupaDKxqFS3B1lttOIgkKxirP7v5Qx7KCFtOXO7JxtvYJD/qKtKXEQggTrGKJqLPyiZlNiYimHHGICLSWIZcQ==
   dependencies:
     protobufjs "6.8.8"
     source-map-support "0.5.9"