Merge "Update jgit to c6b0ee04e49c96e0beec4154196c416abcf2bcc9" into stable-3.3
diff --git a/.bazelrc b/.bazelrc
index 5b0fa81..72138d2 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -2,7 +2,7 @@
 build --repository_cache=~/.gerritcodereview/bazel-cache/repository
 build --action_env=PATH
 build --disk_cache=~/.gerritcodereview/bazel-cache/cas
-build --java_toolchain=//tools:error_prone_warnings_toolchain_java11
+build --java_toolchain=//tools:error_prone_warnings_toolchain
 
 # Enable strict_action_env flag to. For more information on this feature see
 # https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk.
@@ -13,6 +13,6 @@
 
 test --build_tests_only
 test --test_output=errors
-test --java_toolchain=//tools:error_prone_warnings_toolchain_java11
+test --java_toolchain=//tools:error_prone_warnings_toolchain
 
 import %workspace%/tools/remote-bazelrc
diff --git a/.bazelversion b/.bazelversion
index 6aba2b2..af8c8ec 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-4.2.0
+4.2.2
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 8a4845c..0a06c28 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -11,7 +11,7 @@
   [--user <NAME> | -u <NAME>]
   [--owned]
   [--visible-to-all]
-  [-q <GROUP>]
+  [-g <GROUP>]
   [--verbose | -v]
 --
 
@@ -67,8 +67,8 @@
 	(groups that are explicitly marked as visible to all registered
 	users).
 
--q::
-	Group that should be inspected. The `-q` option can be specified
+-g::
+	Group that should be inspected. The `-g` option can be specified
 	multiple times to define several groups to be inspected. If
 	specified the listed groups will only contain groups that were
 	specified to be inspected. This is e.g. useful in combination with
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 10baed2..085a5d5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -256,8 +256,8 @@
 if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
 the password in the request is first checked against the HTTP password and, if
 it does not match, it is then validated against the LDAP password.
-Service users that only exist in the Gerrit database are authenticated by their
-HTTP passwords.
+Service users that are link:cmd-create-account.html[internal-only] are
+authenticated by their HTTP passwords.
 
 * `LDAP_BIND`
 +
@@ -611,7 +611,7 @@
 single call would trigger a full LDAP authentication and groups resolution
 which could introduce a noticeable latency on the overall execution
 and produce unwanted load to the LDAP server.
-+
+
 [[auth.gitOAuthProvider]]auth.gitOAuthProvider::
 +
 Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
@@ -677,8 +677,10 @@
 [[auth.autoUpdateAccountActiveStatus]]auth.autoUpdateAccountActiveStatus::
 +
 Whether to allow automatic synchronization of an account's inactive flag upon login.
++
 If set to true, upon login, if the authentication back-end reports the account as active,
-the account's inactive flag in the internal Gerrit database will be updated to be active.
+the account's inactive flag in NoteDb will be updated to be active.
++
 If the authentication back-end reports the account as inactive, the account's flag will be
 updated to be inactive and the login attempt will be blocked. Users enabling this feature
 should ensure that their authentication back-end is supported. Currently, only
@@ -938,8 +940,8 @@
 this cache should be disabled in a cluster setup using multiple primary
 or multiple replica nodes.
 +
-The cache should be flushed whenever the database changes table is modified
-outside of Gerrit.
+The cache should be flushed whenever NoteDb change metadata in a repository is
+modified outside of Gerrit.
 
 cache `"diff"`::
 +
@@ -1257,7 +1259,7 @@
 Whether the first user that logs in to the Gerrit server should
 automatically be added to the administrator group and hence get the
 `administrateServer` capability assigned. This is useful to bootstrap
-the authentication database.
+the link:config-accounts.html[account data].
 +
 Default is true.
 
@@ -2192,7 +2194,7 @@
 By default unset, as the HTTP daemon must be configured externally
 by the system administrator, and might not even be running on the
 same host as Gerrit.
-+
+
 [[gerrit.installBatchModule]]gerrit.installBatchModule::
 +
 Repeatable list of class name of additional Guice modules to load as
@@ -2202,7 +2204,7 @@
 located under the `/lib` directory.
 +
 By default unset.
-+
+
 [[gerrit.installDbModule]]gerrit.installDbModule::
 +
 Repeatable list of class name of additional Guice modules to load at
@@ -4094,18 +4096,6 @@
 +
 Default is 5 minutes.
 
-[[receive.changeUpdateThreads]]receive.changeUpdateThreads::
-+
-Number of threads to perform change creation or patch set updates
-concurrently. Each thread uses its own database connection from
-the database connection pool, and if all threads are busy then
-main receive thread will also perform a change creation or patch
-set update.
-+
-Defaults to 1, using only the main receive thread. This feature is for
-databases with very high latency that can benefit from concurrent
-operations when multiple changes are impacted at once.
-
 [[receive.checkMagicRefs]]receive.checkMagicRefs::
 +
 If true, Gerrit will verify the destination repository has
@@ -4327,9 +4317,8 @@
 
 [[repository.name.ownerGroup]]repository.<name>.ownerGroup::
 +
-A name of a group which exists in the database. Zero, one or many
-groups are allowed.  Each on its own line.  Groups which don't exist
-in the database are ignored.
+A name of a link:config-groups.html[group] which exists. Zero, one or many
+groups are allowed.  Each on its own line.  Groups which don't exist are ignored.
 
 [[retry]]
 === Section retry
@@ -5153,6 +5142,7 @@
 used for suggesting accounts when adding members to a group.
 +
 By default 0.
+
 [[suggest.relevantChanges]]suggest.relevantChanges::
 +
 When suggesting reviewers, we go over recent changes of the user, and
@@ -5552,10 +5542,6 @@
 [auth]
   registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
 
-[database]
-  username = webuser
-  password = s3kr3t
-
 [ldap]
   password = l3tm3srch
 
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index bf45836..5d71b41 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -55,17 +55,16 @@
 in future gerrit releases. To build Gerrit with Java 8 language level, run:
 
 ```
-  $ bazel build --java_toolchain //tools:error_prone_warnings_toolchain :release
+  $ bazel build :release
 ```
 
 [[java-11]]
 ==== Java 11 support
 
-Java language level 11 is the default. To build Gerrit with Java 11 language
-level, run:
+To build Gerrit with Java 11 language level, run:
 
 ```
-  $ bazel build :release
+  $ bazel build --java_toolchain=//tools:error_prone_warnings_toolchain_java11 :release
 ```
 
 [[java-13]]
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/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 2bfb5d5..32bfc6b 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -74,8 +74,8 @@
 link:user-search.html#_search_operators[query string] must be provided
 by the `q` parameter. The `n` parameter can be used to limit the
 returned results. The `no-limit` parameter can be used remove the default
-limit on queries and return all results. This might not be supported by
-all index backends.
+limit on queries and return all results (does not apply to anonymous requests).
+This might not be supported by all index backends.
 
 As result a list of link:#change-info[ChangeInfo] entries is returned.
 The change output is sorted by the last update time, most recently
diff --git a/WORKSPACE b/WORKSPACE
index 3b8b768..cdec888 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -33,6 +33,15 @@
 load("//tools:nongoogle.bzl", "declare_nongoogle_deps")
 
 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 = "766796de71916118e528b9f4334c29c9c9b4e926227bf3264dee555e6a4306c8",
     strip_prefix = "rbe_autoconfig-2.0.0",
@@ -57,8 +66,38 @@
 
 http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "5bf77cc2d13ddf9124f4c1453dd96063774d755d4fc75d922471540d1c9a8ea8",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.0.0/rules_nodejs-2.0.0.tar.gz"],
+    sha256 = "c077680a307eb88f3e62b0b662c2e9c6315319385bc8c637a861ffdbed8ca247",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.1.0/rules_nodejs-5.1.0.tar.gz"],
+)
+
+load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
+
+build_bazel_rules_nodejs_dependencies()
+
+# This is required just because we have a dependency on @bazel/concatjs.
+# We don't actually use any of this web_testing stuff.
+# TODO: Remove this dependency.
+http_archive(
+    name = "io_bazel_rules_webtesting",
+    sha256 = "e9abb7658b6a129740c0b3ef6f5a2370864e102a5ba5ffca2cea565829ed825a",
+    urls = [
+        "https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.5/rules_webtesting.tar.gz",
+    ],
+)
+
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories")
+
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+web_test_repositories()
+
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+load("@io_bazel_rules_webtesting//web/versioned:browsers-0.3.3.bzl", "browser_repositories")
+
+# TODO: Remove this, see comments on `io_bazel_rules_webtesting`.
+browser_repositories(
+    chromium = True,
+    firefox = True,
 )
 
 # Golang support for PolyGerrit local dev server.
@@ -138,8 +177,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
@@ -228,32 +267,6 @@
     sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
 )
 
-SLF4J_VERS = "1.7.33"
-
-maven_jar(
-    name = "log-api",
-    artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
-    sha1 = "d375aa1b98d34d5ddf73a3f19eaad66e98975b12",
-)
-
-maven_jar(
-    name = "log-ext",
-    artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
-    sha1 = "00da03640ae1ad57f964dcaa542fb5d804dce8a6",
-)
-
-maven_jar(
-    name = "impl-log4j",
-    artifact = "org.slf4j:slf4j-reload4j:" + SLF4J_VERS,
-    sha1 = "ddc89144bfb56781936120b2334a70869b68db6d",
-)
-
-maven_jar(
-    name = "jcl-over-slf4j",
-    artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
-    sha1 = "28c441128bc81b6d95cc2857ae5bb46ae5bf658b",
-)
-
 maven_jar(
     name = "json-smart",
     artifact = "net.minidev:json-smart:1.1.1",
@@ -931,37 +944,57 @@
     sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
 )
 
-load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
+load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")
+
+node_repositories(
+    node_version = "16.13.2",
+    yarn_version = "1.22.17",
+)
 
 yarn_install(
     name = "npm",
+    exports_directories_only = False,
+    frozen_lockfile = False,
     package_json = "//:package.json",
+    symlink_node_modules = True,
     yarn_lock = "//:yarn.lock",
 )
 
 yarn_install(
     name = "ui_npm",
     args = ["--prod"],
+    exports_directories_only = False,
+    frozen_lockfile = False,
     package_json = "//:polygerrit-ui/app/package.json",
+    symlink_node_modules = True,
     yarn_lock = "//:polygerrit-ui/app/yarn.lock",
 )
 
 yarn_install(
     name = "ui_dev_npm",
+    exports_directories_only = False,
+    frozen_lockfile = False,
     package_json = "//:polygerrit-ui/package.json",
+    symlink_node_modules = True,
     yarn_lock = "//:polygerrit-ui/yarn.lock",
 )
 
 yarn_install(
     name = "tools_npm",
+    exports_directories_only = False,
+    frozen_lockfile = False,
     package_json = "//:tools/node_tools/package.json",
+    symlink_node_modules = True,
     yarn_lock = "//:tools/node_tools/yarn.lock",
 )
 
 yarn_install(
     name = "plugins_npm",
     args = ["--prod"],
+    exports_directories_only = False,
+    frozen_lockfile = False,
     package_json = "//:plugins/package.json",
+    symlink_node_modules = True,
     yarn_lock = "//:plugins/yarn.lock",
 )
 
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/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index f44a78d..482c804 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -436,7 +436,6 @@
 
     baseConfig.setInt("index", null, "batchThreads", -1);
 
-    baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
     Module module = createModule();
     Module auditModule = createAuditModule();
     Module sshModule = createSshModule();
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index c05516b..1251259 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -56,6 +56,7 @@
  */
 public abstract class QueryProcessor<T> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final int MAX_LIMIT_BUFFER_MULTIPLIER = 100;
 
   protected static class Metrics {
     final Timer1<String> executionTime;
@@ -356,7 +357,7 @@
 
   private int getEffectiveLimit(Predicate<T> p) {
     if (isNoLimit == true) {
-      return Integer.MAX_VALUE;
+      return getIndexSize() + MAX_LIMIT_BUFFER_MULTIPLIER * getBatchSize();
     }
     List<Integer> possibleLimits = new ArrayList<>(4);
     possibleLimits.add(getBackendSupportedLimit());
@@ -384,4 +385,8 @@
   }
 
   protected abstract String formatForLogging(T t);
+
+  protected abstract int getIndexSize();
+
+  protected abstract int getBatchSize();
 }
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index f6c395e..1999270 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/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 93cf0de..904490c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -389,18 +389,24 @@
         TraceContext.newTimer(
             "Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
       Ref ref = repo.exactRef(refName);
-      if (ref == null) {
-        return StarRef.MISSING;
-      }
+      return readLabels(repo, ref);
+    }
+  }
 
-      try (ObjectReader reader = repo.newObjectReader()) {
-        ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
-        return StarRef.create(
-            ref,
-            Splitter.on(CharMatcher.whitespace())
-                .omitEmptyStrings()
-                .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
-      }
+  public static StarRef readLabels(Repository repo, Ref ref) throws IOException {
+    if (ref == null) {
+      return StarRef.MISSING;
+    }
+    try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                String.format("Read star labels from %s (without ref lookup)", ref.getName()));
+        ObjectReader reader = repo.newObjectReader()) {
+      ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
+      return StarRef.create(
+          ref,
+          Splitter.on(CharMatcher.whitespace())
+              .omitEmptyStrings()
+              .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
     }
   }
 
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index b12e585..ea327f8 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -117,7 +117,7 @@
         return true;
       }
     } catch (ResourceConflictException e) {
-      logger.atInfo().log("Account %s already deactivated, continuing...", userName);
+      logger.atInfo().withCause(e).log("Account %s already deactivated, continuing...", userName);
     } catch (Exception e) {
       logger.atSevere().withCause(e).log(
           "Error deactivating account: %s (%s) %s",
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 1d31da9..71ce4d8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -112,8 +112,10 @@
   private static final String PASSWORD_KEY = "password";
 
   /**
-   * Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link
-   * AuthType#HTTP_LDAP}, and {@link AuthType#LDAP_BIND} usernames.
+   * Scheme used to label accounts created, when using the LDAP-based authentication types {@link
+   * AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link AuthType#HTTP_LDAP}, and {@link
+   * AuthType#LDAP_BIND}. The external ID stores the username. Accounts with such an external ID
+   * will be authenticated against the configured LDAP identity provider.
    *
    * <p>The name {@code gerrit:} was a very poor choice.
    *
diff --git a/java/com/google/gerrit/server/change/ResetCherryPickOp.java b/java/com/google/gerrit/server/change/ResetCherryPickOp.java
new file mode 100644
index 0000000..d1177d4
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ResetCherryPickOp.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+
+/** Reset cherryPickOf to an empty value. */
+public class ResetCherryPickOp implements BatchUpdateOp {
+
+  @Override
+  public boolean updateChange(ChangeContext ctx) throws RestApiException {
+    Change change = ctx.getChange();
+    if (change.getCherryPickOf() == null) {
+      return false;
+    }
+
+    ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+    update.resetCherryPickOf();
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 0c3c4fb..1382be1 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -124,12 +124,6 @@
     a.id = change.getKey().get();
     a.number = change.getId().get();
     a.subject = change.getSubject();
-    try {
-      a.commitMessage = changeDataFactory.create(change).commitMessage();
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log(
-          "Error while getting full commit message for change %d", a.number);
-    }
     a.url = getChangeUrl(change);
     a.owner = asAccountAttribute(change.getOwner());
     a.assignee = asAccountAttribute(change.getAssignee());
@@ -147,11 +141,8 @@
   /** Create a {@link ChangeAttribute} instance from the specified change. */
   public ChangeAttribute asChangeAttribute(Change change, ChangeNotes notes) {
     ChangeAttribute a = asChangeAttribute(change);
-    Set<String> hashtags = notes.load().getHashtags();
-    if (!hashtags.isEmpty()) {
-      a.hashtags = new ArrayList<>(hashtags.size());
-      a.hashtags.addAll(hashtags);
-    }
+    addHashTags(a, notes);
+    addCommitMessage(a, notes);
     return a;
   }
   /**
@@ -347,6 +338,15 @@
     a.commitMessage = commitMessage;
   }
 
+  private void addCommitMessage(ChangeAttribute changeAttribute, ChangeNotes notes) {
+    try {
+      addCommitMessage(changeAttribute, changeDataFactory.create(notes).commitMessage());
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log(
+          "Error while getting full commit message for change %d", changeAttribute.number);
+    }
+  }
+
   public void addPatchSets(
       RevWalk revWalk,
       ChangeAttribute ca,
@@ -563,4 +563,12 @@
     }
     return null;
   }
+
+  private void addHashTags(ChangeAttribute changeAttribute, ChangeNotes notes) {
+    Set<String> hashtags = notes.load().getHashtags();
+    if (!hashtags.isEmpty()) {
+      changeAttribute.hashtags = new ArrayList<>(hashtags.size());
+      changeAttribute.hashtags.addAll(hashtags);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/server/git/RefCache.java b/java/com/google/gerrit/server/git/RefCache.java
index 5a5cae9..2dee427 100644
--- a/java/com/google/gerrit/server/git/RefCache.java
+++ b/java/com/google/gerrit/server/git/RefCache.java
@@ -37,4 +37,7 @@
    *     present with a value of {@link ObjectId#zeroId()}.
    */
   Optional<ObjectId> get(String refName) throws IOException;
+
+  /** Closes this cache, releasing the references to any underlying resources. */
+  void close();
 }
diff --git a/java/com/google/gerrit/server/git/RepoRefCache.java b/java/com/google/gerrit/server/git/RepoRefCache.java
index 6b2493a..7a61e67 100644
--- a/java/com/google/gerrit/server/git/RepoRefCache.java
+++ b/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -28,8 +28,11 @@
 public class RepoRefCache implements RefCache {
   private final RefDatabase refdb;
   private final Map<String, Optional<ObjectId>> ids;
+  private final Repository repo;
 
   public RepoRefCache(Repository repo) {
+    repo.incrementOpen();
+    this.repo = repo;
     this.refdb = repo.getRefDatabase();
     this.ids = new HashMap<>();
   }
@@ -50,4 +53,9 @@
   public Map<String, Optional<ObjectId>> getCachedRefs() {
     return Collections.unmodifiableMap(ids);
   }
+
+  @Override
+  public void close() {
+    repo.close();
+  }
 }
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index d037994..06cc228 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PublishCommentsOp;
+import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -366,7 +367,7 @@
         () -> {
           String oldName = Thread.currentThread().getName();
           Thread.currentThread().setName(oldName + "-for-" + currentThreadName);
-          try {
+          try (PerThreadCache threadLocalCache = PerThreadCache.create()) {
             return receiveCommits.processCommands(commands, monitor);
           } finally {
             Thread.currentThread().setName(oldName);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 324be5e..0209105 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -3281,7 +3281,7 @@
                     rw.markStart(newTip);
                     rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
 
-                    Map<Change.Key, ChangeNotes> byKey = null;
+                    Map<Change.Key, ChangeData> changeDataByKey = null;
                     List<ReplaceRequest> replaceAndClose = new ArrayList<>();
 
                     int existingPatchSets = 0;
@@ -3321,8 +3321,8 @@
 
                       for (String changeId :
                           ChangeUtil.getChangeIdsFromFooter(c, urlFormatter.get())) {
-                        if (byKey == null) {
-                          byKey =
+                        if (changeDataByKey == null) {
+                          changeDataByKey =
                               retryHelper
                                   .changeIndexQuery(
                                       "queryOpenChangesByKeyByBranch",
@@ -3330,14 +3330,15 @@
                                   .call();
                         }
 
-                        ChangeNotes onto = byKey.get(Change.key(changeId.trim()));
+                        ChangeData onto = changeDataByKey.get(Change.key(changeId.trim()));
                         if (onto != null) {
                           newPatchSets++;
                           // Hold onto this until we're done with the walk, as the call to
                           // req.validate below calls isMergedInto which resets the walk.
+                          ChangeNotes ontoNotes = onto.notes();
                           ReplaceRequest req =
-                              new ReplaceRequest(onto.getChangeId(), c, cmd, false);
-                          req.notes = onto;
+                              new ReplaceRequest(ontoNotes.getChangeId(), c, cmd, false);
+                          req.notes = ontoNotes;
                           replaceAndClose.add(req);
                           continue COMMIT;
                         }
@@ -3410,14 +3411,17 @@
     }
   }
 
-  private Map<Change.Key, ChangeNotes> openChangesByKeyByBranch(
+  private Map<Change.Key, ChangeData> openChangesByKeyByBranch(
       InternalChangeQuery internalChangeQuery, BranchNameKey branch) {
     try (TraceTimer traceTimer =
         newTimer("openChangesByKeyByBranch", Metadata.builder().branchName(branch.branch()))) {
-      Map<Change.Key, ChangeNotes> r = new HashMap<>();
+      Map<Change.Key, ChangeData> r = new HashMap<>();
       for (ChangeData cd : internalChangeQuery.byBranchOpen(branch)) {
         try {
-          r.put(cd.change().getKey(), cd.notes());
+          // ChangeData is not materialised into a ChangeNotes for avoiding
+          // to load a potentially large number of changes meta-data into memory
+          // which would cause unnecessary disk I/O, CPU and heap utilisation.
+          r.put(cd.change().getKey(), cd);
         } catch (NoSuchChangeException e) {
           // Ignore deleted change
         }
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index c67df8b..f84b696 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -266,6 +266,12 @@
         "multiple Change-Id lines in message footer";
     private static final String INVALID_CHANGE_ID_MSG =
         "invalid Change-Id line format in message footer";
+    private static final String HTTP_INSTALL_HOOK =
+        "f=\"$(git rev-parse --git-dir)/hooks/commit-msg\"; curl -o \"$f\""
+            + " %stools/hooks/commit-msg ; chmod +x \"$f\"";
+    private static final String SSH_INSTALL_HOOK =
+        "gitdir=$(git rev-parse --git-dir); scp -p -P %d %s@%s:hooks/commit-msg ${gitdir}/hooks/";
+    private static final String CHANGE_ID_MISSING_INSTALL_HOOKS = "  %s\nor, for http(s):\n  %s";
 
     @VisibleForTesting
     public static final String CHANGE_ID_MISMATCH_MSG =
@@ -372,14 +378,15 @@
       // If there are no SSH keys, the commit-msg hook must be installed via
       // HTTP(S)
       Optional<String> webUrl = urlFormatter.getWebUrl();
+
+      String httpHook = String.format(HTTP_INSTALL_HOOK, webUrl.get());
+
       if (hostKeys.isEmpty()) {
         checkState(webUrl.isPresent());
-        return String.format(
-            "  f=\"$(git rev-parse --git-dir)/hooks/commit-msg\"; curl -o \"$f\" %stools/hooks/commit-msg ; chmod +x \"$f\"",
-            webUrl.get());
+        return httpHook;
       }
 
-      // SSH keys exist, so the hook can be installed with scp.
+      // SSH keys exist, so the hook might be able to be installed with scp.
       String sshHost;
       int sshPort;
       String host = hostKeys.get(0).getHost();
@@ -397,9 +404,10 @@
         sshPort = 22;
       }
 
-      return String.format(
-          "  gitdir=$(git rev-parse --git-dir); scp -p -P %d %s@%s:hooks/commit-msg ${gitdir}/hooks/",
-          sshPort, user.getUserName().orElse("<USERNAME>"), sshHost);
+      String sshHook =
+          String.format(
+              SSH_INSTALL_HOOK, sshPort, user.getUserName().orElse("<USERNAME>"), sshHost);
+      return String.format(CHANGE_ID_MISSING_INSTALL_HOOKS, sshHook, httpHook);
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 8d76e23..7890d35 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -397,14 +397,24 @@
 
   @Override
   protected void add(RecipientType rt, Account.Id to) {
-    Optional<AccountState> accountState = args.accountCache.get(to);
-    if (!accountState.isPresent()) {
-      return;
-    }
-    if (accountState.get().generalPreferences().getEmailStrategy()
-            == EmailStrategy.ATTENTION_SET_ONLY
-        && !currentAttentionSet.contains(to)) {
-      return;
+    addRecipient(rt, to, /* isWatcher= */ false);
+  }
+
+  /** This bypasses the EmailStrategy.ATTENTION_SET_ONLY strategy when adding the recipient. */
+  @Override
+  protected void addWatcher(RecipientType rt, Account.Id to) {
+    addRecipient(rt, to, /* isWatcher= */ true);
+  }
+
+  private void addRecipient(RecipientType rt, Account.Id to, boolean isWatcher) {
+    if (!isWatcher) {
+      Optional<AccountState> accountState = args.accountCache.get(to);
+      if (accountState.isPresent()
+          && accountState.get().generalPreferences().getEmailStrategy()
+              == EmailStrategy.ATTENTION_SET_ONLY
+          && !currentAttentionSet.contains(to)) {
+        return;
+      }
     }
     if (emailOnlyAuthors && !authors.contains(to)) {
       return;
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index b78dc62..2ab73a8 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -60,7 +60,7 @@
       // TODO(hiesel): Remove special handling for owners
       StreamSupport.stream(matching.all().accounts.spliterator(), false)
           .filter(this::isOwnerOfProjectOrBranch)
-          .forEach(acc -> add(RecipientType.TO, acc));
+          .forEach(acc -> addWatcher(RecipientType.TO, acc));
       // Add everyone else. Owners added above will not be duplicated.
       add(RecipientType.TO, matching.to);
       add(RecipientType.CC, matching.cc);
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 5ffd928..91310ce 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -82,13 +82,15 @@
   /** Add users or email addresses to the TO, CC, or BCC list. */
   protected void add(RecipientType type, Watchers.List list) {
     for (Account.Id user : list.accounts) {
-      add(type, user);
+      addWatcher(type, user);
     }
     for (Address addr : list.emails) {
       add(type, addr);
     }
   }
 
+  protected abstract void addWatcher(RecipientType type, Account.Id to);
+
   public String getSshHost() {
     String host = Iterables.getFirst(args.sshAddresses, null);
     if (host == null) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 78ba243..6f9f7e8 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -55,6 +55,7 @@
 import com.google.common.collect.Tables;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
@@ -153,7 +154,10 @@
   private ReviewerByEmailSet pendingReviewersByEmail;
   private Change.Id revertOf;
   private int updateCount;
-  private PatchSet.Id cherryPickOf;
+  // Null indicates that the field was not parsed (yet).
+  // We only set the value once, based on the latest update (the actual value or Optional.empty() if
+  // the latest record unsets the field).
+  private Optional<PatchSet.Id> cherryPickOf;
 
   ChangeNotesParser(
       Change.Id changeId,
@@ -254,7 +258,7 @@
         firstNonNull(workInProgress, false),
         firstNonNull(hasReviewStarted, true),
         revertOf,
-        cherryPickOf,
+        cherryPickOf != null ? cherryPickOf.orElse(null) : null,
         updateCount);
   }
 
@@ -999,15 +1003,31 @@
     return Change.id(revertOf);
   }
 
-  private PatchSet.Id parseCherryPickOf(ChangeNotesCommit commit) throws ConfigInvalidException {
-    String cherryPickOf = parseOneFooter(commit, FOOTER_CHERRY_PICK_OF);
-    if (cherryPickOf == null) {
+  /**
+   * Parses {@link ChangeNoteUtil#FOOTER_CHERRY_PICK_OF} of the commit.
+   *
+   * @param commit the commit to parse.
+   * @return {@link Optional} value of the parsed footer or {@code null} if the footer is missing in
+   *     this commit.
+   * @throws ConfigInvalidException if the footer value could not be parsed as a valid {@link
+   *     PatchSet.Id}.
+   */
+  @Nullable
+  private Optional<PatchSet.Id> parseCherryPickOf(ChangeNotesCommit commit)
+      throws ConfigInvalidException {
+    String footer = parseOneFooter(commit, FOOTER_CHERRY_PICK_OF);
+    if (footer == null) {
+      // The footer is missing, nothing to parse.
       return null;
-    }
-    try {
-      return PatchSet.Id.parse(cherryPickOf);
-    } catch (IllegalArgumentException e) {
-      throw new ConfigInvalidException("\"" + cherryPickOf + "\" is not a valid patchset", e);
+    } else if (footer.equals("")) {
+      // Empty footer value, cherryPickOf was unset at this commit.
+      return Optional.empty();
+    } else {
+      try {
+        return Optional.of(PatchSet.Id.parse(footer));
+      } catch (IllegalArgumentException e) {
+        throw new ConfigInvalidException("\"" + footer + "\" is not a valid patchset", e);
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 3312d6c..52c551e 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -152,7 +152,9 @@
   private Boolean isPrivate;
   private Boolean workInProgress;
   private Integer revertOf;
-  private String cherryPickOf;
+  // If null, the update does not modify the field. Otherwise, it updates the field with the
+  // new value or resets if cherryPickOf == Optional.empty().
+  private Optional<String> cherryPickOf;
 
   private ChangeDraftUpdate draftUpdate;
   private RobotCommentUpdate robotCommentUpdate;
@@ -473,7 +475,12 @@
   }
 
   public void setCherryPickOf(String cherryPickOf) {
-    this.cherryPickOf = cherryPickOf;
+    checkArgument(cherryPickOf != null, "use resetCherryPickOf");
+    this.cherryPickOf = Optional.of(cherryPickOf);
+  }
+
+  public void resetCherryPickOf() {
+    this.cherryPickOf = Optional.empty();
   }
 
   /** @return the tree id for the updated tree */
@@ -753,7 +760,12 @@
     }
 
     if (cherryPickOf != null) {
-      addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf);
+      if (cherryPickOf.isPresent()) {
+        addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf.get());
+      } else {
+        // Update cherryPickOf with an empty value.
+        addFooter(msg, FOOTER_CHERRY_PICK_OF).append('\n');
+      }
     }
 
     if (plannedAttentionSetUpdates != null) {
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 47e12ff..d743921 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -340,4 +340,16 @@
       counterLock.unlock();
     }
   }
+
+  /**
+   * Retrieves the last returned sequence number.
+   *
+   * <p>Explicitly calls {@link #next()} if this instance didn't return sequence number until now.
+   */
+  public int last() {
+    if (counter == 0) {
+      next();
+    }
+    return counter - 1;
+  }
 }
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
index 7a8e28f..e44d031 100644
--- a/java/com/google/gerrit/server/notedb/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -55,6 +55,9 @@
   private final RepoSequence changeSeq;
   private final RepoSequence groupSeq;
   private final Timer2<SequenceType, Boolean> nextIdLatency;
+  private final int accountBatchSize;
+  private final int changeBatchSize;
+  private final int groupBatchSize = 1;
 
   @Inject
   public Sequences(
@@ -65,7 +68,7 @@
       AllUsersName allUsers,
       MetricMaker metrics) {
 
-    int accountBatchSize =
+    accountBatchSize =
         cfg.getInt(
             SECTION_NOTEDB,
             NAME_ACCOUNTS,
@@ -80,7 +83,7 @@
             () -> FIRST_ACCOUNT_ID,
             accountBatchSize);
 
-    int changeBatchSize =
+    changeBatchSize =
         cfg.getInt(
             SECTION_NOTEDB,
             NAME_CHANGES,
@@ -95,7 +98,6 @@
             () -> FIRST_CHANGE_ID,
             changeBatchSize);
 
-    int groupBatchSize = 1;
     groupSeq =
         new RepoSequence(
             repoManager,
@@ -144,6 +146,18 @@
     }
   }
 
+  public int changeBatchSize() {
+    return changeBatchSize;
+  }
+
+  public int groupBatchSize() {
+    return groupBatchSize;
+  }
+
+  public int accountBatchSize() {
+    return accountBatchSize;
+  }
+
   public int currentChangeId() {
     return changeSeq.current();
   }
@@ -156,6 +170,18 @@
     return groupSeq.current();
   }
 
+  public int lastChangeId() {
+    return changeSeq.last();
+  }
+
+  public int lastGroupId() {
+    return groupSeq.last();
+  }
+
+  public int lastAccountId() {
+    return accountSeq.last();
+  }
+
   public void setChangeIdValue(int value) {
     changeSeq.storeNew(value);
   }
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
index 2e29bbd..e380ef1 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.account.AccountIndexRewriter;
 import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -41,6 +42,7 @@
  */
 public class AccountQueryProcessor extends QueryProcessor<AccountState> {
   private final AccountControl.Factory accountControlFactory;
+  private final Sequences sequences;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -57,7 +59,8 @@
       IndexConfig indexConfig,
       AccountIndexCollection indexes,
       AccountIndexRewriter rewriter,
-      AccountControl.Factory accountControlFactory) {
+      AccountControl.Factory accountControlFactory,
+      Sequences sequences) {
     super(
         metricMaker,
         AccountSchemaDefinitions.INSTANCE,
@@ -67,6 +70,7 @@
         FIELD_LIMIT,
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.accountControlFactory = accountControlFactory;
+    this.sequences = sequences;
   }
 
   @Override
@@ -79,4 +83,14 @@
   protected String formatForLogging(AccountState accountState) {
     return accountState.account().id().toString();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return sequences.lastAccountId();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return sequences.accountBatchSize();
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index 370bc75..9677321 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.ArrayList;
@@ -65,6 +66,7 @@
   private final Map<String, DynamicBean> dynamicBeans = new HashMap<>();
   private final List<Extension<ChangePluginDefinedInfoFactory>>
       changePluginDefinedInfoFactoriesByPlugin = new ArrayList<>();
+  private final Sequences sequences;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -82,6 +84,7 @@
       ChangeIndexCollection indexes,
       ChangeIndexRewriter rewriter,
       DynamicSet<ChangeAttributeFactory> attributeFactories,
+      Sequences sequences,
       ChangeIsVisibleToPredicate.Factory changeIsVisibleToPredicateFactory,
       DynamicSet<ChangePluginDefinedInfoFactory> changePluginDefinedInfoFactories) {
     super(
@@ -94,6 +97,7 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
     this.changeIsVisibleToPredicateFactory = changeIsVisibleToPredicateFactory;
+    this.sequences = sequences;
 
     ImmutableListMultimap.Builder<String, ChangeAttributeFactory> factoriesBuilder =
         ImmutableListMultimap.builder();
@@ -163,4 +167,14 @@
   protected String formatForLogging(ChangeData changeData) {
     return changeData.getId().toString();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return sequences.lastChangeId();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return sequences.changeBatchSize();
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index b931457..6907f15 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelTypes;
@@ -28,6 +29,7 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryResult;
 import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.cache.PerThreadCache;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
@@ -205,7 +207,7 @@
         return;
       }
 
-      try {
+      try (PerThreadCache ignored = PerThreadCache.create()) {
         final QueryStatsAttribute stats = new QueryStatsAttribute();
         stats.runTimeMilliseconds = TimeUtil.nowMs();
 
@@ -250,7 +252,8 @@
       ChangeData d, Map<Project.NameKey, Repository> repos, Map<Project.NameKey, RevWalk> revWalks)
       throws IOException {
     LabelTypes labelTypes = d.getLabelTypes();
-    ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), d.notes());
+    ChangeAttribute c = eventFactory.asChangeAttribute(d.change());
+    c.hashtags = Lists.newArrayList(d.hashtags());
     eventFactory.extend(c, d.change());
 
     if (!trackingFooters.isEmpty()) {
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
index 86c574d..ed687f3 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.index.group.GroupIndexRewriter;
 import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
+import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -42,6 +43,7 @@
 public class GroupQueryProcessor extends QueryProcessor<InternalGroup> {
   private final Provider<CurrentUser> userProvider;
   private final GroupControl.GenericFactory groupControlFactory;
+  private final Sequences sequences;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -58,7 +60,8 @@
       IndexConfig indexConfig,
       GroupIndexCollection indexes,
       GroupIndexRewriter rewriter,
-      GroupControl.GenericFactory groupControlFactory) {
+      GroupControl.GenericFactory groupControlFactory,
+      Sequences sequences) {
     super(
         metricMaker,
         GroupSchemaDefinitions.INSTANCE,
@@ -69,6 +72,7 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.userProvider = userProvider;
     this.groupControlFactory = groupControlFactory;
+    this.sequences = sequences;
   }
 
   @Override
@@ -81,4 +85,14 @@
   protected String formatForLogging(InternalGroup internalGroup) {
     return internalGroup.getGroupUUID().get();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return sequences.lastGroupId();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return sequences.groupBatchSize();
+  }
 }
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
index 66eab7b..11783fc 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountLimits;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -42,6 +43,7 @@
 public class ProjectQueryProcessor extends QueryProcessor<ProjectData> {
   private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> userProvider;
+  private final ProjectCache projectCache;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -58,7 +60,8 @@
       IndexConfig indexConfig,
       ProjectIndexCollection indexes,
       ProjectIndexRewriter rewriter,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ProjectCache projectCache) {
     super(
         metricMaker,
         ProjectSchemaDefinitions.INSTANCE,
@@ -69,6 +72,7 @@
         () -> limitsFactory.create(userProvider.get()).getQueryLimit());
     this.permissionBackend = permissionBackend;
     this.userProvider = userProvider;
+    this.projectCache = projectCache;
   }
 
   @Override
@@ -81,4 +85,14 @@
   protected String formatForLogging(ProjectData projectData) {
     return projectData.getProject().getName();
   }
+
+  @Override
+  protected int getIndexSize() {
+    return projectCache.all().size();
+  }
+
+  @Override
+  protected int getBatchSize() {
+    return 1;
+  }
 }
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 3f28a03..4c07d8a 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",
@@ -31,6 +32,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 c80bf57..17e31bd 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/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index fdac552..6ee3284c 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.ResetCherryPickOp;
 import com.google.gerrit.server.change.SetCherryPickOp;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -371,7 +372,7 @@
                     git,
                     destChanges.get(0).notes(),
                     cherryPickCommit,
-                    sourceChange.currentPatchSetId(),
+                    sourceChange,
                     newTopic,
                     workInProgress);
           } else {
@@ -456,7 +457,7 @@
       Repository git,
       ChangeNotes destNotes,
       CodeReviewCommit cherryPickCommit,
-      PatchSet.Id sourcePatchSetId,
+      @Nullable Change sourceChange,
       String topic,
       @Nullable Boolean workInProgress)
       throws IOException {
@@ -469,7 +470,11 @@
       inserter.setWorkInProgress(workInProgress);
     }
     bu.addOp(destChange.getId(), inserter);
-    if (destChange.getCherryPickOf() == null
+    PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
+    // If sourceChange is not provided, reset cherryPickOf to avoid stale value.
+    if (sourcePatchSetId == null) {
+      bu.addOp(destChange.getId(), new ResetCherryPickOp());
+    } else if (destChange.getCherryPickOf() == null
         || !destChange.getCherryPickOf().equals(sourcePatchSetId)) {
       SetCherryPickOp cherryPickOfUpdater = setCherryPickOfFactory.create(sourcePatchSetId);
       bu.addOp(destChange.getId(), cherryPickOfUpdater);
diff --git a/java/com/google/gerrit/server/restapi/change/QueryChanges.java b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
index 3c8157b..7df74f8 100644
--- a/java/com/google/gerrit/server/restapi/change/QueryChanges.java
+++ b/java/com/google/gerrit/server/restapi/change/QueryChanges.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
 import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.change.ChangeJson;
@@ -95,7 +96,9 @@
     this.start = start;
   }
 
-  @Option(name = "--no-limit", usage = "Return all results, overriding the default limit")
+  @Option(
+      name = "--no-limit",
+      usage = "Return all results, overriding the default limit. Ignored for anonymous users.")
   public void setNoLimit(boolean on) {
     this.noLimit = on;
   }
@@ -168,7 +171,7 @@
     if (start != null) {
       queryProcessor.setStart(start);
     }
-    if (noLimit != null) {
+    if (noLimit != null && !AnonymousUser.class.isAssignableFrom(userProvider.get().getClass())) {
       queryProcessor.setNoLimit(noLimit);
     }
     if (skipVisibility != null) {
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 5b3e8dc..87b00c1 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -80,13 +80,18 @@
       throws PermissionBackendException {
     Optional<InternalGroup> group = groupCache.get(groupUuid);
     if (group.isPresent()) {
-      InternalGroupDescription internalGroup = new InternalGroupDescription(group.get());
-      GroupControl groupControl = groupControlFactory.controlFor(internalGroup);
-      return getTransitiveMembers(internalGroup, groupControl);
+      return getTransitiveMembers(group.get());
     }
     return ImmutableList.of();
   }
 
+  public List<AccountInfo> getTransitiveMembers(InternalGroup group)
+      throws PermissionBackendException {
+    InternalGroupDescription internalGroup = new InternalGroupDescription(group);
+    GroupControl groupControl = groupControlFactory.controlFor(internalGroup);
+    return getTransitiveMembers(internalGroup, groupControl);
+  }
+
   private List<AccountInfo> getTransitiveMembers(
       GroupDescription.Internal group, GroupControl groupControl)
       throws PermissionBackendException {
@@ -110,6 +115,14 @@
     return toAccountInfos(directMembers);
   }
 
+  protected List<AccountInfo> getMembers(InternalGroup group) throws PermissionBackendException {
+    if (recursive) {
+      return getTransitiveMembers(group);
+    } else {
+      return getDirectMembers(group);
+    }
+  }
+
   private List<AccountInfo> toAccountInfos(Set<Account.Id> members)
       throws PermissionBackendException {
     AccountLoader accountLoader = accountLoaderFactory.create(true);
diff --git a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
index c223aec..9a39575 100644
--- a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
+++ b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
@@ -39,13 +39,19 @@
 public class ChainedReceiveCommands implements RefCache {
   private final Map<String, ReceiveCommand> commands = new LinkedHashMap<>();
   private final RepoRefCache refCache;
+  private final boolean closeRefCache;
 
   public ChainedReceiveCommands(Repository repo) {
-    this(new RepoRefCache(repo));
+    this(new RepoRefCache(repo), true);
   }
 
   public ChainedReceiveCommands(RepoRefCache refCache) {
+    this(refCache, false);
+  }
+
+  private ChainedReceiveCommands(RepoRefCache refCache, boolean closeRefCache) {
     this.refCache = requireNonNull(refCache);
+    this.closeRefCache = closeRefCache;
   }
 
   public RepoRefCache getRepoRefCache() {
@@ -122,4 +128,11 @@
   public Map<String, ReceiveCommand> getCommands() {
     return Collections.unmodifiableMap(commands);
   }
+
+  @Override
+  public void close() {
+    if (closeRefCache) {
+      refCache.close();
+    }
+  }
 }
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index 340b910..59f3f0c 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -43,6 +43,8 @@
     volatile long started;
     volatile long finished;
 
+    private IdentifiedUser identifiedUser;
+
     private Context(SshSession s, String c, long at) {
       session = s;
       commandLine = c;
@@ -68,8 +70,10 @@
     public CurrentUser getUser() {
       CurrentUser user = session.getUser();
       if (user != null && user.isIdentifiedUser()) {
-        IdentifiedUser identifiedUser = userFactory.create(user.getAccountId());
-        identifiedUser.setAccessPath(user.getAccessPath());
+        if (identifiedUser == null) {
+          identifiedUser = userFactory.create(user.getAccountId());
+          identifiedUser.setAccessPath(user.getAccessPath());
+        }
         return identifiedUser;
       }
       return user;
diff --git a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index 3269c2b..37f4245 100644
--- a/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -79,7 +79,7 @@
         return;
       }
 
-      List<AccountInfo> members = getDirectMembers(group.get());
+      List<AccountInfo> members = getMembers(group.get());
       ColumnFormatter formatter = new ColumnFormatter(writer, '\t');
       formatter.addColumn("id");
       formatter.addColumn("username");
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 162f324..e002eeb 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -456,7 +456,7 @@
 
     MyParser(Object bean) {
       super(bean, ParserProperties.defaults().withAtSyntax(false));
-      parseAdditionalOptions(bean, new HashSet<>());
+      parseAdditionalOptions("", bean, new HashSet<>());
       addOptionsWithMetRequirements();
       ensureOptionsInitialized();
     }
@@ -527,7 +527,7 @@
       }
     }
 
-    private void parseAdditionalOptions(Object bean, Set<Object> parsedBeans) {
+    private void parseAdditionalOptions(String prefix, Object bean, Set<Object> parsedBeans) {
       for (Class<?> c = bean.getClass(); c != null; c = c.getSuperclass()) {
         for (Field f : c.getDeclaredFields()) {
           if (f.isAnnotationPresent(Options.class)) {
@@ -537,7 +537,8 @@
             } catch (IllegalAccessException e) {
               throw new IllegalAnnotationError(e);
             }
-            parseWithPrefix(f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
+            parseWithPrefix(
+                prefix + f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
           }
         }
       }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 0c30ef5..7c504b8 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -2618,7 +2618,7 @@
   }
 
   @Test
-  public void queryChangesNoLimit() throws Exception {
+  public void queryChangesNoLimitRegisteredUser() throws Exception {
     projectOperations
         .allProjectsForUpdate()
         .add(
@@ -2636,6 +2636,26 @@
   }
 
   @Test
+  public void queryChangesNoLimitIgnoredForAnonymousUser() throws Exception {
+    int limit = 2;
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(GlobalCapability.QUERY_LIMIT)
+                .group(SystemGroupBackend.ANONYMOUS_USERS)
+                .range(0, limit))
+        .update();
+    for (int i = 0; i < 3; i++) {
+      createChange();
+    }
+    requestScopeOperations.setApiUserAnonymous();
+    List<ChangeInfo> resultsWithDefaultLimit = gApi.changes().query().get();
+    List<ChangeInfo> resultsWithNoLimit = gApi.changes().query().withNoLimit().get();
+    assertThat(resultsWithDefaultLimit).hasSize(limit);
+    assertThat(resultsWithNoLimit).hasSize(limit);
+  }
+
+  @Test
   public void queryChangesStart() throws Exception {
     PushOneCommit.Result r1 = createChange();
     createChange();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index 2356327..3b5ac78 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -24,6 +24,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -147,77 +148,237 @@
   }
 
   @Test
-  public void cherryPickWithoutMessage() throws Exception {
-    String branch = "foo";
+  public void cherryPickWithoutMessageSameBranch() throws Exception {
+    String destBranch = "master";
 
     // Create change to cherry-pick
-    RevCommit revCommit = createChange().getCommit();
-
-    // Create target branch to cherry-pick to.
-    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());
+    PushOneCommit.Result r = createChange();
+    ChangeInfo changeToCherryPick = info(r.getChangeId());
+    RevCommit commitToCherryPick = r.getCommit();
 
     // Cherry-pick without message.
     CherryPickInput input = new CherryPickInput();
-    input.destination = branch;
-    String changeId =
-        gApi.projects().name(project.get()).commit(revCommit.name()).cherryPick(input).get().id;
+    input.destination = destBranch;
+    ChangeInfo cherryPickResult =
+        gApi.projects()
+            .name(project.get())
+            .commit(commitToCherryPick.name())
+            .cherryPick(input)
+            .get();
 
+    // Expect that the Change-Id of the cherry-picked commit was used for the cherry-pick change.
+    // New patch-set to existing change was uploaded.
+    assertThat(cherryPickResult._number).isEqualTo(changeToCherryPick._number);
+    assertThat(cherryPickResult.revisions).hasSize(2);
+    assertThat(cherryPickResult.changeId).isEqualTo(changeToCherryPick.changeId);
+    assertThat(cherryPickResult.messages).hasSize(2);
+
+    // Cherry-pick of is not set, because the source change was not provided.
+    assertThat(cherryPickResult.cherryPickOfChange).isNull();
+    assertThat(cherryPickResult.cherryPickOfPatchSet).isNull();
     // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
-    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
-    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision);
     assertThat(revInfo).isNotNull();
-    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
+    assertThat(revInfo.commit.message).isEqualTo(commitToCherryPick.getFullMessage());
   }
 
   @Test
-  public void cherryPickCommitWithoutChangeId() throws Exception {
+  public void cherryPickWithoutMessageOtherBranch() throws Exception {
+    String destBranch = "foo";
+    createBranch(BranchNameKey.create(project, destBranch));
+
+    // Create change to cherry-pick
+    PushOneCommit.Result r = createChange();
+    ChangeInfo changeToCherryPick = info(r.getChangeId());
+    RevCommit commitToCherryPick = r.getCommit();
+
+    // Cherry-pick without message.
     CherryPickInput input = new CherryPickInput();
-    input.destination = "foo";
+    input.destination = destBranch;
+    ChangeInfo cherryPickResult =
+        gApi.projects()
+            .name(project.get())
+            .commit(commitToCherryPick.name())
+            .cherryPick(input)
+            .get();
+
+    // Expect that the Change-Id of the cherry-picked commit was used for the cherry-pick change.
+    // New change in destination branch was created.
+    assertThat(cherryPickResult._number).isGreaterThan(changeToCherryPick._number);
+    assertThat(cherryPickResult.revisions).hasSize(1);
+    assertThat(cherryPickResult.changeId).isEqualTo(changeToCherryPick.changeId);
+    assertThat(cherryPickResult.messages).hasSize(1);
+
+    // Cherry-pick of is not set, because the source change was not provided.
+    assertThat(cherryPickResult.cherryPickOfChange).isNull();
+    assertThat(cherryPickResult.cherryPickOfPatchSet).isNull();
+    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
+    RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(commitToCherryPick.getFullMessage());
+  }
+
+  @Test
+  public void cherryPickCommitWithoutChangeIdCreateNewChange() throws Exception {
+    String destBranch = "foo";
+    createBranch(BranchNameKey.create(project, destBranch));
+
+    CherryPickInput input = new CherryPickInput();
+    input.destination = destBranch;
     input.message = "it goes to foo branch";
-    gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput());
 
-    RevCommit revCommit = createNewCommitWithoutChangeId("refs/heads/master", "a.txt", "content");
-    ChangeInfo changeInfo =
-        gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get();
+    RevCommit commitToCherryPick =
+        createNewCommitWithoutChangeId("refs/heads/master", "a.txt", "content");
+    ChangeInfo cherryPickResult =
+        gApi.projects()
+            .name(project.get())
+            .commit(commitToCherryPick.getName())
+            .cherryPick(input)
+            .get();
 
-    assertThat(changeInfo.messages).hasSize(1);
-    Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator();
+    assertThat(cherryPickResult.messages).hasSize(1);
+    Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator();
     String expectedMessage =
-        String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName());
+        String.format("Patch Set 1: Cherry Picked from commit %s.", commitToCherryPick.getName());
     assertThat(messageIterator.next().message).isEqualTo(expectedMessage);
 
-    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision);
     assertThat(revInfo).isNotNull();
     CommitInfo commitInfo = revInfo.commit;
     assertThat(commitInfo.message)
-        .isEqualTo(input.message + "\n\nChange-Id: " + changeInfo.changeId + "\n");
+        .isEqualTo(input.message + "\n\nChange-Id: " + cherryPickResult.changeId + "\n");
   }
 
   @Test
-  public void cherryPickCommitWithChangeId() throws Exception {
-    CherryPickInput input = new CherryPickInput();
-    input.destination = "foo";
+  public void cherryPickCommitWithChangeIdCreateNewChange() throws Exception {
+    String destBranch = "foo";
+    createBranch(BranchNameKey.create(project, destBranch));
 
-    RevCommit revCommit = createChange().getCommit();
-    List<String> footers = revCommit.getFooterLines("Change-Id");
+    PushOneCommit.Result r = createChange();
+    ChangeInfo changeToCherryPick = info(r.getChangeId());
+    RevCommit commitToCherryPick = r.getCommit();
+    List<String> footers = commitToCherryPick.getFooterLines("Change-Id");
     assertThat(footers).hasSize(1);
     String changeId = footers.get(0);
 
-    input.message = "it goes to foo branch\n\nChange-Id: " + changeId;
-    gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput());
+    CherryPickInput input = new CherryPickInput();
+    input.destination = destBranch;
+    input.message =
+        String.format(
+            "it goes to foo branch\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n\nChange-Id: %s\n",
+            changeId);
 
-    ChangeInfo changeInfo =
-        gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get();
+    ChangeInfo cherryPickResult =
+        gApi.projects()
+            .name(project.get())
+            .commit(commitToCherryPick.getName())
+            .cherryPick(input)
+            .get();
 
-    assertThat(changeInfo.messages).hasSize(1);
-    Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator();
+    // No change was found in destination branch with the provided Change-Id.
+    assertThat(cherryPickResult._number).isGreaterThan(changeToCherryPick._number);
+    assertThat(cherryPickResult.changeId).isEqualTo(changeId);
+    assertThat(cherryPickResult.revisions).hasSize(1);
+    assertThat(cherryPickResult.messages).hasSize(1);
+    Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator();
     String expectedMessage =
-        String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName());
+        String.format("Patch Set 1: Cherry Picked from commit %s.", commitToCherryPick.getName());
     assertThat(messageIterator.next().message).isEqualTo(expectedMessage);
 
-    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+    // Cherry-pick of is not set, because the source change was not provided.
+    assertThat(cherryPickResult.cherryPickOfChange).isNull();
+    assertThat(cherryPickResult.cherryPickOfPatchSet).isNull();
+    RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision);
     assertThat(revInfo).isNotNull();
-    assertThat(revInfo.commit.message).isEqualTo(input.message + "\n");
+    assertThat(revInfo.commit.message).isEqualTo(input.message);
+  }
+
+  @Test
+  public void cherryPickCommitToExistingChange() throws Exception {
+    String destBranch = "foo";
+    createBranch(BranchNameKey.create(project, destBranch));
+
+    PushOneCommit.Result r = createChange("refs/for/" + destBranch);
+    ChangeInfo existingDestChange = info(r.getChangeId());
+
+    String commitToCherryPick = createChange().getCommit().getName();
+
+    CherryPickInput input = new CherryPickInput();
+    input.destination = destBranch;
+    input.message =
+        String.format(
+            "it goes to foo branch\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef\n\nChange-Id: %s\n",
+            existingDestChange.changeId);
+    input.allowConflicts = true;
+    input.allowEmpty = true;
+
+    ChangeInfo cherryPickResult =
+        gApi.projects().name(project.get()).commit(commitToCherryPick).cherryPick(input).get();
+
+    // New patch-set to existing change was uploaded.
+    assertThat(cherryPickResult._number).isEqualTo(existingDestChange._number);
+    assertThat(cherryPickResult.changeId).isEqualTo(existingDestChange.changeId);
+    assertThat(cherryPickResult.messages).hasSize(2);
+    assertThat(cherryPickResult.revisions).hasSize(2);
+    Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator();
+
+    assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 2.");
+    // Cherry-pick of is not set, because the source change was not provided.
+    assertThat(cherryPickResult.cherryPickOfChange).isNull();
+    assertThat(cherryPickResult.cherryPickOfPatchSet).isNull();
+    RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(input.message);
+  }
+
+  @Test
+  public void cherryPickCommitToExistingCherryPickedChange() throws Exception {
+    String destBranch = "foo";
+    createBranch(BranchNameKey.create(project, destBranch));
+
+    PushOneCommit.Result r = createChange("refs/for/" + destBranch);
+    ChangeInfo existingDestChange = info(r.getChangeId());
+
+    r = createChange();
+    ChangeInfo changeToCherryPick = info(r.getChangeId());
+    RevCommit commitToCherryPick = r.getCommit();
+
+    CherryPickInput input = new CherryPickInput();
+    input.destination = destBranch;
+    input.message =
+        String.format("it goes to foo branch\n\nChange-Id: %s\n", existingDestChange.changeId);
+    input.allowConflicts = true;
+    input.allowEmpty = true;
+    // Use RevisionAPI to submit initial cherryPick.
+    ChangeInfo cherryPickResult =
+        gApi.changes().id(changeToCherryPick.changeId).current().cherryPick(input).get();
+    assertThat(cherryPickResult.changeId).isEqualTo(existingDestChange.changeId);
+    // Cherry-pick was set.
+    assertThat(cherryPickResult.cherryPickOfChange).isEqualTo(changeToCherryPick._number);
+    assertThat(cherryPickResult.cherryPickOfPatchSet).isEqualTo(1);
+    RevisionInfo revInfo = cherryPickResult.revisions.get(cherryPickResult.currentRevision);
+    assertThat(revInfo).isNotNull();
+    assertThat(revInfo.commit.message).isEqualTo(input.message);
+    // Use CommitApi to update the cherryPick change.
+    cherryPickResult =
+        gApi.projects()
+            .name(project.get())
+            .commit(commitToCherryPick.getName())
+            .cherryPick(input)
+            .get();
+
+    assertThat(cherryPickResult.changeId).isEqualTo(existingDestChange.changeId);
+    assertThat(cherryPickResult.messages).hasSize(3);
+    Iterator<ChangeMessageInfo> messageIterator = cherryPickResult.messages.iterator();
+
+    assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 2.");
+    assertThat(messageIterator.next().message).isEqualTo("Uploaded patch set 3.");
+    // Cherry-pick was reset to empty value.
+    assertThat(cherryPickResult._number).isEqualTo(existingDestChange._number);
+    assertThat(cherryPickResult.cherryPickOfChange).isNull();
+    assertThat(cherryPickResult.cherryPickOfPatchSet).isNull();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 61eef63..5c50949 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -1438,11 +1438,7 @@
 
     // Add preference for the user such that they only receive an email on changes that require
     // their attention.
-    requestScopeOperations.setApiUser(user.id());
-    GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
-    prefs.emailStrategy = EmailStrategy.ATTENTION_SET_ONLY;
-    gApi.accounts().self().setPreferences(prefs);
-    requestScopeOperations.setApiUser(admin.id());
+    setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY);
 
     // Add user to attention set. They receive an email since they are in the attention set.
     change(r).addReviewer(user.id().toString());
@@ -1464,17 +1460,34 @@
   public void attentionSetWithEmailFilterImpactingOnlyChangeEmails() throws Exception {
     // Add preference for the user such that they only receive an email on changes that require
     // their attention.
-    requestScopeOperations.setApiUser(user.id());
-    GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
-    prefs.emailStrategy = EmailStrategy.ATTENTION_SET_ONLY;
-    gApi.accounts().self().setPreferences(prefs);
-    requestScopeOperations.setApiUser(admin.id());
+    setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY);
 
     // Ensure emails that don't relate to changes are still sent.
     gApi.accounts().id(user.id().get()).generateHttpPassword();
     assertThat(sender.getMessages()).isNotEmpty();
   }
 
+  @Test
+  public void outsideAttentionSet_watchProjectEmailReceived() throws Exception {
+    setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY);
+
+    requestScopeOperations.setApiUser(user.id());
+    watch(project.get());
+
+    createChange();
+
+    assertThat(sender.getMessages()).isNotEmpty();
+    sender.clear();
+  }
+
+  private void setEmailStrategyForUser(EmailStrategy es) throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
+    prefs.emailStrategy = es;
+    gApi.accounts().self().setPreferences(prefs);
+    requestScopeOperations.setApiUser(admin.id());
+  }
+
   private List<AttentionSetUpdate> getAttentionSetUpdatesForUser(
       PushOneCommit.Result r, TestAccount account) {
     return getAttentionSetUpdates(r.getChange().getId()).stream()
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..1bb9784 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -15,6 +15,7 @@
 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 java.util.function.Supplier;
diff --git a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
new file mode 100644
index 0000000..81d6814
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
@@ -0,0 +1,275 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository.Builder;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Test;
+
+public class RepoRefCacheTest {
+  private static final String TEST_BRANCH = "main";
+
+  @Test
+  @SuppressWarnings("resource")
+  public void repositoryUseShouldBeTrackedByRepoRefCache() throws Exception {
+    RefCache cache;
+    TestRepositoryWithRefCounting repoWithRefCounting;
+
+    try (TestRepositoryWithRefCounting repo =
+        TestRepositoryWithRefCounting.createWithBranch(TEST_BRANCH)) {
+      assertThat(repo.refCounter()).isEqualTo(1);
+      repoWithRefCounting = repo;
+      cache = new RepoRefCache(repo);
+    }
+
+    assertThat(repoWithRefCounting.refCounter()).isEqualTo(1);
+    assertThat(cache.get(Constants.R_HEADS + TEST_BRANCH)).isNotNull();
+  }
+
+  private static class TestRepositoryWithRefCounting extends Repository {
+    private int refCounter;
+
+    static TestRepositoryWithRefCounting createWithBranch(String branchName) throws Exception {
+      Builder builder =
+          new InMemoryRepository.Builder()
+              .setRepositoryDescription(new DfsRepositoryDescription(""))
+              .setFS(FS.detect().setUserHome(null));
+      TestRepositoryWithRefCounting testRepo = new TestRepositoryWithRefCounting(builder);
+      new TestRepository<>(testRepo).branch(branchName).commit().message("").create();
+      return testRepo;
+    }
+
+    private final Repository repo;
+
+    private TestRepositoryWithRefCounting(InMemoryRepository.Builder builder) throws IOException {
+      super(builder);
+
+      repo = builder.build();
+      refCounter = 1;
+    }
+
+    public int refCounter() {
+      return refCounter;
+    }
+
+    @Override
+    public void incrementOpen() {
+      repo.incrementOpen();
+      refCounter++;
+    }
+
+    @Override
+    public void close() {
+      repo.close();
+      refCounter--;
+    }
+
+    @Override
+    public void create(boolean bare) throws IOException {}
+
+    @Override
+    public ObjectDatabase getObjectDatabase() {
+      checkIsOpen();
+      return repo.getObjectDatabase();
+    }
+
+    @Override
+    public RefDatabase getRefDatabase() {
+      RefDatabase refDatabase = repo.getRefDatabase();
+      return new RefDatabase() {
+
+        @Override
+        public int hashCode() {
+          return refDatabase.hashCode();
+        }
+
+        @Override
+        public void create() throws IOException {
+          refDatabase.create();
+        }
+
+        @Override
+        public void close() {
+          checkIsOpen();
+          refDatabase.close();
+        }
+
+        @Override
+        public boolean isNameConflicting(String name) throws IOException {
+          checkIsOpen();
+          return refDatabase.isNameConflicting(name);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+          return refDatabase.equals(obj);
+        }
+
+        @Override
+        public Collection<String> getConflictingNames(String name) throws IOException {
+          checkIsOpen();
+          return refDatabase.getConflictingNames(name);
+        }
+
+        @Override
+        public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+          checkIsOpen();
+          return refDatabase.newUpdate(name, detach);
+        }
+
+        @Override
+        public RefRename newRename(String fromName, String toName) throws IOException {
+          checkIsOpen();
+          return refDatabase.newRename(fromName, toName);
+        }
+
+        @Override
+        public BatchRefUpdate newBatchUpdate() {
+          checkIsOpen();
+          return refDatabase.newBatchUpdate();
+        }
+
+        @Override
+        public boolean performsAtomicTransactions() {
+          checkIsOpen();
+          return refDatabase.performsAtomicTransactions();
+        }
+
+        @Override
+        public Ref exactRef(String name) throws IOException {
+          checkIsOpen();
+          return refDatabase.exactRef(name);
+        }
+
+        @Override
+        public String toString() {
+          return refDatabase.toString();
+        }
+
+        @Override
+        public Map<String, Ref> exactRef(String... refs) throws IOException {
+          checkIsOpen();
+          return refDatabase.exactRef(refs);
+        }
+
+        @Override
+        public Ref firstExactRef(String... refs) throws IOException {
+          checkIsOpen();
+          return refDatabase.firstExactRef(refs);
+        }
+
+        @Override
+        public List<Ref> getRefs() throws IOException {
+          checkIsOpen();
+          return refDatabase.getRefs();
+        }
+
+        @Override
+        public Map<String, Ref> getRefs(String prefix) throws IOException {
+          checkIsOpen();
+          return refDatabase.getRefs(prefix);
+        }
+
+        @Override
+        public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+          checkIsOpen();
+          return refDatabase.getRefsByPrefix(prefix);
+        }
+
+        @Override
+        public boolean hasRefs() throws IOException {
+          checkIsOpen();
+          return refDatabase.hasRefs();
+        }
+
+        @Override
+        public List<Ref> getAdditionalRefs() throws IOException {
+          checkIsOpen();
+          return refDatabase.getAdditionalRefs();
+        }
+
+        @Override
+        public Ref peel(Ref ref) throws IOException {
+          checkIsOpen();
+          return refDatabase.peel(ref);
+        }
+
+        @Override
+        public void refresh() {
+          checkIsOpen();
+          refDatabase.refresh();
+        }
+      };
+    }
+
+    @Override
+    public StoredConfig getConfig() {
+      return repo.getConfig();
+    }
+
+    @Override
+    public AttributesNodeProvider createAttributesNodeProvider() {
+      checkIsOpen();
+      return repo.createAttributesNodeProvider();
+    }
+
+    @Override
+    public void scanForRepoChanges() throws IOException {
+      checkIsOpen();
+    }
+
+    @Override
+    public void notifyIndexChanged(boolean internal) {
+      checkIsOpen();
+    }
+
+    @Override
+    public ReflogReader getReflogReader(String refName) throws IOException {
+      checkIsOpen();
+      return repo.getReflogReader(refName);
+    }
+
+    private void checkIsOpen() {
+      if (refCounter <= 0) {
+        throw new IllegalStateException("Repository is not open (refCounter=" + refCounter + ")");
+      }
+    }
+
+    @Override
+    public String getIdentifier() {
+      return "foo";
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 938fffc..6b9859f4 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -3146,6 +3146,34 @@
   }
 
   @Test
+  public void resetCherryPickOf() throws Exception {
+    Change destinationChange = newChange();
+    Change cherryPickChange = newChange();
+    assertThat(newNotes(destinationChange).getChange().getCherryPickOf()).isNull();
+
+    ChangeUpdate update = newUpdate(destinationChange, changeOwner);
+    update.setCherryPickOf(
+        cherryPickChange.currentPatchSetId().getCommaSeparatedChangeAndPatchSetId());
+    update.commit();
+    assertThat(newNotes(destinationChange).getChange().getCherryPickOf())
+        .isEqualTo(cherryPickChange.currentPatchSetId());
+
+    update = newUpdate(destinationChange, changeOwner);
+    update.resetCherryPickOf();
+    update.commit();
+    assertThat(newNotes(destinationChange).getChange().getCherryPickOf()).isNull();
+
+    // Can set again after reset.
+    cherryPickChange = newChange();
+    update = newUpdate(destinationChange, changeOwner);
+    update.setCherryPickOf(
+        cherryPickChange.currentPatchSetId().getCommaSeparatedChangeAndPatchSetId());
+    update.commit();
+    assertThat(newNotes(destinationChange).getChange().getCherryPickOf())
+        .isEqualTo(cherryPickChange.currentPatchSetId());
+  }
+
+  @Test
   public void updateCount() throws Exception {
     Change c = newChange();
     assertThat(newNotes(c).getUpdateCount()).isEqualTo(1);
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index a768eaf..0c9f731 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -91,27 +91,37 @@
     assertThat(s.acquireCount).isEqualTo(0);
 
     assertThat(s.next()).isEqualTo(1);
+    assertThat(s.last()).isEqualTo(1);
     assertThat(s.acquireCount).isEqualTo(1);
     assertThat(s.next()).isEqualTo(2);
+    assertThat(s.last()).isEqualTo(2);
     assertThat(s.acquireCount).isEqualTo(1);
     assertThat(s.next()).isEqualTo(3);
+    assertThat(s.last()).isEqualTo(3);
     assertThat(s.acquireCount).isEqualTo(1);
 
     assertThat(s.next()).isEqualTo(4);
+    assertThat(s.last()).isEqualTo(4);
     assertThat(s.acquireCount).isEqualTo(2);
     assertThat(s.next()).isEqualTo(5);
+    assertThat(s.last()).isEqualTo(5);
     assertThat(s.acquireCount).isEqualTo(2);
     assertThat(s.next()).isEqualTo(6);
+    assertThat(s.last()).isEqualTo(6);
     assertThat(s.acquireCount).isEqualTo(2);
 
     assertThat(s.next()).isEqualTo(7);
+    assertThat(s.last()).isEqualTo(7);
     assertThat(s.acquireCount).isEqualTo(3);
     assertThat(s.next()).isEqualTo(8);
+    assertThat(s.last()).isEqualTo(8);
     assertThat(s.acquireCount).isEqualTo(3);
     assertThat(s.next()).isEqualTo(9);
+    assertThat(s.last()).isEqualTo(9);
     assertThat(s.acquireCount).isEqualTo(3);
 
     assertThat(s.next()).isEqualTo(10);
+    assertThat(s.last()).isEqualTo(10);
     assertThat(s.acquireCount).isEqualTo(4);
   }
 
@@ -127,6 +137,8 @@
     assertThat(s2.next()).isEqualTo(5);
     assertThat(s1.next()).isEqualTo(3);
     assertThat(s2.next()).isEqualTo(6);
+    assertThat(s1.last()).isEqualTo(3);
+    assertThat(s2.last()).isEqualTo(6);
 
     // s2 acquires 7-9; s1 acquires 10-12.
     assertThat(s2.next()).isEqualTo(7);
@@ -135,6 +147,8 @@
     assertThat(s1.next()).isEqualTo(11);
     assertThat(s2.next()).isEqualTo(9);
     assertThat(s1.next()).isEqualTo(12);
+    assertThat(s1.last()).isEqualTo(12);
+    assertThat(s2.last()).isEqualTo(9);
   }
 
   @Test
@@ -284,48 +298,61 @@
   }
 
   @Test
-  public void nextWithCountOneCaller() throws Exception {
+  public void nextWithCountAndLastByOneCaller() throws Exception {
     RepoSequence s = newSequence("id", 1, 3);
     assertThat(s.next(2)).containsExactly(1, 2).inOrder();
     assertThat(s.acquireCount).isEqualTo(1);
+    assertThat(s.last()).isEqualTo(2);
     assertThat(s.next(2)).containsExactly(3, 4).inOrder();
     assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.last()).isEqualTo(4);
     assertThat(s.next(2)).containsExactly(5, 6).inOrder();
     assertThat(s.acquireCount).isEqualTo(2);
+    assertThat(s.last()).isEqualTo(6);
 
     assertThat(s.next(3)).containsExactly(7, 8, 9).inOrder();
     assertThat(s.acquireCount).isEqualTo(3);
+    assertThat(s.last()).isEqualTo(9);
     assertThat(s.next(3)).containsExactly(10, 11, 12).inOrder();
     assertThat(s.acquireCount).isEqualTo(4);
+    assertThat(s.last()).isEqualTo(12);
     assertThat(s.next(3)).containsExactly(13, 14, 15).inOrder();
     assertThat(s.acquireCount).isEqualTo(5);
+    assertThat(s.last()).isEqualTo(15);
 
     assertThat(s.next(7)).containsExactly(16, 17, 18, 19, 20, 21, 22).inOrder();
     assertThat(s.acquireCount).isEqualTo(6);
+    assertThat(s.last()).isEqualTo(22);
     assertThat(s.next(7)).containsExactly(23, 24, 25, 26, 27, 28, 29).inOrder();
     assertThat(s.acquireCount).isEqualTo(7);
+    assertThat(s.last()).isEqualTo(29);
     assertThat(s.next(7)).containsExactly(30, 31, 32, 33, 34, 35, 36).inOrder();
     assertThat(s.acquireCount).isEqualTo(8);
+    assertThat(s.last()).isEqualTo(36);
   }
 
   @Test
-  public void nextWithCountMultipleCallers() throws Exception {
+  public void nextWithCountAndLastByMultipleCallers() throws Exception {
     RepoSequence s1 = newSequence("id", 1, 3);
     RepoSequence s2 = newSequence("id", 1, 4);
 
     assertThat(s1.next(2)).containsExactly(1, 2).inOrder();
+    assertThat(s1.last()).isEqualTo(2);
     assertThat(s1.acquireCount).isEqualTo(1);
 
     // s1 hasn't exhausted its last batch.
     assertThat(s2.next(2)).containsExactly(4, 5).inOrder();
+    assertThat(s2.last()).isEqualTo(5);
     assertThat(s2.acquireCount).isEqualTo(1);
 
     // s1 acquires again to cover this request, plus a whole new batch.
     assertThat(s1.next(3)).containsExactly(3, 8, 9);
+    assertThat(s1.last()).isEqualTo(9);
     assertThat(s1.acquireCount).isEqualTo(2);
 
     // s2 hasn't exhausted its last batch, do so now.
     assertThat(s2.next(2)).containsExactly(6, 7);
+    assertThat(s2.last()).isEqualTo(7);
     assertThat(s2.acquireCount).isEqualTo(1);
   }
 
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index dc980c2..13e81f7 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -27,11 +27,15 @@
 guice-servlet
 httpasyncclient
 httpcore-nio
+impl-log4j
 j2objc
 jackson-annotations
 jackson-core
+jcl-over-slf4j
 jna
 jruby
+log-api
+log-ext
 log4j
 mina-core
 nekohtml
diff --git a/package.json b/package.json
index 70f290b..c75795d 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,10 @@
   "description": "Gerrit Code Review",
   "dependencies": {},
   "devDependencies": {
-    "@bazel/rollup": "^2.0.0",
-    "@bazel/terser": "^2.0.0",
-    "@bazel/typescript": "^2.0.0",
+    "@bazel/concatjs": "^5.1.0",
+    "@bazel/rollup": "^5.1.0",
+    "@bazel/terser": "^5.1.0",
+    "@bazel/typescript": "^5.1.0",
     "eslint": "^6.6.0",
     "eslint-config-google": "^0.13.0",
     "eslint-plugin-html": "^6.0.0",
@@ -16,6 +17,7 @@
     "gts": "^2.0.2",
     "polymer-cli": "^1.9.11",
     "prettier": "2.0.5",
+    "rollup": "^2.3.4",
     "terser": "^4.8.0",
     "typescript": "3.9.5"
   },
diff --git a/plugins/replication b/plugins/replication
index bad55d9..fb4854b 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit bad55d97ac86f6b16b63a1c4870762aa101332d8
+Subproject commit fb4854b57300f1060690b59722eae8c3a18cdab5
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 2266ba0..41ee468 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -74,6 +74,14 @@
 
 More information for installing and using nodejs rules can be found here https://bazelbuild.github.io/rules_nodejs/install.html
 
+### Upgrade to @bazel-scoped packages
+
+It might be necessary to run this command to upgrade to major `rules_nodejs` release:
+
+```sh
+yarn remove @bazel/...
+```
+
 ## Setup typescript support in the IDE
 
 Modern IDE should automatically handle typescript settings from the 
diff --git a/polygerrit-ui/app/node_modules_licenses/BUILD b/polygerrit-ui/app/node_modules_licenses/BUILD
index 7652ddc..77400c6 100644
--- a/polygerrit-ui/app/node_modules_licenses/BUILD
+++ b/polygerrit-ui/app/node_modules_licenses/BUILD
@@ -1,4 +1,4 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//@bazel/concatjs:index.bzl", "ts_library")
 load("//tools/node_tools/node_modules_licenses:node_modules_licenses.bzl", "node_modules_licenses")
 
 filegroup(
@@ -6,17 +6,18 @@
     srcs = glob(["licenses/*.txt"]),
 )
 
+# TODO: Would be nice to use `ts_project` from @bazel/typescript instead.
+# We would prefer to not depend on @bazel/concatjs ...
 ts_library(
     name = "licenses-config",
     srcs = [
         "licenses.ts",
     ],
     compiler = "//tools/node_tools:tsc_wrapped-bin",
-    node_modules = "@tools_npm//:node_modules",
     tsconfig = "tsconfig.json",
     deps = [
         "//tools/node_tools/node_modules_licenses:licenses-map",
-        "@tools_npm//@types/node",
+        "@tools_npm//:node_modules",
     ],
 )
 
diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
index 2a75fc1..3bd9be9 100755
--- a/tools/js/download_bower.py
+++ b/tools/js/download_bower.py
@@ -55,7 +55,7 @@
         raise OSError('Command failed: %s' % ' '.join(cmd))
 
     try:
-        info = json.loads(out)
+        info = json.loads(out.decode('utf8'))
     except ValueError:
         raise ValueError('invalid JSON from %s:\n%s' % (" ".join(cmd), out))
     info_name = info.get('name')
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 5736e4d..10898a4 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.3.10-SNAPSHOT</version>
+  <version>3.3.12-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 09c7b55..e327bc2 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.3.10-SNAPSHOT</version>
+  <version>3.3.12-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index b7f1bab..f571f21 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.3.10-SNAPSHOT</version>
+  <version>3.3.12-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 48d5f0b..38ace2f 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.3.10-SNAPSHOT</version>
+  <version>3.3.12-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/tools/maven/package.bzl b/tools/maven/package.bzl
index b25656d..eacb02b 100644
--- a/tools/maven/package.bzl
+++ b/tools/maven/package.bzl
@@ -40,7 +40,7 @@
         src = {},
         doc = {},
         war = {}):
-    build_cmd = ["bazel_cmd", "build"]
+    build_cmd = ["bazel_cmd", "build", "--java_toolchain=//tools:error_prone_warnings_toolchain_java11"]
     mvn_cmd = ["python", "tools/maven/mvn.py", "-v", version]
     api_cmd = mvn_cmd[:]
     api_targets = []
diff --git a/tools/node_tools/BUILD b/tools/node_tools/BUILD
index 03e3a13..8aeed017 100644
--- a/tools/node_tools/BUILD
+++ b/tools/node_tools/BUILD
@@ -28,13 +28,19 @@
 
 # Create a tsc_wrapped compiler rule to use in the ts_library
 # compiler attribute when using self-managed dependencies
+# TODO: Would be nice to just use `tsc-bin` below instead.
+# We would prefer to not depend on @bazel/concatjs ...
 nodejs_binary(
     name = "tsc_wrapped-bin",
     # Point bazel to your node_modules to find the entry point
-    data = ["@tools_npm//:node_modules"],
+    data = [
+        "@tools_npm//:node_modules",
+        "@tools_npm//@bazel/concatjs",
+        "@tools_npm//typescript",
+    ],
     # It seems, bazel uses different approaches to compile ts files (it runs some
     # ts service in background). It works without any workaround.
-    entry_point = "@tools_npm//:node_modules/@bazel/typescript/internal/tsc_wrapped/tsc_wrapped.js",
+    entry_point = "@tools_npm//:node_modules/@bazel/concatjs/internal/tsc_wrapped/tsc_wrapped.js",
 )
 
 # Wrap a typescript into a tsc-bin binary.
diff --git a/tools/node_tools/node_modules_licenses/BUILD b/tools/node_tools/node_modules_licenses/BUILD
index 581b3a9..f28ed35 100644
--- a/tools/node_tools/node_modules_licenses/BUILD
+++ b/tools/node_tools/node_modules_licenses/BUILD
@@ -1,17 +1,18 @@
 load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//@bazel/concatjs:index.bzl", "ts_library")
 load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
 
 package(default_visibility = ["//visibility:public"])
 
+# TODO: Would be nice to use `ts_project` from @bazel/typescript instead.
+# We would prefer to not depend on @bazel/concatjs ...
 ts_library(
     name = "licenses-map",
     srcs = glob(["*.ts"]),
     compiler = "//tools/node_tools:tsc_wrapped-bin",
-    node_modules = "@tools_npm//:node_modules",
     tsconfig = "tsconfig.json",
     deps = [
-        "@tools_npm//@types/node",
+        "@tools_npm//:node_modules",
     ],
 )
 
diff --git a/tools/node_tools/node_modules_licenses/tsconfig.json b/tools/node_tools/node_modules_licenses/tsconfig.json
index 2046c394..cb7bb60 100644
--- a/tools/node_tools/node_modules_licenses/tsconfig.json
+++ b/tools/node_tools/node_modules_licenses/tsconfig.json
@@ -1,5 +1,13 @@
 {
   "compilerOptions": {
+    "plugins": [
+      {
+        "name": "@bazel/tsetse",
+        "disabledRules": [
+          "must-type-assert-json-parse"
+        ]
+      }
+    ],
     "target": "es6",
     "module": "commonjs",
     "allowSyntheticDefaultImports": true,
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 1030877..038f191 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -3,8 +3,9 @@
   "description": "Gerrit Build Tools",
   "browser": false,
   "dependencies": {
-    "@bazel/rollup": "^2.0.0",
-    "@bazel/typescript": "^2.0.0",
+    "@bazel/concatjs": "^5.1.0",
+    "@bazel/rollup": "^5.1.0",
+    "@bazel/typescript": "^5.1.0",
     "@types/node": "^10.17.12",
     "@types/parse5": "^4.0.0",
     "@types/parse5-html-rewriting-stream": "^5.1.2",
@@ -13,7 +14,7 @@
     "parse5-html-rewriting-stream": "^5.1.1",
     "polymer-bundler": "^4.0.10",
     "polymer-cli": "^1.9.11",
-    "rollup": "^1.27.5",
+    "rollup": "^2.3.4",
     "rollup-plugin-node-resolve": "^5.2.0",
     "rollup-plugin-terser": "^5.1.3",
     "typescript": "3.9.5"
diff --git a/tools/node_tools/polygerrit_app_preprocessor/BUILD b/tools/node_tools/polygerrit_app_preprocessor/BUILD
index b5ee34f..47f2a41 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/BUILD
+++ b/tools/node_tools/polygerrit_app_preprocessor/BUILD
@@ -1,13 +1,14 @@
 load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
 load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//@bazel/concatjs:index.bzl", "ts_library")
 
 package(default_visibility = ["//visibility:public"])
 
+# TODO: Would be nice to use `ts_project` from @bazel/typescript instead.
+# We would prefer to not depend on @bazel/concatjs ...
 ts_library(
     name = "preprocessor",
     srcs = glob(["*.ts"]),
-    node_modules = "@tools_npm//:node_modules",
     tsconfig = "tsconfig.json",
     deps = [
         "//tools/node_tools/utils",
diff --git a/tools/node_tools/polygerrit_app_preprocessor/links-updater.ts b/tools/node_tools/polygerrit_app_preprocessor/links-updater.ts
index 24e445d..9f872cc 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/links-updater.ts
+++ b/tools/node_tools/polygerrit_app_preprocessor/links-updater.ts
@@ -39,7 +39,7 @@
     process.exit(1);
   }
 
-  const jsonRedirects: JSONRedirects = JSON.parse(fs.readFileSync(process.argv[3], {encoding: "utf-8"}));
+  const jsonRedirects: JSONRedirects = JSON.parse(fs.readFileSync(process.argv[3], {encoding: "utf-8"})) as JSONRedirects;
   const redirectsResolver = new RedirectsResolver(jsonRedirects.redirects);
 
   const input = readMultilineParamFile(process.argv[2]);
diff --git a/tools/node_tools/utils/BUILD b/tools/node_tools/utils/BUILD
index 5c407ca..0a6e768 100644
--- a/tools/node_tools/utils/BUILD
+++ b/tools/node_tools/utils/BUILD
@@ -1,11 +1,12 @@
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//@bazel/concatjs:index.bzl", "ts_library")
 
 package(default_visibility = ["//visibility:public"])
 
+# TODO: Would be nice to use `ts_project` from @bazel/typescript instead.
+# We would prefer to not depend on @bazel/concatjs ...
 ts_library(
     name = "utils",
     srcs = glob(["*.ts"]),
-    node_modules = "@tools_npm//:node_modules",
     tsconfig = "tsconfig.json",
     deps = [
         "@tools_npm//:node_modules",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 993bfe9..527868b 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -492,20 +492,39 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
-"@bazel/rollup@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-2.0.0.tgz#1980cb3f6922227659260bfdca99c457889a5bc1"
-  integrity sha512-mifUfCZbD1RIhfowh4N8E4881ag3FChz7F4z35wxMOP52g1q3+6Bvh5wv9iysFQopxGmS5jNEj3Dq/CWtSoOnw==
-
-"@bazel/typescript@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-2.0.0.tgz#2ff5615f09c733cc681ba2ada92b11c356b694cd"
-  integrity sha512-5FPkxULWIjAKLG5J1XvpXpY1/4IK39dAoWA/Hhg+16gXTES32fT8w42k96pb6BTaNnyBuYgIHBpELEAJ40OOAQ==
+"@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==
   dependencies:
     protobufjs "6.8.8"
+    source-map-support "0.5.9"
+    tsutils "3.21.0"
+
+"@bazel/rollup@^5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.1.0.tgz#dc858ddc93c9fdb9cc2e7982e632c939c646ebdc"
+  integrity sha512-wEiWdSyVbsycSirSYjR6FGfPGbRNI7sGNAYmrV0hIzYIi+KqXeTNcwKIRSE9PESP3mb0VWbZmHvXvmrWk6daPQ==
+  dependencies:
+    "@bazel/worker" "5.1.0"
+
+"@bazel/typescript@^5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-5.1.0.tgz#348552355cc92a43f22e637fabce76ed64505548"
+  integrity sha512-E7wYv1tBFtcsFp0YN7Cf9Lv184xOzvT5WJKwZxt+43oq8R5tGmTSuqQwm4c9JmEq6s0eZmwUaRv+WXp9hxsE4A==
+  dependencies:
+    "@bazel/worker" "5.1.0"
+    protobufjs "6.8.8"
     semver "5.6.0"
     source-map-support "0.5.9"
-    tsutils "2.27.2"
+    tsutils "3.21.0"
+
+"@bazel/worker@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.1.0.tgz#6f1e0f3ef628e3449d424cacd341c9abd09a3735"
+  integrity sha512-u3aU93UtHz3vL6ozezq0jnw83s1cNT4dAnW+vvB7M++YKFlB3CWzZFb0JRJbCp1b6DDe30ML0WOdd3nVYuylpw==
+  dependencies:
+    google-protobuf "^3.6.1"
 
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
@@ -3732,6 +3751,11 @@
     bindings "^1.5.0"
     nan "^2.12.1"
 
+fsevents@~2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -3953,6 +3977,11 @@
     pify "^3.0.0"
     slash "^1.0.0"
 
+google-protobuf@^3.6.1:
+  version "3.19.4"
+  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.4.tgz#8d32c3e34be9250956f28c0fb90955d13f311888"
+  integrity sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg==
+
 got@^5.0.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
@@ -6827,7 +6856,7 @@
   dependencies:
     estree-walker "^0.6.1"
 
-rollup@^1.27.5, rollup@^1.3.0:
+rollup@^1.3.0:
   version "1.30.0"
   resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.30.0.tgz#ae9c893804e8eaa8f8f74b0aaf7e7fb4374a9d01"
   integrity sha512-ANcmfaSQwpcJtZUTA0ZMNBtFcQ1B4A5FldlNqEK0WdWm9sHSKu93ffa2KV1ux8HA/yKIV/ZARV28m7rNdXJgEw==
@@ -6836,6 +6865,13 @@
     "@types/node" "*"
     acorn "^7.1.0"
 
+rollup@^2.3.4:
+  version "2.35.1"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.35.1.tgz#e6bc8d10893556a638066f89e8c97f422d03968c"
+  integrity sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA==
+  optionalDependencies:
+    fsevents "~2.1.2"
+
 run-async@^2.0.0, run-async@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@@ -7844,10 +7880,10 @@
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
   integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
 
-tsutils@2.27.2:
-  version "2.27.2"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7"
-  integrity sha512-qf6rmT84TFMuxAKez2pIfR8UCai49iQsfB7YWVjV1bKpy/d0PWT5rEOSM6La9PiHZ0k1RRZQiwVdVJfQ3BPHgg==
+tsutils@3.21.0:
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
   dependencies:
     tslib "^1.8.1"
 
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 5ae1f4c..3c4044b 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -11,8 +11,34 @@
 
     maven_jar(
         name = "log4j",
-        artifact = "ch.qos.reload4j:reload4j:1.2.18.1",
-        sha1 = "7075022a11e18c1ad230de5be074e0c691fed17b",
+        artifact = "ch.qos.reload4j:reload4j:1.2.19",
+        sha1 = "4eae9978468c5e885a6fb44df7e2bbc07a20e6ce",
+    )
+
+    SLF4J_VERS = "1.7.36"
+
+    maven_jar(
+        name = "log-api",
+        artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
+        sha1 = "6c62681a2f655b49963a5983b8b0950a6120ae14",
+    )
+
+    maven_jar(
+        name = "log-ext",
+        artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
+        sha1 = "99f282aea4b6dbca04d00f0ade6e5ed61ee7091a",
+    )
+
+    maven_jar(
+        name = "impl-log4j",
+        artifact = "org.slf4j:slf4j-reload4j:" + SLF4J_VERS,
+        sha1 = "db708f7d959dee1857ac524636e85ecf2e1781c1",
+    )
+
+    maven_jar(
+        name = "jcl-over-slf4j",
+        artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
+        sha1 = "d877e195a05aca4a2f1ad2ff14bfec1393af4b5e",
     )
 
     maven_jar(
diff --git a/version.bzl b/version.bzl
index 116ce2e..7b5e75c 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.3.10-SNAPSHOT"
+GERRIT_VERSION = "3.3.12-SNAPSHOT"
diff --git a/yarn.lock b/yarn.lock
index 438cafd..d20d82e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -485,25 +485,44 @@
     lodash "^4.17.11"
     to-fast-properties "^2.0.0"
 
-"@bazel/rollup@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-2.0.0.tgz#1980cb3f6922227659260bfdca99c457889a5bc1"
-  integrity sha512-mifUfCZbD1RIhfowh4N8E4881ag3FChz7F4z35wxMOP52g1q3+6Bvh5wv9iysFQopxGmS5jNEj3Dq/CWtSoOnw==
-
-"@bazel/terser@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-2.0.0.tgz#a841db8aefd7c51c216b34a26bc02a6c93d5e56a"
-  integrity sha512-6mBYcfzP6pWxycYZ8r4Lz5kgiWZ7n08bVHZBIRExFeqs7Yy92dD92LPeA9FZIzFiX00IuR9Q1Lqy23xH5q7FeQ==
-
-"@bazel/typescript@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-2.0.0.tgz#2ff5615f09c733cc681ba2ada92b11c356b694cd"
-  integrity sha512-5FPkxULWIjAKLG5J1XvpXpY1/4IK39dAoWA/Hhg+16gXTES32fT8w42k96pb6BTaNnyBuYgIHBpELEAJ40OOAQ==
+"@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==
   dependencies:
     protobufjs "6.8.8"
+    source-map-support "0.5.9"
+    tsutils "3.21.0"
+
+"@bazel/rollup@^5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.1.0.tgz#dc858ddc93c9fdb9cc2e7982e632c939c646ebdc"
+  integrity sha512-wEiWdSyVbsycSirSYjR6FGfPGbRNI7sGNAYmrV0hIzYIi+KqXeTNcwKIRSE9PESP3mb0VWbZmHvXvmrWk6daPQ==
+  dependencies:
+    "@bazel/worker" "5.1.0"
+
+"@bazel/terser@^5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-5.1.0.tgz#5c82b93f4d9def8103c16be2dd33900d156fa066"
+  integrity sha512-uE3hTqfkZr4nvlk3jwi0xx6URqqI7r6GGPtDAU02/PVei+O4PfThaov7cwHO+D1FnoLncDqChb9Iolr7Crw/8A==
+
+"@bazel/typescript@^5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-5.1.0.tgz#348552355cc92a43f22e637fabce76ed64505548"
+  integrity sha512-E7wYv1tBFtcsFp0YN7Cf9Lv184xOzvT5WJKwZxt+43oq8R5tGmTSuqQwm4c9JmEq6s0eZmwUaRv+WXp9hxsE4A==
+  dependencies:
+    "@bazel/worker" "5.1.0"
+    protobufjs "6.8.8"
     semver "5.6.0"
     source-map-support "0.5.9"
-    tsutils "2.27.2"
+    tsutils "3.21.0"
+
+"@bazel/worker@5.1.0":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.1.0.tgz#6f1e0f3ef628e3449d424cacd341c9abd09a3735"
+  integrity sha512-u3aU93UtHz3vL6ozezq0jnw83s1cNT4dAnW+vvB7M++YKFlB3CWzZFb0JRJbCp1b6DDe30ML0WOdd3nVYuylpw==
+  dependencies:
+    google-protobuf "^3.6.1"
 
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
@@ -937,9 +956,9 @@
   integrity sha512-rp7La3m845mSESCgsJePNL/JQyhkOJA6G4vcwvVgkDAwHhGdq5GCumxmPjEk1MZf+8p5ZQAUE7tqgQRQTXN7uQ==
 
 "@types/node@^10.1.0":
-  version "10.17.24"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944"
-  integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA==
+  version "10.17.49"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.49.tgz#ecf0b67bab4b84d0ec9b0709db4aac3824a51c4a"
+  integrity sha512-PGaJNs5IZz5XgzwJvL/1zRfZB7iaJ5BydZ8/Picm+lUNYoNO9iVTQkVy5eUh0dZDrx3rBOIs3GCbCRmMuYyqwg==
 
 "@types/node@^4.0.30":
   version "4.9.3"
@@ -4257,6 +4276,11 @@
     nan "^2.12.1"
     node-pre-gyp "^0.12.0"
 
+fsevents@~2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -4537,6 +4561,11 @@
     pify "^3.0.0"
     slash "^1.0.0"
 
+google-protobuf@^3.6.1:
+  version "3.19.4"
+  resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.4.tgz#8d32c3e34be9250956f28c0fb90955d13f311888"
+  integrity sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg==
+
 got@^5.0.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
@@ -8122,6 +8151,13 @@
     "@types/node" "^12.0.10"
     acorn "^6.1.1"
 
+rollup@^2.3.4:
+  version "2.35.1"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.35.1.tgz#e6bc8d10893556a638066f89e8c97f422d03968c"
+  integrity sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA==
+  optionalDependencies:
+    fsevents "~2.1.2"
+
 run-async@^2.0.0, run-async@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@@ -9257,10 +9293,10 @@
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
   integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
 
-tsutils@2.27.2:
-  version "2.27.2"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7"
-  integrity sha512-qf6rmT84TFMuxAKez2pIfR8UCai49iQsfB7YWVjV1bKpy/d0PWT5rEOSM6La9PiHZ0k1RRZQiwVdVJfQ3BPHgg==
+tsutils@3.21.0:
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
   dependencies:
     tslib "^1.8.1"