Merge "Remove opt_ prefix"
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index d8a95ef..a35f508 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1385,6 +1385,33 @@
   The change could not be rebased due to a path conflict during merge.
 ----
 
+Rebasing a change is allowed for the change owner, users with the
+link:access-control.html#category_rebase[Rebase] permission and users
+with the link:access-control.html#category_submit[Submit] permission.
+
+In addition, the rebaser or the original uploader, if rebasing is done
+on behalf of the uploader (see `rebase_on_behalf_of_uploader` option in
+link:#rebase-input[RebaseInput]), needs to have all permissions that
+are required to create the new patch set:
+
+* the link:access-control.html#category_push[Push] permission
+* the link:access-control.html#category_add_patch_set[Add Patch Set]
+  permission (only if the user is not the change owner)
+* the link:access-control.html#category_forge_author[Forge Author]
+  permission (only if the commit author is forged)
+* the link:access-control.html#category_forge_server[Forge Server]
+  permission (only if the commit author is the server identity)
+
+The same permissions were required for the upload of the original patch
+set. This means if the rebase is done on behalf of the uploader these
+permission checks should just pass, unless the uploader lost
+permissions after the upload of the original patch set. In this case
+rebasing on behalf of the uploader is not possible and a normal rebase
+(on behalf of the rebaser) must be done, which means that the rebaser
+becomes the uploader and takes over the change. If self approvals are
+disallowed, this means that the rebaser can no longer approve the
+change (as approvals of the uploader are ignored).
+
 [[rebase-chain]]
 === Rebase Chain
 --
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 5b1fa9b..9f38fcb 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -41,6 +41,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.api.TagCommand;
@@ -490,12 +491,14 @@
     public void assertMessage(String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
       assertThat(refUpdate).isNotNull();
-      assertThat(message(refUpdate).toLowerCase()).contains(expectedMessage.toLowerCase());
+      assertThat(message(refUpdate).toLowerCase(Locale.US))
+          .contains(expectedMessage.toLowerCase(Locale.US));
     }
 
     public void assertNotMessage(String message) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
-      assertThat(message(refUpdate).toLowerCase()).doesNotContain(message.toLowerCase());
+      assertThat(message(refUpdate).toLowerCase(Locale.US))
+          .doesNotContain(message.toLowerCase(Locale.US));
     }
 
     public String getMessage() {
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index 0a42d09..c7a9a63 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -22,6 +22,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Server wide capabilities. Represented as {@link Permission} objects.
@@ -162,7 +163,7 @@
 
     NAMES_LC = new ArrayList<>(NAMES_ALL.size());
     for (String name : NAMES_ALL) {
-      NAMES_LC.add(name.toLowerCase());
+      NAMES_LC.add(name.toLowerCase(Locale.US));
     }
   }
 
@@ -173,7 +174,7 @@
 
   /** Returns true if the name is recognized as a capability name. */
   public static boolean isGlobalCapability(String varName) {
-    return NAMES_LC.contains(varName.toLowerCase());
+    return NAMES_LC.contains(varName.toLowerCase(Locale.US));
   }
 
   /** Returns true if the capability should have a range attached. */
diff --git a/java/com/google/gerrit/common/data/ParameterizedString.java b/java/com/google/gerrit/common/data/ParameterizedString.java
index 84bb535..c8c2b2b 100644
--- a/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -19,6 +19,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 /** Performs replacements on strings such as <code>Hello ${user}</code>. */
@@ -213,7 +214,7 @@
         new Function() {
           @Override
           String apply(String a) {
-            return a.toLowerCase();
+            return a.toLowerCase(Locale.US);
           }
         });
     m.put(
@@ -221,7 +222,7 @@
         new Function() {
           @Override
           String apply(String a) {
-            return a.toUpperCase();
+            return a.toUpperCase(Locale.US);
           }
         });
     m.put(
diff --git a/java/com/google/gerrit/entities/EmailHeader.java b/java/com/google/gerrit/entities/EmailHeader.java
index e43b6a3..d3710c4 100644
--- a/java/com/google/gerrit/entities/EmailHeader.java
+++ b/java/com/google/gerrit/entities/EmailHeader.java
@@ -115,8 +115,8 @@
         byte[] buf = new String(Character.toChars(cp)).getBytes(UTF_8);
         for (byte b : buf) {
           r.append('=');
-          r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase());
-          r.append(Integer.toHexString(b & 0x0f).toUpperCase());
+          r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase(Locale.US));
+          r.append(Integer.toHexString(b & 0x0f).toUpperCase(Locale.US));
         }
 
       } else {
diff --git a/java/com/google/gerrit/entities/LabelTypes.java b/java/com/google/gerrit/entities/LabelTypes.java
index a2f2e0b..fa7b741 100644
--- a/java/com/google/gerrit/entities/LabelTypes.java
+++ b/java/com/google/gerrit/entities/LabelTypes.java
@@ -19,6 +19,7 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 
@@ -38,11 +39,11 @@
   }
 
   public Optional<LabelType> byLabel(LabelId labelId) {
-    return Optional.ofNullable(byLabel().get(labelId.get().toLowerCase()));
+    return Optional.ofNullable(byLabel().get(labelId.get().toLowerCase(Locale.US)));
   }
 
   public Optional<LabelType> byLabel(String labelName) {
-    return Optional.ofNullable(byLabel().get(labelName.toLowerCase()));
+    return Optional.ofNullable(byLabel().get(labelName.toLowerCase(Locale.US)));
   }
 
   private Map<String, LabelType> byLabel() {
@@ -52,7 +53,7 @@
           Map<String, LabelType> l = new HashMap<>();
           if (labelTypes != null) {
             for (LabelType t : labelTypes) {
-              l.put(t.getName().toLowerCase(), t);
+              l.put(t.getName().toLowerCase(Locale.US), t);
             }
           }
           byLabel = l;
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 6a50711..2a34579 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -22,6 +22,7 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.function.Consumer;
 
 /** A single permission within an {@link AccessSection} of a project. */
@@ -64,37 +65,37 @@
 
   static {
     NAMES_LC = new ArrayList<>();
-    NAMES_LC.add(ABANDON.toLowerCase());
-    NAMES_LC.add(ADD_PATCH_SET.toLowerCase());
-    NAMES_LC.add(CREATE.toLowerCase());
-    NAMES_LC.add(CREATE_SIGNED_TAG.toLowerCase());
-    NAMES_LC.add(CREATE_TAG.toLowerCase());
-    NAMES_LC.add(DELETE.toLowerCase());
-    NAMES_LC.add(DELETE_CHANGES.toLowerCase());
-    NAMES_LC.add(DELETE_OWN_CHANGES.toLowerCase());
-    NAMES_LC.add(EDIT_HASHTAGS.toLowerCase());
-    NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
-    NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
-    NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
-    NAMES_LC.add(FORGE_SERVER.toLowerCase());
-    NAMES_LC.add(LABEL.toLowerCase());
-    NAMES_LC.add(LABEL_AS.toLowerCase());
-    NAMES_LC.add(REMOVE_LABEL.toLowerCase());
-    NAMES_LC.add(OWNER.toLowerCase());
-    NAMES_LC.add(PUSH.toLowerCase());
-    NAMES_LC.add(PUSH_MERGE.toLowerCase());
-    NAMES_LC.add(READ.toLowerCase());
-    NAMES_LC.add(REBASE.toLowerCase());
-    NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
-    NAMES_LC.add(REVERT.toLowerCase());
-    NAMES_LC.add(SUBMIT.toLowerCase());
-    NAMES_LC.add(SUBMIT_AS.toLowerCase());
-    NAMES_LC.add(TOGGLE_WORK_IN_PROGRESS_STATE.toLowerCase());
-    NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase());
+    NAMES_LC.add(ABANDON.toLowerCase(Locale.US));
+    NAMES_LC.add(ADD_PATCH_SET.toLowerCase(Locale.US));
+    NAMES_LC.add(CREATE.toLowerCase(Locale.US));
+    NAMES_LC.add(CREATE_SIGNED_TAG.toLowerCase(Locale.US));
+    NAMES_LC.add(CREATE_TAG.toLowerCase(Locale.US));
+    NAMES_LC.add(DELETE.toLowerCase(Locale.US));
+    NAMES_LC.add(DELETE_CHANGES.toLowerCase(Locale.US));
+    NAMES_LC.add(DELETE_OWN_CHANGES.toLowerCase(Locale.US));
+    NAMES_LC.add(EDIT_HASHTAGS.toLowerCase(Locale.US));
+    NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase(Locale.US));
+    NAMES_LC.add(FORGE_AUTHOR.toLowerCase(Locale.US));
+    NAMES_LC.add(FORGE_COMMITTER.toLowerCase(Locale.US));
+    NAMES_LC.add(FORGE_SERVER.toLowerCase(Locale.US));
+    NAMES_LC.add(LABEL.toLowerCase(Locale.US));
+    NAMES_LC.add(LABEL_AS.toLowerCase(Locale.US));
+    NAMES_LC.add(REMOVE_LABEL.toLowerCase(Locale.US));
+    NAMES_LC.add(OWNER.toLowerCase(Locale.US));
+    NAMES_LC.add(PUSH.toLowerCase(Locale.US));
+    NAMES_LC.add(PUSH_MERGE.toLowerCase(Locale.US));
+    NAMES_LC.add(READ.toLowerCase(Locale.US));
+    NAMES_LC.add(REBASE.toLowerCase(Locale.US));
+    NAMES_LC.add(REMOVE_REVIEWER.toLowerCase(Locale.US));
+    NAMES_LC.add(REVERT.toLowerCase(Locale.US));
+    NAMES_LC.add(SUBMIT.toLowerCase(Locale.US));
+    NAMES_LC.add(SUBMIT_AS.toLowerCase(Locale.US));
+    NAMES_LC.add(TOGGLE_WORK_IN_PROGRESS_STATE.toLowerCase(Locale.US));
+    NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase(Locale.US));
 
     LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL);
-    LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
-    REMOVE_LABEL_INDEX = NAMES_LC.indexOf(Permission.REMOVE_LABEL.toLowerCase());
+    LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase(Locale.US));
+    REMOVE_LABEL_INDEX = NAMES_LC.indexOf(Permission.REMOVE_LABEL.toLowerCase(Locale.US));
   }
 
   /** Returns true if the name is recognized as a permission name. */
@@ -102,7 +103,7 @@
     return isLabel(varName)
         || isLabelAs(varName)
         || isRemoveLabel(varName)
-        || NAMES_LC.contains(varName.toLowerCase());
+        || NAMES_LC.contains(varName.toLowerCase(Locale.US));
   }
 
   public static boolean hasRange(String varName) {
@@ -226,7 +227,7 @@
       return REMOVE_LABEL_INDEX;
     }
 
-    int index = NAMES_LC.indexOf(a.getName().toLowerCase());
+    int index = NAMES_LC.indexOf(a.getName().toLowerCase(Locale.US));
     return 0 <= index ? index : NAMES_LC.size();
   }
 
diff --git a/java/com/google/gerrit/extensions/client/GerritTopMenu.java b/java/com/google/gerrit/extensions/client/GerritTopMenu.java
index b7e1a5a..b9a395e 100644
--- a/java/com/google/gerrit/extensions/client/GerritTopMenu.java
+++ b/java/com/google/gerrit/extensions/client/GerritTopMenu.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.client;
 
+import java.util.Locale;
+
 public enum GerritTopMenu {
   ALL,
   MY,
@@ -25,6 +27,6 @@
   public final String menuName;
 
   GerritTopMenu() {
-    menuName = name().substring(0, 1) + name().substring(1).toLowerCase();
+    menuName = name().substring(0, 1) + name().substring(1).toLowerCase(Locale.US);
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/WebLinkInfo.java b/java/com/google/gerrit/extensions/common/WebLinkInfo.java
index eeed284..fbd8d2f 100644
--- a/java/com/google/gerrit/extensions/common/WebLinkInfo.java
+++ b/java/com/google/gerrit/extensions/common/WebLinkInfo.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.extensions.common;
 
-import com.google.errorprone.annotations.InlineMe;
 import java.util.Objects;
 
 public class WebLinkInfo {
@@ -23,12 +22,6 @@
   public String imageUrl;
   public String url;
 
-  @InlineMe(replacement = "this(name, imageUrl, url)")
-  @Deprecated
-  public WebLinkInfo(String name, String imageUrl, String url, String target) {
-    this(name, imageUrl, url);
-  }
-
   public WebLinkInfo(String name, String imageUrl, String url) {
     this.name = name;
     this.imageUrl = imageUrl;
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 71dff97..fff4045 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -37,6 +37,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -82,7 +83,7 @@
       if (strs.length != 0) {
         Map<Long, Fingerprint> fps = Maps.newHashMapWithExpectedSize(strs.length);
         for (String str : strs) {
-          str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
+          str = CharMatcher.whitespace().removeFrom(str).toUpperCase(Locale.US);
           Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
           fps.put(fp.getId(), fp);
         }
diff --git a/java/com/google/gerrit/gpg/server/GpgKeys.java b/java/com/google/gerrit/gpg/server/GpgKeys.java
index b3a2f53..00a0f57 100644
--- a/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -48,6 +48,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.Locale;
 import java.util.Map;
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.openpgp.PGPException;
@@ -106,7 +107,7 @@
 
   static ExternalId findGpgKey(String str, Iterable<ExternalId> existingExtIds)
       throws ResourceNotFoundException {
-    str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
+    str = CharMatcher.whitespace().removeFrom(str).toUpperCase(Locale.US);
     if ((str.length() != 8 && str.length() != 40)
         || !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
       throw new ResourceNotFoundException(str);
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index e909701..d6718ca 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -79,6 +79,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -159,7 +160,7 @@
 
     if (!_env.envMap.containsKey("SystemRoot")) {
       String os = System.getProperty("os.name");
-      if (os != null && os.toLowerCase().contains("windows")) {
+      if (os != null && os.toLowerCase(Locale.US).contains("windows")) {
         String sysroot = System.getenv("SystemRoot");
         if (sysroot == null || sysroot.isEmpty()) {
           sysroot = "C:\\WINDOWS";
@@ -576,7 +577,7 @@
 
     for (String name : getHeaderNames(req)) {
       final String value = req.getHeader(name);
-      env.set("HTTP_" + name.toUpperCase().replace('-', '_'), value);
+      env.set("HTTP_" + name.toUpperCase(Locale.US).replace('-', '_'), value);
     }
 
     Project.NameKey nameKey = projectState.getNameKey();
diff --git a/java/com/google/gerrit/index/IndexType.java b/java/com/google/gerrit/index/IndexType.java
index 75f8351..d8c8f6a 100644
--- a/java/com/google/gerrit/index/IndexType.java
+++ b/java/com/google/gerrit/index/IndexType.java
@@ -19,6 +19,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
+import java.util.Locale;
 import java.util.Optional;
 
 /**
@@ -51,7 +52,7 @@
     if (Strings.isNullOrEmpty(value)) {
       return Optional.empty();
     }
-    value = value.toUpperCase().replace("-", "_");
+    value = value.toUpperCase(Locale.US).replace("-", "_");
     IndexType type = new IndexType(value);
     if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
       checkArgument(
@@ -67,7 +68,7 @@
   }
 
   public IndexType(@Nullable String type) {
-    this.type = type == null ? getDefault() : type.toLowerCase();
+    this.type = type == null ? getDefault() : type.toLowerCase(Locale.US);
   }
 
   public static String getDefault() {
diff --git a/java/com/google/gerrit/index/IndexedField.java b/java/com/google/gerrit/index/IndexedField.java
index 99004bb..94943d6 100644
--- a/java/com/google/gerrit/index/IndexedField.java
+++ b/java/com/google/gerrit/index/IndexedField.java
@@ -36,6 +36,7 @@
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.stream.StreamSupport;
@@ -351,7 +352,8 @@
 
     private static String checkName(String name) {
       String allowedCharacters = "abcdefghijklmnopqrstuvwxyz0123456789_";
-      CharMatcher m = CharMatcher.anyOf(allowedCharacters + allowedCharacters.toUpperCase());
+      CharMatcher m =
+          CharMatcher.anyOf(allowedCharacters + allowedCharacters.toUpperCase(Locale.US));
       checkArgument(name != null && m.matchesAllOf(name), "illegal field name: %s", name);
       return name;
     }
diff --git a/java/com/google/gerrit/index/query/IndexPredicate.java b/java/com/google/gerrit/index/query/IndexPredicate.java
index de81c47..0bde640 100644
--- a/java/com/google/gerrit/index/query/IndexPredicate.java
+++ b/java/com/google/gerrit/index/query/IndexPredicate.java
@@ -23,6 +23,7 @@
 import com.google.common.primitives.Longs;
 import com.google.gerrit.index.FieldType;
 import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.StreamSupport;
@@ -117,7 +118,8 @@
   }
 
   private static ImmutableSet<String> tokenizeString(String value) {
-    return StreamSupport.stream(FULL_TEXT_SPLITTER.split(value.toLowerCase()).spliterator(), false)
+    return StreamSupport.stream(
+            FULL_TEXT_SPLITTER.split(value.toLowerCase(Locale.US)).spliterator(), false)
         .filter(s -> !s.trim().isEmpty())
         .collect(toImmutableSet());
   }
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 6be78d9..8783593 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -44,6 +44,7 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.NavigableMap;
 import java.util.Properties;
@@ -195,7 +196,7 @@
         String cn = programClassName(name);
         clazz = Class.forName(PKG + "." + cn, true, loader);
       } catch (ClassNotFoundException cnfe) {
-        if (name.equals(name.toLowerCase())) {
+        if (name.equals(name.toLowerCase(Locale.US))) {
           clazz = Class.forName(PKG + "." + name, true, loader);
         } else {
           throw cnfe;
@@ -239,7 +240,7 @@
   }
 
   private static String programClassName(String cn) {
-    if (cn.equals(cn.toLowerCase())) {
+    if (cn.equals(cn.toLowerCase(Locale.US))) {
       StringBuilder buf = new StringBuilder();
       buf.append(Character.toUpperCase(cn.charAt(0)));
       for (int i = 1; i < cn.length(); i++) {
diff --git a/java/com/google/gerrit/mail/RawMailParser.java b/java/com/google/gerrit/mail/RawMailParser.java
index 929e9f9..79d1cb8f 100644
--- a/java/com/google/gerrit/mail/RawMailParser.java
+++ b/java/com/google/gerrit/mail/RawMailParser.java
@@ -26,6 +26,7 @@
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.time.Instant;
+import java.util.Locale;
 import org.apache.james.mime4j.MimeException;
 import org.apache.james.mime4j.dom.Entity;
 import org.apache.james.mime4j.dom.Message;
@@ -90,7 +91,7 @@
 
     // Add additional headers
     mimeMessage.getHeader().getFields().stream()
-        .filter(f -> !MAIN_HEADERS.contains(f.getName().toLowerCase()))
+        .filter(f -> !MAIN_HEADERS.contains(f.getName().toLowerCase(Locale.US)))
         .forEach(f -> messageBuilder.addAdditionalHeader(f.getName() + ": " + f.getBody()));
 
     // Add text and html body parts
diff --git a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 7666076..865f7d7 100644
--- a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.Nullable;
 import java.io.Console;
 import java.util.EnumSet;
+import java.util.Locale;
 import java.util.Set;
 
 /** Console based interaction with the invoking user. */
@@ -165,15 +166,15 @@
         String def, Set<String> allowedValues, @FormatString String fmt, Object... args) {
       for (; ; ) {
         String r = readString(def, fmt, args);
-        if (allowedValues.contains(r.toLowerCase())) {
-          return r.toLowerCase();
+        if (allowedValues.contains(r.toLowerCase(Locale.US))) {
+          return r.toLowerCase(Locale.US);
         }
         if (!"?".equals(r)) {
           console.printf("error: '%s' is not a valid choice\n", r);
         }
         console.printf("       Supported options are:\n");
         for (String v : allowedValues) {
-          console.printf("         %s\n", v.toLowerCase());
+          console.printf("         %s\n", v.toLowerCase(Locale.US));
         }
       }
     }
@@ -210,7 +211,8 @@
         T def, A options, String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (; ; ) {
-        String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString().toLowerCase());
+        String r =
+            console.readLine("%-30s [%s/?]: ", prompt, def.toString().toLowerCase(Locale.US));
         if (r == null) {
           throw abort();
         }
@@ -228,7 +230,7 @@
         }
         console.printf("       Supported options are:\n");
         for (T e : options) {
-          console.printf("         %s\n", e.toString().toLowerCase());
+          console.printf("         %s\n", e.toString().toLowerCase(Locale.US));
         }
       }
     }
diff --git a/java/com/google/gerrit/pgm/util/AbstractProgram.java b/java/com/google/gerrit/pgm/util/AbstractProgram.java
index 96b042a..00cba31 100644
--- a/java/com/google/gerrit/pgm/util/AbstractProgram.java
+++ b/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlers;
 import java.io.StringWriter;
+import java.util.Locale;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.Option;
 
@@ -35,7 +36,7 @@
     if (0 < dot) {
       n = n.substring(dot + 1);
     }
-    return n.toLowerCase();
+    return n.toLowerCase(Locale.US);
   }
 
   public final int main(String[] argv) throws Exception {
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index 2c5b539..2265055 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -31,6 +31,7 @@
 import java.security.SecureRandom;
 import java.util.Collection;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Random;
 import java.util.Set;
@@ -149,7 +150,7 @@
   }
 
   public static String status(Change c) {
-    return c != null ? c.getStatus().name().toLowerCase() : "deleted";
+    return c != null ? c.getStatus().name().toLowerCase(Locale.US) : "deleted";
   }
 
   private static final Pattern LINK_CHANGE_ID_PATTERN = Pattern.compile("I[0-9a-f]{40}");
diff --git a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
index 89727c7..676640d 100644
--- a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
+++ b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.entities.PatchSetApproval.UUID;
 import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
 import java.time.Instant;
+import java.util.Locale;
 import javax.inject.Singleton;
 
 /**
@@ -44,6 +45,6 @@
                 value,
                 invocationCount)
             .replace("-", "_")
-            .toLowerCase());
+            .toLowerCase(Locale.US));
   }
 }
diff --git a/java/com/google/gerrit/server/cache/PerThreadProjectCache.java b/java/com/google/gerrit/server/cache/PerThreadProjectCache.java
index 86f1d2d..8394343 100644
--- a/java/com/google/gerrit/server/cache/PerThreadProjectCache.java
+++ b/java/com/google/gerrit/server/cache/PerThreadProjectCache.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.cache;
 
 import com.google.common.collect.Maps;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.entities.Project;
 import java.util.Map;
 import java.util.function.Supplier;
@@ -39,6 +40,7 @@
 
   private PerThreadProjectCache() {}
 
+  @CanIgnoreReturnValue
   public static <T> T getOrCompute(PerThreadCache.Key<Project.NameKey> key, Supplier<T> loader) {
     PerThreadCache perThreadCache = PerThreadCache.get();
     if (perThreadCache != null) {
diff --git a/java/com/google/gerrit/server/change/ArchiveFormatInternal.java b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
index f6e9ff9..0ed1f11 100644
--- a/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
+++ b/java/com/google/gerrit/server/change/ArchiveFormatInternal.java
@@ -17,6 +17,7 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Locale;
 import org.apache.commons.compress.archivers.ArchiveOutputStream;
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.api.ArchiveCommand.Format;
@@ -47,7 +48,7 @@
   }
 
   public String getShortName() {
-    return name().toLowerCase();
+    return name().toLowerCase(Locale.US);
   }
 
   public String getMimeType() {
diff --git a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
index 7fd075e..5e6a520 100644
--- a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Multimap;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
@@ -139,7 +140,7 @@
 
     @Override
     public String toString() {
-      return StringUtils.capitalize(name().toLowerCase());
+      return StringUtils.capitalize(name().toLowerCase(Locale.US));
     }
   }
 
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 8ac858c..4fdbd4a 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -29,6 +29,7 @@
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 
@@ -101,7 +102,7 @@
   @Nullable
   private static String toCoreScheme(String s) {
     try {
-      Field f = CoreDownloadSchemes.class.getField(s.toUpperCase());
+      Field f = CoreDownloadSchemes.class.getField(s.toUpperCase(Locale.US));
       int m = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;
       if ((f.getModifiers() & m) == m && f.getType() == String.class) {
         return (String) f.get(null);
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index c76c78e..ab5c988 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -170,6 +170,11 @@
     public String getTotalDisplay(int total) {
       return String.valueOf(total);
     }
+
+    @Override
+    public void showDuration(boolean enabled) {
+      // not implemented
+    }
   }
 
   /** Handle for a sub-task whose total work can be updated while the task is in progress. */
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 7b8c0d2..0e9f7ca 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -195,6 +195,7 @@
 import com.google.gerrit.server.update.SuperprojectUpdateOnSubmission;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
@@ -891,7 +892,10 @@
                   case UPDATE:
                   case UPDATE_NONFASTFORWARD:
                     Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
-                    autoCloseChanges(c, closeProgress);
+                    try (RefUpdateContext ctx =
+                        RefUpdateContext.open(RefUpdateType.AUTO_CLOSE_CHANGES)) {
+                      autoCloseChanges(c, closeProgress);
+                    }
                     closeProgress.end();
                     break;
 
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index ed58a0b..cc86ffc 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -150,7 +150,7 @@
           .build(
               a -> {
                 String preferredEmail = a.account().preferredEmail();
-                return preferredEmail != null ? preferredEmail.toLowerCase() : null;
+                return preferredEmail != null ? preferredEmail.toLowerCase(Locale.US) : null;
               });
 
   public static final IndexedField<AccountState, String>.SearchSpec
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 92e722d..1d5818a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -1042,7 +1042,7 @@
 
   public static String formatLabel(
       String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
-    return label.toLowerCase()
+    return label.toLowerCase(Locale.US)
         + (value >= 0 ? "+" : "")
         + value
         + (accountId != null ? "," + formatAccount(accountId) : "")
@@ -1055,7 +1055,7 @@
 
   public static String formatLabel(
       String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
-    return label.toLowerCase()
+    return label.toLowerCase(Locale.US)
         + "="
         + value
         + (accountId != null ? "," + formatAccount(accountId) : "")
@@ -1567,7 +1567,7 @@
         continue;
       }
       for (SubmitRecord.Label label : rec.labels) {
-        String sl = label.status.toString() + ',' + label.label.toLowerCase();
+        String sl = label.status.toString() + ',' + label.label.toLowerCase(Locale.US);
         result.add(sl);
         String slc = sl + ',';
         if (label.appliedBy != null) {
@@ -1596,28 +1596,28 @@
           result.add(
               SubmitRecord.Label.Status.OK.name()
                   + ","
-                  + srResult.submitRequirement().name().toLowerCase());
+                  + srResult.submitRequirement().name().toLowerCase(Locale.US));
           result.add(
               SubmitRecord.Label.Status.MAY.name()
                   + ","
-                  + srResult.submitRequirement().name().toLowerCase());
+                  + srResult.submitRequirement().name().toLowerCase(Locale.US));
           break;
         case UNSATISFIED:
           result.add(
               SubmitRecord.Label.Status.NEED.name()
                   + ","
-                  + srResult.submitRequirement().name().toLowerCase());
+                  + srResult.submitRequirement().name().toLowerCase(Locale.US));
           result.add(
               SubmitRecord.Label.Status.REJECT.name()
                   + ","
-                  + srResult.submitRequirement().name().toLowerCase());
+                  + srResult.submitRequirement().name().toLowerCase(Locale.US));
           break;
         case NOT_APPLICABLE:
         case ERROR:
           result.add(
               SubmitRecord.Label.Status.IMPOSSIBLE.name()
                   + ","
-                  + srResult.submitRequirement().name().toLowerCase());
+                  + srResult.submitRequirement().name().toLowerCase(Locale.US));
       }
     }
     return result;
diff --git a/java/com/google/gerrit/server/ioutil/HostPlatform.java b/java/com/google/gerrit/server/ioutil/HostPlatform.java
index e27d17c..b912c52 100644
--- a/java/com/google/gerrit/server/ioutil/HostPlatform.java
+++ b/java/com/google/gerrit/server/ioutil/HostPlatform.java
@@ -16,6 +16,7 @@
 
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.util.Locale;
 
 public final class HostPlatform {
   private static final boolean win32 = compute("windows");
@@ -34,7 +35,7 @@
     final String osDotName =
         AccessController.doPrivileged(
             (PrivilegedAction<String>) () -> System.getProperty("os.name"));
-    return osDotName != null && osDotName.toLowerCase().contains(platform);
+    return osDotName != null && osDotName.toLowerCase(Locale.US).contains(platform);
   }
 
   private HostPlatform() {}
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index d298f1c..ec81cf6 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -24,6 +24,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeSizeBucket;
@@ -465,7 +466,18 @@
   }
 
   @Override
-  protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
+  protected boolean isRecipientAllowed(Address addr) throws PermissionBackendException {
+    if (!projectState.statePermitsRead()) {
+      return false;
+    }
+    return args.permissionBackend
+        .user(args.anonymousUser.get())
+        .change(changeData)
+        .test(ChangePermission.READ);
+  }
+
+  @Override
+  protected boolean isRecipientAllowed(Account.Id to) throws PermissionBackendException {
     if (!projectState.statePermitsRead()) {
       return false;
     }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index fa4f04e..d00c874 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -528,6 +528,45 @@
    * Adds a recipient that the email will be sent to.
    *
    * @param rt category of recipient (TO, CC, BCC)
+   * @param addr Name and email of the recipient.
+   */
+  public final void addByEmail(RecipientType rt, Address addr) {
+    addByEmail(rt, addr, false);
+  }
+
+  /**
+   * Adds a recipient that the email will be sent to.
+   *
+   * @param rt category of recipient (TO, CC, BCC).
+   * @param addr Name and email of the recipient.
+   * @param override if the recipient was added previously and override is false no change is made
+   *     regardless of {@code rt}.
+   */
+  public final void addByEmail(RecipientType rt, Address addr, boolean override) {
+    try {
+      if (isRecipientAllowed(addr)) {
+        add(rt, addr, override);
+      }
+    } catch (PermissionBackendException e) {
+      logger.atSevere().withCause(e).log("Error checking permissions for email address: %s", addr);
+    }
+  }
+
+  /**
+   * Returns whether this email is allowed to be sent to the given address
+   *
+   * @param addr email address of recipient.
+   * @throws PermissionBackendException thrown if checking a permission fails due to an error in the
+   *     permission backend
+   */
+  protected boolean isRecipientAllowed(Address addr) throws PermissionBackendException {
+    return true;
+  }
+
+  /**
+   * Adds a recipient that the email will be sent to.
+   *
+   * @param rt category of recipient (TO, CC, BCC)
    * @param to Gerrit Account of the recipient.
    */
   protected void addByAccountId(RecipientType rt, Account.Id to) {
@@ -544,45 +583,27 @@
    */
   protected void addByAccountId(RecipientType rt, Account.Id to, boolean override) {
     try {
-      if (!rcptTo.contains(to) && isVisibleTo(to)) {
+      if (!rcptTo.contains(to) && isRecipientAllowed(to)) {
         rcptTo.add(to);
-        addByEmail(rt, toAddress(to), override);
+        add(rt, toAddress(to), override);
       }
     } catch (PermissionBackendException e) {
-      logger.atSevere().withCause(e).log("Error reading database for account: %s", to);
+      logger.atSevere().withCause(e).log("Error checking permissions for account: %s", to);
     }
   }
 
   /**
-   * Returns whether this email is visible to the given account
+   * Returns whether this email is allowed to be sent to the given account
    *
    * @param to account.
    * @throws PermissionBackendException thrown if checking a permission fails due to an error in the
    *     permission backend
    */
-  protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
+  protected boolean isRecipientAllowed(Account.Id to) throws PermissionBackendException {
     return true;
   }
 
-  /**
-   * Adds a recipient that the email will be sent to.
-   *
-   * @param rt category of recipient (TO, CC, BCC)
-   * @param addr Name and email of the recipient.
-   */
-  public final void addByEmail(RecipientType rt, Address addr) {
-    addByEmail(rt, addr, false);
-  }
-
-  /**
-   * Adds a recipient that the email will be sent to.
-   *
-   * @param rt category of recipient (TO, CC, BCC).
-   * @param addr Name and email of the recipient.
-   * @param override if the recipient was added previously and override is false no change is made
-   *     regardless of {@code rt}.
-   */
-  public final void addByEmail(RecipientType rt, Address addr, boolean override) {
+  private final void add(RecipientType rt, Address addr, boolean override) {
     if (addr != null && addr.email() != null && addr.email().length() > 0) {
       if (!args.validator.isValid(addr.email())) {
         logger.atWarning().log("Not emailing %s (invalid email address)", addr.email());
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
index 38ab8e9..84de569 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.git.InsertedObject;
 import java.io.IOException;
 import java.util.List;
+import java.util.Locale;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -125,10 +126,10 @@
       List<FooterLine> src = getFooterLines();
       footerLines = MultimapBuilder.hashKeys(src.size()).arrayListValues(1).build();
       for (FooterLine fl : src) {
-        footerLines.put(fl.getKey().toLowerCase(), fl.getValue());
+        footerLines.put(fl.getKey().toLowerCase(Locale.US), fl.getValue());
       }
     }
-    return footerLines.get(key.getName().toLowerCase());
+    return footerLines.get(key.getName().toLowerCase(Locale.US));
   }
 
   public boolean isAttentionSetCommitOnly(boolean hasChangeMessage) {
@@ -137,7 +138,7 @@
             .keySet()
             .equals(
                 Sets.newHashSet(
-                    FOOTER_PATCH_SET.getName().toLowerCase(),
-                    FOOTER_ATTENTION.getName().toLowerCase()));
+                    FOOTER_PATCH_SET.getName().toLowerCase(Locale.US),
+                    FOOTER_ATTENTION.getName().toLowerCase(Locale.US)));
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 0ee0689..951a478 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -93,6 +93,7 @@
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -760,7 +761,7 @@
       throw expectedOneFooter(FOOTER_STATUS, statusLines);
     }
     Change.Status status =
-        Enums.getIfPresent(Change.Status.class, statusLines.get(0).toUpperCase()).orNull();
+        Enums.getIfPresent(Change.Status.class, statusLines.get(0).toUpperCase(Locale.US)).orNull();
     if (status == null) {
       throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
     }
@@ -802,7 +803,7 @@
       PatchSetState state =
           Enums.getIfPresent(
                   PatchSetState.class,
-                  withParens.substring(1, withParens.length() - 1).toUpperCase())
+                  withParens.substring(1, withParens.length() - 1).toUpperCase(Locale.US))
               .orNull();
       if (state != null) {
         return state;
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index f8c7426..0a895fb 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -95,6 +95,7 @@
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -741,7 +742,7 @@
     }
 
     if (status != null) {
-      addFooter(msg, FOOTER_STATUS, status.name().toLowerCase());
+      addFooter(msg, FOOTER_STATUS, status.name().toLowerCase(Locale.US));
       if (status.equals(Change.Status.ABANDONED)) {
         clearAttentionSet("Change was abandoned");
       }
@@ -1129,7 +1130,7 @@
   private void addPatchSetFooter(StringBuilder sb, PatchSet.Id ps) {
     addFooter(sb, FOOTER_PATCH_SET).append(ps.get());
     if (psState != null) {
-      sb.append(" (").append(psState.name().toLowerCase()).append(')');
+      sb.append(" (").append(psState.name().toLowerCase(Locale.US)).append(')');
     }
     sb.append('\n');
   }
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index f9eaa906..6c8087e0 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -720,7 +720,7 @@
     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
     extensionPanelSections = new LinkedHashMap<>();
     for (String name : rc.getSubsections(EXTENSION_PANELS)) {
-      String lower = name.toLowerCase();
+      String lower = name.toLowerCase(Locale.US);
       if (lowerNames.containsKey(lower)) {
         error(
             String.format(
@@ -970,7 +970,7 @@
     submitRequirementSections = new LinkedHashMap<>();
     for (String name : rc.getSubsections(SUBMIT_REQUIREMENT)) {
       checkDuplicateSrDefinition(rc, name);
-      String lower = name.toLowerCase();
+      String lower = name.toLowerCase(Locale.US);
       if (lowerNames.containsKey(lower)) {
         error(
             String.format(
@@ -1102,7 +1102,7 @@
     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
     labelSections = new LinkedHashMap<>();
     for (String name : rc.getSubsections(LABEL)) {
-      String lower = name.toLowerCase();
+      String lower = name.toLowerCase(Locale.US);
       if (lowerNames.containsKey(lower)) {
         error(String.format("Label \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower)));
       }
@@ -1530,7 +1530,7 @@
     if (capability != null) {
       Set<String> have = new HashSet<>();
       for (Permission permission : sort(capability.getPermissions())) {
-        have.add(permission.getName().toLowerCase());
+        have.add(permission.getName().toLowerCase(Locale.US));
 
         boolean needRange = GlobalCapability.hasRange(permission.getName());
         List<String> rules = new ArrayList<>();
@@ -1544,7 +1544,7 @@
         rc.setStringList(CAPABILITY, null, permission.getName(), rules);
       }
       for (String varName : rc.getNames(CAPABILITY)) {
-        if (!have.contains(varName.toLowerCase())) {
+        if (!have.contains(varName.toLowerCase(Locale.US))) {
           rc.unset(CAPABILITY, null, varName);
         }
       }
@@ -1575,7 +1575,7 @@
 
       Set<String> have = new HashSet<>();
       for (Permission permission : sort(as.getPermissions())) {
-        have.add(permission.getName().toLowerCase());
+        have.add(permission.getName().toLowerCase(Locale.US));
 
         boolean needRange = Permission.hasRange(permission.getName());
         List<String> rules = new ArrayList<>();
@@ -1591,7 +1591,7 @@
 
       for (String varName : rc.getNames(ACCESS, refName)) {
         if (isCoreOrPluginPermission(convertLegacyPermission(varName))
-            && !have.contains(varName.toLowerCase())) {
+            && !have.contains(varName.toLowerCase(Locale.US))) {
           rc.unset(ACCESS, refName, varName);
         }
       }
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 6352f66..9899a6d 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -56,6 +56,7 @@
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -380,7 +381,7 @@
     Map<String, SubmitRequirement> requirements = new LinkedHashMap<>();
     for (ProjectState s : treeInOrder()) {
       for (SubmitRequirement requirement : s.getConfig().getSubmitRequirementSections().values()) {
-        String lowerName = requirement.name().toLowerCase();
+        String lowerName = requirement.name().toLowerCase(Locale.US);
         SubmitRequirement old = requirements.get(lowerName);
         if (old == null || old.allowOverrideInChildProjects()) {
           requirements.put(lowerName, requirement);
@@ -395,7 +396,7 @@
     Map<String, LabelType> types = new LinkedHashMap<>();
     for (ProjectState s : treeInOrder()) {
       for (LabelType type : s.getConfig().getLabelSections().values()) {
-        String lower = type.getName().toLowerCase();
+        String lower = type.getName().toLowerCase(Locale.US);
         LabelType old = types.get(lower);
         if (old == null || old.isCanOverride()) {
           types.put(lower, type);
@@ -449,11 +450,11 @@
   public List<CommentLinkInfo> getCommentLinks() {
     Map<String, CommentLinkInfo> cls = new LinkedHashMap<>();
     for (CommentLinkInfo cl : commentLinks) {
-      cls.put(cl.name.toLowerCase(), cl);
+      cls.put(cl.name.toLowerCase(Locale.US), cl);
     }
     for (ProjectState s : treeInOrder()) {
       for (StoredCommentLinkInfo cl : s.getConfig().getCommentLinkSections().values()) {
-        String name = cl.getName().toLowerCase();
+        String name = cl.getName().toLowerCase(Locale.US);
         if (cl.getOverrideOnly()) {
           CommentLinkInfo parent = cls.get(name);
           if (parent == null) {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index 870fc3e..0991f20 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -36,6 +36,7 @@
 import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Scopes;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
@@ -207,7 +208,8 @@
     return globalSubmitRequirements.stream()
         .collect(
             toImmutableMap(
-                globalRequirement -> globalRequirement.name().toLowerCase(), Function.identity()));
+                globalRequirement -> globalRequirement.name().toLowerCase(Locale.US),
+                Function.identity()));
   }
 
   /** Evaluate the predicate recursively using change data. */
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
index e54e5af..403e526 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
@@ -28,6 +28,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -142,10 +143,12 @@
             // (projectConfigRequirements should not contain legacy entries)
             // TODO(ghareeb): remove the filter statement
             .filter(entry -> !entry.getValue().isLegacy())
-            .collect(Collectors.toMap(sr -> sr.getKey().name().toLowerCase(), sr -> sr.getValue()));
+            .collect(
+                Collectors.toMap(
+                    sr -> sr.getKey().name().toLowerCase(Locale.US), sr -> sr.getValue()));
     for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy :
         legacyRequirements.entrySet()) {
-      String srName = legacy.getKey().name().toLowerCase();
+      String srName = legacy.getKey().name().toLowerCase(Locale.US);
       SubmitRequirementResult projectConfigResult = requirementsByName.get(srName);
       SubmitRequirementResult legacyResult = legacy.getValue();
       // If there's no project config requirement with the same name as the legacy requirement
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
index 11749cc..ed876c1 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.group.GroupResolver;
 import com.google.inject.Inject;
 import java.util.Arrays;
+import java.util.Locale;
 import java.util.Optional;
 
 public class ApprovalQueryBuilder extends QueryBuilder<ApprovalContext, ApprovalQueryBuilder> {
@@ -113,7 +114,7 @@
 
   private static <T extends Enum<T>> Optional<T> parseEnumValue(Class<T> clazz, String value) {
     return Optional.ofNullable(
-        Enums.getIfPresent(clazz, value.toUpperCase().replace('-', '_')).orNull());
+        Enums.getIfPresent(clazz, value.toUpperCase(Locale.US).replace('-', '_')).orNull());
   }
 
   private <T extends Enum<T>> String formatEnumValues(Class<T> clazz) {
diff --git a/java/com/google/gerrit/server/query/change/BranchSetIndexPredicate.java b/java/com/google/gerrit/server/query/change/BranchSetIndexPredicate.java
new file mode 100644
index 0000000..eb35b14
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/BranchSetIndexPredicate.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.OrPredicate;
+import com.google.gerrit.index.query.Predicate;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** A Predicate to match any number of BranchNameKeys with O(1) efficiency */
+public class BranchSetIndexPredicate extends OrPredicate<ChangeData> {
+  private final String name;
+  private final Set<BranchNameKey> branches;
+
+  public BranchSetIndexPredicate(String name, Set<BranchNameKey> branches) throws StorageException {
+    super(getPredicates(branches));
+    this.name = name;
+    this.branches = branches;
+  }
+
+  @Override
+  public boolean match(ChangeData changeData) {
+    Change change = changeData.change();
+    if (change == null) {
+      return false;
+    }
+
+    return branches.contains(change.getDest());
+  }
+
+  @Override
+  public String toString() {
+    return "BranchSetIndexPredicate[" + name + "]" + super.toString();
+  }
+
+  private static List<Predicate<ChangeData>> getPredicates(Set<BranchNameKey> branches) {
+    return branches.stream()
+        .map(
+            branchNameKey ->
+                Predicate.and(
+                    ChangePredicates.project(branchNameKey.project()),
+                    ChangePredicates.ref(branchNameKey.branch())))
+        .collect(Collectors.toList());
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index e9bf3c2..528d0ce 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -202,14 +202,14 @@
   public static Predicate<ChangeData> hashtag(String hashtag) {
     // Use toLowerCase without locale to match behavior in ChangeField.
     return new ChangeIndexPredicate(
-        ChangeField.HASHTAG_SPEC, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+        ChangeField.HASHTAG_SPEC, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase(Locale.US));
   }
 
   /** Returns a predicate that matches changes tagged with the provided {@code hashtag}. */
   public static Predicate<ChangeData> fuzzyHashtag(String hashtag) {
     // Use toLowerCase without locale to match behavior in ChangeField.
     return new ChangeIndexPredicate(
-        ChangeField.FUZZY_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+        ChangeField.FUZZY_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase(Locale.US));
   }
 
   /**
@@ -218,7 +218,7 @@
   public static Predicate<ChangeData> prefixHashtag(String hashtag) {
     // Use toLowerCase without locale to match behavior in ChangeField.
     return new ChangeIndexPredicate(
-        ChangeField.PREFIX_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+        ChangeField.PREFIX_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase(Locale.US));
   }
 
   /** Returns a predicate that matches changes that modified the provided {@code file}. */
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 14b1d11..ef067a1 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -65,6 +65,7 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.account.QueryList;
 import com.google.gerrit.server.account.VersionedAccountDestinations;
 import com.google.gerrit.server.account.VersionedAccountQueries;
 import com.google.gerrit.server.change.ChangeTriplet;
@@ -101,6 +102,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -490,7 +492,8 @@
 
   protected final Arguments args;
   protected Map<String, String> hasOperandAliases = Collections.emptyMap();
-  private Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
+  private final Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
+  private final Map<Account.Id, QueryList> queryListByAccount = new HashMap<>();
 
   private static final Splitter RULE_SPLITTER = Splitter.on("=");
   private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
@@ -1102,7 +1105,7 @@
     // submit record status, interpret as a submit record query.
     int eq = name.indexOf('=');
     if (eq > 0) {
-      String statusName = name.substring(eq + 1).toUpperCase();
+      String statusName = name.substring(eq + 1).toUpperCase(Locale.US);
       if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
         SubmitRecord.Label.Status status =
             Enums.getIfPresent(SubmitRecord.Label.Status.class, statusName).orNull();
@@ -1414,16 +1417,16 @@
     String name = null;
     Account.Id account = null;
 
-    try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
-      // [name=]<name>
-      if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
-        name = inputArgs.keyValue.get(ARG_ID_NAME).value();
-      } else if (inputArgs.positional.size() == 1) {
-        name = Iterables.getOnlyElement(inputArgs.positional);
-      } else if (inputArgs.positional.size() > 1) {
-        throw new QueryParseException("Error parsing named query: " + value);
-      }
+    // [name=]<name>
+    if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
+      name = inputArgs.keyValue.get(ARG_ID_NAME).value();
+    } else if (inputArgs.positional.size() == 1) {
+      name = Iterables.getOnlyElement(inputArgs.positional);
+    } else if (inputArgs.positional.size() > 1) {
+      throw new QueryParseException("Error parsing named query: " + value);
+    }
 
+    try {
       // [,user=<user>]
       if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
         Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
@@ -1437,9 +1440,7 @@
         account = self();
       }
 
-      VersionedAccountQueries q = VersionedAccountQueries.forUser(account);
-      q.load(args.allUsersName, git);
-      String query = q.getQueryList().getQuery(name);
+      String query = getQueryList(account).getQuery(name);
       if (query != null) {
         return parse(query);
       }
@@ -1452,6 +1453,23 @@
     throw new QueryParseException("Unknown named query: " + name);
   }
 
+  protected QueryList getQueryList(Account.Id account) throws ConfigInvalidException, IOException {
+    QueryList ql = queryListByAccount.get(account);
+    if (ql == null) {
+      ql = loadQueryList(account);
+      queryListByAccount.put(account, ql);
+    }
+    return ql;
+  }
+
+  protected QueryList loadQueryList(Account.Id account) throws ConfigInvalidException, IOException {
+    VersionedAccountQueries q = VersionedAccountQueries.forUser(account);
+    try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
+      q.load(args.allUsersName, git);
+    }
+    return q.getQueryList();
+  }
+
   @Operator
   public Predicate<ChangeData> reviewedby(String who)
       throws QueryParseException, IOException, ConfigInvalidException {
@@ -1465,16 +1483,16 @@
     String name = null;
     Account.Id account = null;
 
-    try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
-      // [name=]<name>
-      if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
-        name = inputArgs.keyValue.get(ARG_ID_NAME).value();
-      } else if (inputArgs.positional.size() == 1) {
-        name = Iterables.getOnlyElement(inputArgs.positional);
-      } else if (inputArgs.positional.size() > 1) {
-        throw new QueryParseException("Error parsing named destination: " + value);
-      }
+    // [name=]<name>
+    if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
+      name = inputArgs.keyValue.get(ARG_ID_NAME).value();
+    } else if (inputArgs.positional.size() == 1) {
+      name = Iterables.getOnlyElement(inputArgs.positional);
+    } else if (inputArgs.positional.size() > 1) {
+      throw new QueryParseException("Error parsing named destination: " + value);
+    }
 
+    try {
       // [,user=<user>]
       if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
         Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
@@ -1488,9 +1506,9 @@
         account = self();
       }
 
-      Set<BranchNameKey> destinations = getDestinationList(git, account).getDestinations(name);
+      Set<BranchNameKey> destinations = getDestinationList(account).getDestinations(name);
       if (destinations != null && !destinations.isEmpty()) {
-        return new DestinationPredicate(destinations, value);
+        return new BranchSetIndexPredicate(FIELD_DESTINATION + ":" + value, destinations);
       }
     } catch (RepositoryNotFoundException e) {
       throw new QueryParseException(
@@ -1501,20 +1519,22 @@
     throw new QueryParseException("Unknown named destination: " + name);
   }
 
-  protected DestinationList getDestinationList(Repository git, Account.Id account)
+  protected DestinationList getDestinationList(Account.Id account)
       throws ConfigInvalidException, RepositoryNotFoundException, IOException {
     DestinationList dl = destinationListByAccount.get(account);
     if (dl == null) {
-      dl = loadDestinationList(git, account);
+      dl = loadDestinationList(account);
       destinationListByAccount.put(account, dl);
     }
     return dl;
   }
 
-  protected DestinationList loadDestinationList(Repository git, Account.Id account)
+  protected DestinationList loadDestinationList(Account.Id account)
       throws ConfigInvalidException, RepositoryNotFoundException, IOException {
     VersionedAccountDestinations d = VersionedAccountDestinations.forUser(account);
-    d.load(args.allUsersName, git);
+    try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
+      d.load(args.allUsersName, git);
+    }
     return d.getDestinationList();
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index fa48511..d949ea8 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.index.change.ChangeField;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.NavigableMap;
 import java.util.Objects;
@@ -74,11 +75,11 @@
   }
 
   public static String canonicalize(Change.Status status) {
-    return status.name().toLowerCase();
+    return status.name().toLowerCase(Locale.US);
   }
 
   public static Predicate<ChangeData> parse(String value) throws QueryParseException {
-    String lower = value.toLowerCase();
+    String lower = value.toLowerCase(Locale.US);
     NavigableMap<String, Predicate<ChangeData>> head = PREDICATES.tailMap(lower, true);
     if (!head.isEmpty()) {
       // Assume no statuses share a common prefix so we can only walk one entry.
diff --git a/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
deleted file mode 100644
index 3c3d70f..0000000
--- a/java/com/google/gerrit/server/query/change/DestinationPredicate.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2015 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.query.change;
-
-import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.index.query.PostFilterPredicate;
-import java.util.Set;
-
-public class DestinationPredicate extends PostFilterPredicate<ChangeData> {
-  protected Set<BranchNameKey> destinations;
-
-  public DestinationPredicate(Set<BranchNameKey> destinations, String value) {
-    super(ChangeQueryBuilder.FIELD_DESTINATION, value);
-    this.destinations = destinations;
-  }
-
-  @Override
-  public boolean match(ChangeData object) {
-    Change change = object.change();
-    if (change == null) {
-      return false;
-    }
-    return destinations.contains(change.getDest());
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
index 1ea6c41..243712d 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -20,12 +20,13 @@
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.server.index.change.ChangeField;
+import java.util.Locale;
 import java.util.Set;
 
 public class SubmitRecordPredicate extends ChangeIndexPredicate {
   public static Predicate<ChangeData> create(
       String label, SubmitRecord.Label.Status status, Set<Account.Id> accounts) {
-    String lowerLabel = label.toLowerCase();
+    String lowerLabel = label.toLowerCase(Locale.US);
     if (accounts == null || accounts.isEmpty()) {
       return new SubmitRecordPredicate(status.name() + ',' + lowerLabel);
     }
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
index f7135982..599683e 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.inject.Inject;
 import java.util.List;
+import java.util.Locale;
 
 /** Parses a query string meant to be applied to project objects. */
 public class ProjectQueryBuilderImpl extends QueryBuilder<ProjectData, ProjectQueryBuilderImpl>
@@ -72,7 +73,7 @@
     }
     ProjectState parsedState;
     try {
-      parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
+      parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase(Locale.US));
     } catch (IllegalArgumentException e) {
       throw error("state operator must be either 'active' or 'read-only'", e);
     }
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 6ab2c44..c45694e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -45,6 +45,7 @@
 import com.google.inject.Singleton;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import org.kohsuke.args4j.Option;
@@ -124,7 +125,7 @@
   }
 
   private boolean want(String name) {
-    return query == null || query.contains(name.toLowerCase());
+    return query == null || query.contains(name.toLowerCase(Locale.US));
   }
 
   private void addRanges(Map<String, Object> have, AccountLimits limits) {
diff --git a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
index e3ab135..763212d 100644
--- a/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
+++ b/java/com/google/gerrit/server/restapi/change/AllowedFormats.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.config.DownloadConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.Locale;
 import java.util.Set;
 
 @Singleton
@@ -36,7 +37,7 @@
       for (String ext : format.getSuffixes()) {
         exts.put(ext, format);
       }
-      exts.put(format.name().toLowerCase(), format);
+      exts.put(format.name().toLowerCase(Locale.US), format);
     }
     extensions = exts.build();
 
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index 84cf209..9715a5d 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -83,7 +83,7 @@
   }
 
   /**
-   * Parses {@link ChangeResource} from {@link Change.Id}
+   * Parses {@link ChangeResource} from {@link com.google.gerrit.entities.Change.Id}
    *
    * <p>Reads the change from index, since project is unknown.
    */
@@ -106,7 +106,8 @@
   }
 
   /**
-   * Parses {@link ChangeResource} from {@link Change.Id} in {@code project} at {@code metaRevId}
+   * Parses {@link ChangeResource} from {@link com.google.gerrit.entities.Change.Id} in {@code
+   * project} at {@code metaRevId}
    *
    * <p>Read change from ChangeNotesCache, so the method can be used upon creation, when the change
    * might not be yet available in the index.
@@ -123,7 +124,7 @@
   }
 
   /**
-   * Parses {@link ChangeResource} from {@link Change.Id}
+   * Parses {@link ChangeResource} from {@link com.google.gerrit.entities.Change.Id}
    *
    * <p>Reads the change from index, since project is unknown.
    */
diff --git a/java/com/google/gerrit/server/restapi/change/OnPostReview.java b/java/com/google/gerrit/server/restapi/change/OnPostReview.java
index b179d02..4999f18 100644
--- a/java/com/google/gerrit/server/restapi/change/OnPostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/OnPostReview.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Optional;
 
@@ -28,6 +29,7 @@
    * Allows implementors to return a message that should be included into the change message that is
    * posted on post review.
    *
+   * @param when the timestamp at which the review is posted
    * @param user the user that posts the review
    * @param changeNotes the change on which post review is performed
    * @param patchSet the patch set on which post review is performed
@@ -37,6 +39,7 @@
    *     {@link Optional#empty()} if the change message should not be extended
    */
   default Optional<String> getChangeMessageAddOn(
+      Instant when,
       IdentifiedUser user,
       ChangeNotes changeNotes,
       PatchSet patchSet,
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index 0036d87..a8f8adf 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -1035,7 +1035,8 @@
     onPostReviews.runEach(
         onPostReview ->
             onPostReview
-                .getChangeMessageAddOn(user, ctx.getNotes(), ps, oldApprovals, approvals)
+                .getChangeMessageAddOn(
+                    ctx.getWhen(), user, ctx.getNotes(), ps, oldApprovals, approvals)
                 .ifPresent(
                     pluginMessage ->
                         pluginMessages.add(
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 4c7c352..691fc75 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -85,6 +85,7 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -196,7 +197,8 @@
     }
     if (topic == null) {
       return String.format(
-          "revert-%s-%s", submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase());
+          "revert-%s-%s",
+          submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase(Locale.US));
     }
     return topic;
   }
diff --git a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
index 9ce7ffd..c8f2ed6 100644
--- a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
+++ b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
@@ -33,6 +33,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Collectors;
 
@@ -59,7 +60,8 @@
         updates.asMap().entrySet().stream()
             .collect(
                 Collectors.toMap(
-                    e -> e.getKey().name().toLowerCase(), e -> toEntryInfos(e.getValue()))));
+                    e -> e.getKey().name().toLowerCase(Locale.US),
+                    e -> toEntryInfos(e.getValue()))));
   }
 
   private static List<ConfigUpdateEntryInfo> toEntryInfos(
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
index cab5b45..bfcbffc 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -51,6 +51,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
@@ -381,7 +382,7 @@
 
     String typeName = typeTerm.name();
     try {
-      return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
+      return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase(Locale.US)));
     } catch (IllegalArgumentException e) {
       return typeError(
           "Submit type rule "
diff --git a/java/com/google/gerrit/server/update/context/RefUpdateContext.java b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
index 1144e4f..56d536a 100644
--- a/java/com/google/gerrit/server/update/context/RefUpdateContext.java
+++ b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
@@ -113,6 +113,8 @@
      * <p>If a plugin updates one of a special refs - it must also open a nested context.
      */
     PLUGIN,
+    /** A ref is updated as a part of auto-close-changes. */
+    AUTO_CLOSE_CHANGES
   }
 
   /** Opens a provided context. */
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index e0805c0..143b060 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -56,6 +56,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -358,7 +359,7 @@
   }
 
   private static String asOptionName(LabelType type) {
-    return "--" + type.getName().toLowerCase();
+    return "--" + type.getName().toLowerCase(Locale.US);
   }
 
   private static Option newApproveOption(LabelType type, String usage) {
diff --git a/java/com/google/gerrit/testing/GerritTestName.java b/java/com/google/gerrit/testing/GerritTestName.java
index d287837..14493b6 100644
--- a/java/com/google/gerrit/testing/GerritTestName.java
+++ b/java/com/google/gerrit/testing/GerritTestName.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.testing;
 
 import com.google.common.base.CharMatcher;
+import java.util.Locale;
 import org.junit.BeforeClass;
 import org.junit.rules.TestName;
 import org.junit.rules.TestRule;
@@ -30,7 +31,7 @@
   }
 
   public String getSanitizedMethodName() {
-    String name = delegate.getMethodName().toLowerCase();
+    String name = delegate.getMethodName().toLowerCase(Locale.US);
     name =
         CharMatcher.inRange('a', 'z')
             .or(CharMatcher.inRange('A', 'Z'))
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index 8d1130c..8c87405 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -49,6 +49,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.NavigableSet;
@@ -135,6 +136,11 @@
       private RefUpdateContextValidator() {}
 
       public void validateRefUpdateContext(ReceiveCommand cmd) {
+        String refName = cmd.getRefName();
+
+        if (RefUpdateContextCollector.enabled()) {
+          RefUpdateContextCollector.register(refName, RefUpdateContext.getOpenedContexts());
+        }
         if (TestActionRefUpdateContext.isOpen()
             || RefUpdateContext.hasOpen(OFFLINE_OPERATION)
             || RefUpdateContext.hasOpen(INIT_REPO)
@@ -143,8 +149,6 @@
           return;
         }
 
-        String refName = cmd.getRefName();
-
         Optional<ImmutableList<RefUpdateType>> allowedRefUpdateTypes =
             RefUpdateContextValidator.INSTANCE.getAllowedRefUpdateTypes(refName);
 
@@ -300,6 +304,6 @@
   }
 
   private static String normalize(Project.NameKey name) {
-    return name.get().toLowerCase();
+    return name.get().toLowerCase(Locale.US);
   }
 }
diff --git a/java/com/google/gerrit/testing/IndexVersions.java b/java/com/google/gerrit/testing/IndexVersions.java
index 3810707..2e843fe 100644
--- a/java/com/google/gerrit/testing/IndexVersions.java
+++ b/java/com/google/gerrit/testing/IndexVersions.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.index.SchemaDefinitions;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.NavigableMap;
 import org.eclipse.jgit.lib.Config;
@@ -73,13 +74,14 @@
    *     if any of the specified schema versions doesn't exist
    */
   public static <V> ImmutableList<Integer> get(SchemaDefinitions<V> schemaDef) {
-    String envVar = schemaDef.getName().toUpperCase() + "_INDEX_VERSIONS";
+    String envVar = schemaDef.getName().toUpperCase(Locale.US) + "_INDEX_VERSIONS";
     String value = System.getenv(envVar);
     if (!Strings.isNullOrEmpty(value)) {
       return get(schemaDef, "env variable " + envVar, value);
     }
 
-    String systemProperty = "gerrit.index." + schemaDef.getName().toLowerCase() + ".versions";
+    String systemProperty =
+        "gerrit.index." + schemaDef.getName().toLowerCase(Locale.US) + ".versions";
     value = System.getProperty(systemProperty);
     return get(schemaDef, "system property " + systemProperty, value);
   }
@@ -138,7 +140,10 @@
                 i -> {
                   Config cfg = baseConfig;
                   cfg.setInt(
-                      "index", "lucene", schemaDef.getName().toLowerCase() + "TestVersion", i);
+                      "index",
+                      "lucene",
+                      schemaDef.getName().toLowerCase(Locale.US) + "TestVersion",
+                      i);
                   return cfg;
                 }));
   }
diff --git a/java/com/google/gerrit/testing/RefUpdateContextCollector.java b/java/com/google/gerrit/testing/RefUpdateContextCollector.java
new file mode 100644
index 0000000..88232d2
--- /dev/null
+++ b/java/com/google/gerrit/testing/RefUpdateContextCollector.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testing;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Stores information about each updated ref in tests, together with associated RefUpdateContext(s).
+ *
+ * <p>This is a {@link TestRule}, which clears the stored data after each test.
+ *
+ * <p>Usage:
+ *
+ * <pre>{@code
+ * class ...Test {
+ *  \@Rule
+ *  public RefUpdateContextCollector refContextCollector = new RefUpdateContextCollector();
+ *  ...
+ *  public void test() {
+ *    // some actions
+ *    assertThat(refContextCollector.getContextsByRef("refs/heads/main")).contains(...)
+ *  }
+ *  }
+ * }</pre>
+ */
+public class RefUpdateContextCollector implements TestRule {
+  private static ConcurrentLinkedQueue<Entry<String, ImmutableList<RefUpdateContext>>>
+      touchedRefsWithContexts = null;
+
+  @Override
+  public Statement apply(Statement statement, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        try {
+          touchedRefsWithContexts = new ConcurrentLinkedQueue<>();
+          statement.evaluate();
+        } finally {
+          touchedRefsWithContexts = null;
+        }
+      }
+    };
+  }
+
+  public static boolean enabled() {
+    return touchedRefsWithContexts != null;
+  }
+
+  public static void register(String refName, ImmutableList<RefUpdateContext> openedContexts) {
+    if (touchedRefsWithContexts == null) {
+      return;
+    }
+    touchedRefsWithContexts.add(new SimpleImmutableEntry<>(refName, openedContexts));
+  }
+
+  public ImmutableList<String> getRefsByUpdateType(RefUpdateType refUpdateType) {
+    return touchedRefsWithContexts.stream()
+        .filter(
+            entry ->
+                entry.getValue().stream()
+                    .map(RefUpdateContext::getUpdateType)
+                    .anyMatch(refUpdateType::equals))
+        .map(Entry::getKey)
+        .collect(toImmutableList());
+  }
+
+  public void clear() {
+    touchedRefsWithContexts.clear();
+  }
+}
diff --git a/java/com/google/gerrit/testing/SshMode.java b/java/com/google/gerrit/testing/SshMode.java
index 41633bd..60bd5187 100644
--- a/java/com/google/gerrit/testing/SshMode.java
+++ b/java/com/google/gerrit/testing/SshMode.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Enums;
 import com.google.common.base.Strings;
+import java.util.Locale;
 
 /**
  * Whether to enable/disable tests using SSH by inspecting the global environment.
@@ -43,7 +44,7 @@
     if (Strings.isNullOrEmpty(value)) {
       return YES;
     }
-    value = value.toUpperCase();
+    value = value.toUpperCase(Locale.US);
     SshMode mode = Enums.getIfPresent(SshMode.class, value).orNull();
     if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
       checkArgument(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
index 80431ee..b80ff9b 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.util.List;
+import java.util.Locale;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
@@ -65,7 +66,8 @@
     gApi.changes().id(changeId).abandon();
     ChangeInfo info = get(changeId, MESSAGES);
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-    assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase(Locale.US))
+        .contains("abandoned");
 
     ResourceConflictException thrown =
         assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).abandon());
@@ -82,13 +84,17 @@
 
     ChangeInfo info = get(a.getChangeId(), MESSAGES);
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-    assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned");
-    assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("deadbeef");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase(Locale.US))
+        .contains("abandoned");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase(Locale.US))
+        .contains("deadbeef");
 
     info = get(b.getChangeId(), MESSAGES);
     assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-    assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned");
-    assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("deadbeef");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase(Locale.US))
+        .contains("abandoned");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase(Locale.US))
+        .contains("deadbeef");
   }
 
   @Test
@@ -292,7 +298,8 @@
     gApi.changes().id(changeId).restore();
     ChangeInfo info = get(changeId, MESSAGES);
     assertThat(info.status).isEqualTo(ChangeStatus.NEW);
-    assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("restored");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase(Locale.US))
+        .contains("restored");
 
     ResourceConflictException thrown =
         assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).restore());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index bb8f3f3..a0f0fe6 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -86,6 +86,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1064,6 +1065,7 @@
 
     @Override
     public Optional<String> getChangeMessageAddOn(
+        Instant when,
         IdentifiedUser user,
         ChangeNotes changeNotes,
         PatchSet patchSet,
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 142a45c..9456a31 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -114,6 +114,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Stream;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -463,7 +464,7 @@
   @Test
   public void createDuplicateInternalGroupCaseInsensitiveName() throws Exception {
     String dupGroupName = name("dupGroupA");
-    String dupGroupNameLowerCase = name("dupGroupA").toLowerCase();
+    String dupGroupNameLowerCase = name("dupGroupA").toLowerCase(Locale.US);
     gApi.groups().create(dupGroupName);
     gApi.groups().create(dupGroupNameLowerCase);
     assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupName);
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
index 0cdac5a..8295550 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
@@ -41,7 +41,9 @@
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
 import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.gerrit.testing.RefUpdateContextCollector;
 import com.google.inject.Inject;
 import java.util.List;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -54,12 +56,16 @@
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 public abstract class AbstractSubmitOnPush extends AbstractDaemonTest {
   @Inject private ApprovalsUtil approvalsUtil;
   @Inject private ProjectOperations projectOperations;
 
+  @Rule
+  public RefUpdateContextCollector refUpdateContextCollector = new RefUpdateContextCollector();
+
   @Before
   public void blockAnonymous() throws Exception {
     blockAnonymousRead();
@@ -229,6 +235,25 @@
   }
 
   @Test
+  public void pushAutoclosesChanges_changeMetaInAutoClosesChangesContext() throws Exception {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/master").group(adminGroupUuid()))
+        .update();
+    PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "one.txt", "One");
+    String refPrefix = r.getChange().getId().toRefPrefix();
+    assertThat(refUpdateContextCollector.getRefsByUpdateType(RefUpdateType.DIRECT_PUSH)).isEmpty();
+    assertThat(refUpdateContextCollector.getRefsByUpdateType(RefUpdateType.AUTO_CLOSE_CHANGES))
+        .isEmpty();
+    git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
+    assertThat(refUpdateContextCollector.getRefsByUpdateType(RefUpdateType.DIRECT_PUSH))
+        .containsExactly("refs/heads/master", refPrefix + "meta");
+    assertThat(refUpdateContextCollector.getRefsByUpdateType(RefUpdateType.AUTO_CLOSE_CHANGES))
+        .containsExactly(refPrefix + "meta");
+  }
+
+  @Test
   public void mergeOnPushToBranchWithChangeMergedInOther() throws Exception {
     enableCreateNewChangeForAllNotInTarget();
     String master = "refs/heads/master";
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
index 80cc508..e352e2d 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.git.ObjectIds;
+import java.util.Locale;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
@@ -61,7 +62,8 @@
     PushOneCommit.Result m = push("refs/heads/master", "1", "f", "1");
     PushOneCommit.Result c = push("refs/for/stable", "2", "f", "2");
 
-    assertThat(c.getMessage().toLowerCase()).doesNotContain(implicitMergeOf(m.getCommit()));
+    assertThat(c.getMessage().toLowerCase(Locale.US))
+        .doesNotContain(implicitMergeOf(m.getCommit()));
   }
 
   @Test
@@ -74,7 +76,8 @@
     PushOneCommit.Result m = push("refs/heads/master", "1", "f", "1");
     PushOneCommit.Result c = push("refs/for/master", "2", "f", "2");
 
-    assertThat(c.getMessage().toLowerCase()).doesNotContain(implicitMergeOf(m.getCommit()));
+    assertThat(c.getMessage().toLowerCase(Locale.US))
+        .doesNotContain(implicitMergeOf(m.getCommit()));
   }
 
   private String implicitMergeOf(ObjectId commit) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index 62ef118..f40910a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -52,6 +52,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
 import org.junit.Test;
@@ -179,7 +180,7 @@
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
     requestScopeOperations.resetCurrentApiUser();
-    String emailOtherCase = email.toUpperCase();
+    String emailOtherCase = email.toUpperCase(Locale.US);
     gApi.accounts().self().email(emailOtherCase).setPreferred();
     assertThat(gApi.accounts().self().get().email).isEqualTo(email);
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 39a32af..61164f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -949,7 +949,7 @@
     extIdNotes.insert(extId);
     extIdNotes.commit(md);
     assertThat(getAccountId(extIdNotes, scheme, id)).isEqualTo(accountId.get());
-    assertThat(getExternalId(extIdNotes, scheme, id.toLowerCase()).isPresent()).isFalse();
+    assertThat(getExternalId(extIdNotes, scheme, id.toLowerCase(Locale.US)).isPresent()).isFalse();
   }
 
   private void testCaseInsensitiveExternalIdKey(
@@ -959,7 +959,8 @@
     extIdNotes.insert(extId);
     extIdNotes.commit(md);
     assertThat(getAccountId(extIdNotes, scheme, id)).isEqualTo(accountId.get());
-    assertThat(getAccountId(extIdNotes, scheme, id.toLowerCase())).isEqualTo(accountId.get());
+    assertThat(getAccountId(extIdNotes, scheme, id.toLowerCase(Locale.US)))
+        .isEqualTo(accountId.get());
   }
 
   /**
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index f46cf0c..f4e9457 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.extensions.api.accounts.UsernameInput;
+import java.util.Locale;
 import org.junit.Test;
 
 public class PutUsernameIT extends AbstractDaemonTest {
@@ -46,7 +47,7 @@
   @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
   public void setExistingCaseInsensitive_Conflict() throws Exception {
     UsernameInput in = new UsernameInput();
-    in.username = admin.username().toUpperCase();
+    in.username = admin.username().toUpperCase(Locale.US);
     adminRestSession
         .put("/accounts/" + accountCreator.create().id().get() + "/username", in)
         .assertConflict();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 80bedcd..6dfa82b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.inject.Inject;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.stream.IntStream;
 import org.junit.Before;
@@ -336,7 +337,7 @@
     reviewers = suggestReviewers(changeId, user1.username() + " example");
     assertThat(reviewers).hasSize(1);
 
-    reviewers = suggestReviewers(changeId, user4.email().toLowerCase());
+    reviewers = suggestReviewers(changeId, user4.email().toLowerCase(Locale.US));
     assertThat(reviewers).hasSize(1);
     assertThat(reviewers.get(0).account.email).isEqualTo(user4.email());
   }
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
index 6f3aa15..35ecceb 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.Locale;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -176,7 +177,7 @@
 
     assertThat(resolve(existingUsername)).containsExactly(idWithUsername);
     assertThat(resolve(existingMixedCaseUsername)).containsExactly(idWithMixedCaseUsername);
-    assertThat(resolve(existingMixedCaseUsername.toLowerCase()))
+    assertThat(resolve(existingMixedCaseUsername.toLowerCase(Locale.US)))
         .containsExactly(idWithMixedCaseUsername);
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index cced47f..28c795d 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
 import static com.google.gerrit.entities.NotifyConfig.NotifyType.ABANDONED_CHANGES;
 import static com.google.gerrit.entities.NotifyConfig.NotifyType.ALL_COMMENTS;
 import static com.google.gerrit.entities.NotifyConfig.NotifyType.NEW_CHANGES;
@@ -28,6 +29,7 @@
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.OWNER_REVIEWERS;
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS;
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.ENABLED;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
@@ -306,6 +308,25 @@
     addReviewerToReviewableChange(batch());
   }
 
+  @Test
+  public void addReviewerToChangeNoAnonymousUsersNotified() throws Exception {
+    StagedChange sc = stageReviewableChange();
+    // Remove read permission for anonymous users.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
+    addReviewer(singly(), sc.changeId, sc.owner, reviewer.email());
+
+    // No BY_EMAIL cc's.
+    assertThat(sender).sent("newchange", sc).to(reviewer).cc(sc.reviewer).noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
   private void addReviewerToReviewableChangeByOwnerCcingSelf(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
@@ -672,6 +693,30 @@
   }
 
   @Test
+  public void commentOnChangeNotVisibleToAnonymousByReviewer() throws Exception {
+    StagedChange sc = stageReviewableChange();
+
+    // Remove read permission for anonymous users.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/*").group(ANONYMOUS_USERS))
+        .add(allow(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+        .update();
+
+    review(sc.reviewer, sc.changeId, ENABLED);
+    // Not cc'ed to BY_EMAIL added addresses.
+    assertThat(sender)
+        .sent("comment", sc)
+        .to(sc.owner)
+        .cc(sc.ccer)
+        .bcc(sc.starrer)
+        .bcc(ALL_COMMENTS)
+        .noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
   public void commentOnReviewableChangeByOwnerCcingSelf() throws Exception {
     StagedChange sc = stageReviewableChange();
     review(sc.owner, sc.changeId, CC_ON_OWN_COMMENTS);
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
index cc61dfb..3145234 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
@@ -26,6 +26,7 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.lang.reflect.Field;
+import java.util.Locale;
 import org.junit.After;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -83,12 +84,13 @@
           continue;
         }
         if (tld.startsWith(UNSUPPORTED_PREFIX)) {
-          String test = "test@example." + tld.toLowerCase().substring(UNSUPPORTED_PREFIX.length());
+          String test =
+              "test@example." + tld.toLowerCase(Locale.US).substring(UNSUPPORTED_PREFIX.length());
           assertWithMessage("expected invalid TLD \"" + test + "\"")
               .that(validator.isValid(test))
               .isFalse();
         } else {
-          String test = "test@example." + tld.toLowerCase();
+          String test = "test@example." + tld.toLowerCase(Locale.US);
           assertWithMessage("failed to validate TLD \"" + test + "\"")
               .that(validator.isValid(test))
               .isTrue();
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
index 3727d38..a01807a 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -33,6 +33,7 @@
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 import java.util.TreeSet;
 import org.bouncycastle.openpgp.PGPPublicKey;
@@ -80,7 +81,7 @@
     PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     String objId = keyObjectId(key.getKeyID()).name();
     assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
-    assertEquals(keyIdToString(key.getKeyID()).toLowerCase(), objId.substring(8, 16));
+    assertEquals(keyIdToString(key.getKeyID()).toLowerCase(Locale.US), objId.substring(8, 16));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java b/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
index 36641fe..8a2de7d 100644
--- a/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/ResourceServletTest.java
@@ -40,6 +40,7 @@
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneOffset;
+import java.util.Locale;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.zip.GZIPInputStream;
 import org.junit.Before;
@@ -331,7 +332,7 @@
   }
 
   private static void assertCacheable(FakeHttpServletResponse res, boolean revalidate) {
-    String header = res.getHeader("Cache-Control").toLowerCase();
+    String header = res.getHeader("Cache-Control").toLowerCase(Locale.US);
     assertThat(header).contains("public");
     if (revalidate) {
       assertThat(header).contains("must-revalidate");
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index 30ae4aa..ce045f7 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -37,6 +37,7 @@
 import com.google.inject.Injector;
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
@@ -114,14 +115,14 @@
   @Test
   public void emailsExistence() {
     assertThat(identifiedUser.hasEmailAddress(TEST_CASES[0])).isTrue();
-    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toLowerCase())).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toLowerCase(Locale.US))).isTrue();
     assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
-    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toUpperCase())).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toUpperCase(Locale.US))).isTrue();
     /* assert again to test cached email address by IdentifiedUser.validEmails */
     assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
 
     assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2])).isTrue();
-    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2].toLowerCase())).isTrue();
+    assertThat(identifiedUser.hasEmailAddress(TEST_CASES[2].toLowerCase(Locale.US))).isTrue();
 
     assertThat(identifiedUser.hasEmailAddress("non-exist@email.com")).isFalse();
     /* assert again to test cached email address by IdentifiedUser.invalidEmails */
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java
index b9c1897..055b95d 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java
@@ -25,7 +25,7 @@
     try (PerThreadCache cache = PerThreadCache.create()) {
       PerThreadCache.Key<Project.NameKey> key =
           PerThreadCache.Key.create(Project.NameKey.class, Project.nameKey("test-project"));
-      String unused = PerThreadProjectCache.getOrCompute(key, () -> "cached");
+      PerThreadProjectCache.getOrCompute(key, () -> "cached");
       String value = PerThreadProjectCache.getOrCompute(key, () -> "directly served");
       assertThat(value).isEqualTo("cached");
     }
@@ -38,12 +38,12 @@
       for (int i = 0; i < 50; i++) {
         PerThreadCache.Key<Project.NameKey> key =
             PerThreadCache.Key.create(Project.NameKey.class, Project.nameKey("test-project" + i));
-        String unused = PerThreadProjectCache.getOrCompute(key, () -> "cached");
+        PerThreadProjectCache.getOrCompute(key, () -> "cached");
       }
       // Assert that the value was not persisted
       PerThreadCache.Key<Project.NameKey> key =
           PerThreadCache.Key.create(Project.NameKey.class, "Project" + 1000);
-      String unused = PerThreadProjectCache.getOrCompute(key, () -> "new value");
+      PerThreadProjectCache.getOrCompute(key, () -> "new value");
       String value = PerThreadProjectCache.getOrCompute(key, () -> "directly served");
       assertThat(value).isEqualTo("directly served");
     }
diff --git a/javatests/com/google/gerrit/server/index/IndexedFieldTest.java b/javatests/com/google/gerrit/server/index/IndexedFieldTest.java
index dacc37d..5029334 100644
--- a/javatests/com/google/gerrit/server/index/IndexedFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/IndexedFieldTest.java
@@ -61,7 +61,6 @@
 import org.junit.runner.RunWith;
 
 /** Tests for {@link com.google.gerrit.index.IndexedField} */
-@SuppressWarnings("serial")
 @RunWith(Theories.class)
 public class IndexedFieldTest {
 
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 1fede32..aea4b95 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -94,6 +94,7 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
@@ -242,7 +243,7 @@
 
     assertQuery(user5.email, user5);
     assertQuery("email:" + user5.email, user5);
-    assertQuery("email:" + user5.email.toUpperCase(), user5);
+    assertQuery("email:" + user5.email.toUpperCase(Locale.US), user5);
   }
 
   @Test
@@ -289,7 +290,7 @@
 
     assertQuery(user1.username, user1);
     assertQuery("username:" + user1.username, user1);
-    assertQuery("username:" + user1.username.toUpperCase(), user1);
+    assertQuery("username:" + user1.username.toUpperCase(Locale.US), user1);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
index f39b875..18714ac 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
@@ -171,7 +171,7 @@
 
   @Override
   public void addHeader(String name, String value) {
-    headers.put(name.toLowerCase(), value);
+    headers.put(name.toLowerCase(Locale.US), value);
   }
 
   @Override
@@ -181,7 +181,7 @@
 
   @Override
   public boolean containsHeader(String name) {
-    return headers.containsKey(name.toLowerCase());
+    return headers.containsKey(name.toLowerCase(Locale.US));
   }
 
   @Override
@@ -232,13 +232,13 @@
 
   @Override
   public void setHeader(String name, String value) {
-    headers.removeAll(name.toLowerCase());
+    headers.removeAll(name.toLowerCase(Locale.US));
     addHeader(name, value);
   }
 
   @Override
   public void setIntHeader(String name, int value) {
-    headers.removeAll(name.toLowerCase());
+    headers.removeAll(name.toLowerCase(Locale.US));
     addIntHeader(name, value);
   }
 
@@ -262,7 +262,7 @@
 
   @Override
   public String getHeader(String name) {
-    return Iterables.getFirst(headers.get(requireNonNull(name.toLowerCase())), null);
+    return Iterables.getFirst(headers.get(requireNonNull(name.toLowerCase(Locale.US))), null);
   }
 
   @Override
@@ -272,7 +272,7 @@
 
   @Override
   public Collection<String> getHeaders(String name) {
-    return headers.get(requireNonNull(name.toLowerCase()));
+    return headers.get(requireNonNull(name.toLowerCase(Locale.US)));
   }
 
   public byte[] getActualBody() {
diff --git a/modules/jgit b/modules/jgit
index 176f17d..0687c73 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 176f17d05ec154ce455ab2bde7429017d43d67fb
+Subproject commit 0687c73a12b5157c350de430f62ea8243d813e19
diff --git a/plugins/package.json b/plugins/package.json
index 612062b..2299a93 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -32,7 +32,7 @@
     "@codemirror/theme-one-dark": "^6.1.1",
     "@codemirror/view": "^6.9.1",
     "lit": "^2.2.3",
-    "rxjs": "^6.6.7",
+    "rxjs": "^7.8.0",
     "sinon": "^13.0.0"
   },
   "license": "Apache-2.0",
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 10db2cf..158435d 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 10db2cf772989d031c6f3558010c51fe07cf9722
+Subproject commit 158435d2aa9f8729e4e78835969e54701af9203b
diff --git a/plugins/webhooks b/plugins/webhooks
index 16110f3..1dc0a71 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 16110f320dd5b6a40af87eaba4bf3af60cb0efd1
+Subproject commit 1dc0a718839f8872a59c189da7243ee77a4fe782
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 3156f4c..bad5fcc 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -2480,12 +2480,12 @@
   dependencies:
     queue-microtask "^1.2.2"
 
-rxjs@^6.6.7:
-  version "6.6.7"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
-  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+rxjs@^7.8.0:
+  version "7.8.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
+  integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
   dependencies:
-    tslib "^1.9.0"
+    tslib "^2.1.0"
 
 safe-buffer@5.2.1, safe-buffer@~5.2.0:
   version "5.2.1"
@@ -2681,10 +2681,10 @@
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
   integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
 
-tslib@^1.9.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
-  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+tslib@^2.1.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
+  integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
 
 tsscmp@1.0.6:
   version "1.0.6"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
index 7a4f97c..4b7f0a3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-action-bar/gr-change-list-action-bar_test.ts
@@ -19,7 +19,7 @@
 } from '../../../test/test-utils';
 import {ChangeInfo, NumericChangeId} from '../../../types/common';
 import './gr-change-list-action-bar';
-import type {GrChangeListActionBar} from './gr-change-list-action-bar';
+import {GrChangeListActionBar} from './gr-change-list-action-bar';
 
 const change1 = {...createChange(), _number: 1 as NumericChangeId, actions: {}};
 const change2 = {...createChange(), _number: 2 as NumericChangeId, actions: {}};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
index 0cc3e18..af997a9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
@@ -32,7 +32,7 @@
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import './gr-change-list-hashtag-flow';
-import type {GrChangeListHashtagFlow} from './gr-change-list-hashtag-flow';
+import {GrChangeListHashtagFlow} from './gr-change-list-hashtag-flow';
 
 suite('gr-change-list-hashtag-flow tests', () => {
   let element: GrChangeListHashtagFlow;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index 85e6212..d2f5fa2 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -41,7 +41,7 @@
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import './gr-change-list-reviewer-flow';
-import type {GrChangeListReviewerFlow} from './gr-change-list-reviewer-flow';
+import {GrChangeListReviewerFlow} from './gr-change-list-reviewer-flow';
 
 const accounts: AccountInfo[] = [
   createAccountWithIdNameAndEmail(0),
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
index d2dced2..489d8ee 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-topic-flow/gr-change-list-topic-flow_test.ts
@@ -32,7 +32,7 @@
 import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import './gr-change-list-topic-flow';
-import type {GrChangeListTopicFlow} from './gr-change-list-topic-flow';
+import {GrChangeListTopicFlow} from './gr-change-list-topic-flow';
 
 suite('gr-change-list-topic-flow tests', () => {
   let element: GrChangeListTopicFlow;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 38acf80..7c85bb6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -89,7 +89,6 @@
   fireError,
   fire,
   fireNoBubble,
-  fireNoBubbleNoCompose,
   fireIronAnnounce,
   fireReload,
   fireServerError,
@@ -121,7 +120,7 @@
 import {customElement, property, state, query} from 'lit/decorators.js';
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
-import {hasHumanReviewer, isOwner} from '../../../utils/change-util';
+import {hasHumanReviewer} from '../../../utils/change-util';
 import {commentsModelToken} from '../../../models/comments/comments-model';
 import {
   CommentEditingChangedDetail,
@@ -301,9 +300,6 @@
   reviewerPendingConfirmation: SuggestedReviewerGroupInfo | null = null;
 
   @state()
-  sendButtonLabel?: string;
-
-  @state()
   savingComments = false;
 
   @state()
@@ -336,14 +332,14 @@
   newAttentionSet: Set<UserId> = new Set();
 
   @state()
-  sendDisabled?: boolean;
-
-  @state()
   patchsetLevelDraftIsResolved = true;
 
   @state()
   patchsetLevelComment?: UnsavedInfo | DraftInfo;
 
+  @state()
+  isOwner = false;
+
   private readonly restApiService: RestApiService =
     getAppContext().restApiService;
 
@@ -619,6 +615,11 @@
     );
     subscribe(
       this,
+      () => this.getChangeModel().isOwner$,
+      x => (this.isOwner = x)
+    );
+    subscribe(
+      this,
       () => this.getCommentsModel().mentionedUsersInDrafts$,
       x => {
         this.mentionedUsers = x;
@@ -712,10 +713,6 @@
     }
     if (changedProperties.has('canBeStarted')) {
       this.computeMessagePlaceholder();
-      this.computeSendButtonLabel();
-    }
-    if (changedProperties.has('sendDisabled')) {
-      this.sendDisabledChanged();
     }
     if (changedProperties.has('attentionExpanded')) {
       this.onAttentionExpandedChange();
@@ -743,7 +740,6 @@
 
   override render() {
     if (!this.change) return;
-    this.sendDisabled = this.computeSendButtonDisabled();
     return html`
       <div tabindex="-1">
         <section class="peopleContainer">
@@ -1015,7 +1011,7 @@
               <gr-button
                 class="edit-attention-button"
                 @click=${this.handleAttentionModify}
-                ?disabled=${this.sendDisabled}
+                ?disabled=${this.isSendDisabled()}
                 link
                 position-below
                 data-label="Edit"
@@ -1214,10 +1210,12 @@
             <gr-button
               id="sendButton"
               primary
-              ?disabled=${this.sendDisabled}
+              ?disabled=${this.isSendDisabled()}
               class="action send"
-              @click=${this.sendTapHandler}
-              >${this.sendButtonLabel}
+              @click=${this.sendClickHandler}
+              >${this.canBeStarted
+                ? ButtonLabels.SEND + ' and ' + ButtonLabels.START_REVIEW
+                : ButtonLabels.SEND}
             </gr-button>
           </gr-tooltip-content>
         </div>
@@ -1352,6 +1350,7 @@
     );
   }
 
+  // visible for testing
   async send(includeComments: boolean, startReview: boolean) {
     this.reporting.time(Timing.SEND_REPLY);
     const labels = this.getLabelScores().getLabelValues();
@@ -1473,16 +1472,11 @@
   }
 
   chooseFocusTarget() {
-    if (!isOwner(this.change, this.account)) return FocusTarget.BODY;
+    if (!this.isOwner) return FocusTarget.BODY;
     if (hasHumanReviewer(this.change)) return FocusTarget.BODY;
     return FocusTarget.REVIEWERS;
   }
 
-  isOwner(account?: AccountInfo, change?: ParsedChangeInfo | ChangeInfo) {
-    if (!account || !change || !change.owner) return false;
-    return account._account_id === change.owner._account_id;
-  }
-
   handle400Error(r?: Response | null) {
     if (!r) throw new Error('Response is empty.');
     let response: Response = r;
@@ -1557,8 +1551,8 @@
     fire(this, 'iron-resize', {});
   }
 
-  computeAttentionButtonTitle(sendDisabled?: boolean) {
-    return sendDisabled
+  computeAttentionButtonTitle() {
+    return this.isSendDisabled()
       ? 'Modify the attention set by adding a comment or use the account ' +
           'hovercard in the change page.'
       : 'Edit attention set changes';
@@ -1606,7 +1600,6 @@
       ? this.draftCommentThreads
       : [];
     const hasVote = !!this.labelsChanged;
-    const isOwner = this.isOwner(this.account, this.change);
     const isUploader = this.uploader?._account_id === this.account._account_id;
 
     this.attentionCcsCount = removeServiceUsers(this.ccs).length;
@@ -1640,7 +1633,7 @@
         .filter(
           r =>
             isAccountNewlyAdded(r, ReviewerState.REVIEWER, this.change) ||
-            (this.canBeStarted && isOwner)
+            (this.canBeStarted && this.isOwner)
         )
         .filter(notIsReviewerAndHasDraftOrLabel)
         .forEach(r => newAttention.add((r as AccountInfo)._account_id!));
@@ -1649,7 +1642,7 @@
         if (this.uploader?._account_id && !isUploader) {
           newAttention.add(this.uploader._account_id);
         }
-        if (this.change.owner?._account_id && !isOwner) {
+        if (this.change.owner?._account_id && !this.isOwner) {
           newAttention.add(this.change.owner._account_id);
         }
       }
@@ -1677,18 +1670,11 @@
   }
 
   computeShowAttentionTip() {
-    if (
-      !this.account ||
-      !this.change?.owner ||
-      !this.currentAttentionSet ||
-      !this.newAttentionSet
-    )
-      return false;
-    const isOwner = this.account._account_id === this.change.owner._account_id;
+    if (!this.currentAttentionSet || !this.newAttentionSet) return false;
     const addedIds = [...this.newAttentionSet].filter(
       id => !this.currentAttentionSet.has(id)
     );
-    return isOwner && addedIds.length > 2;
+    return this.isOwner && addedIds.length > 2;
   }
 
   computeCommentAccounts(threads: CommentThread[]) {
@@ -1710,13 +1696,15 @@
   }
 
   computeShowNoAttentionUpdate() {
-    return this.sendDisabled || this.computeNewAttentionAccounts().length === 0;
+    return (
+      this.isSendDisabled() || this.computeNewAttentionAccounts().length === 0
+    );
   }
 
   computeDoNotUpdateMessage() {
     if (!this.currentAttentionSet || !this.newAttentionSet) return '';
     if (
-      this.sendDisabled ||
+      this.isSendDisabled() ||
       areSetsEqual(this.currentAttentionSet, this.newAttentionSet)
     ) {
       return 'No changes to the attention set.';
@@ -1828,32 +1816,30 @@
     this.rebuildReviewerArrays();
   }
 
-  saveClickHandler(e: Event) {
+  private saveClickHandler(e: Event) {
     e.preventDefault();
-    if (!this.ccsList?.submitEntryText()) {
-      // Do not proceed with the save if there is an invalid email entry in
-      // the text field of the CC entry.
-      return;
+    this.submit(false);
+  }
+
+  private sendClickHandler(e: Event) {
+    e.preventDefault();
+    this.submit(this.canBeStarted);
+  }
+
+  private submit(startReview?: boolean) {
+    if (startReview === undefined) {
+      startReview = this.isOwner && this.canBeStarted;
     }
-    this.send(this.includeComments, false);
-  }
-
-  sendTapHandler(e: Event) {
-    e.preventDefault();
-    this.submit();
-  }
-
-  submit() {
     if (!this.ccsList?.submitEntryText()) {
       // Do not proceed with the send if there is an invalid email entry in
       // the text field of the CC entry.
       return;
     }
-    if (this.sendDisabled) {
+    if (this.isSendDisabled()) {
       fireAlert(this, EMPTY_REPLY_MESSAGE);
       return;
     }
-    return this.send(this.includeComments, this.canBeStarted).catch(err => {
+    return this.send(this.includeComments, startReview).catch(err => {
       fireError(this, `Error submitting review ${err}`);
     });
   }
@@ -1965,12 +1951,6 @@
     this.cancel();
   }
 
-  computeSendButtonLabel() {
-    this.sendButtonLabel = this.canBeStarted
-      ? ButtonLabels.SEND + ' and ' + ButtonLabels.START_REVIEW
-      : ButtonLabels.SEND;
-  }
-
   computeSendButtonTooltip(canBeStarted?: boolean, commentEditing?: boolean) {
     if (commentEditing) {
       return ButtonTooltips.DISABLED_COMMENT_EDITING;
@@ -1982,7 +1962,8 @@
     return savingComments ? 'saving' : '';
   }
 
-  computeSendButtonDisabled() {
+  // visible for testing
+  isSendDisabled() {
     if (
       this.canBeStarted === undefined ||
       this.patchsetLevelDraftMessage === undefined ||
@@ -2030,10 +2011,6 @@
     this.pluginMessage = message;
   }
 
-  sendDisabledChanged() {
-    fireNoBubbleNoCompose(this, 'send-disabled-changed', {});
-  }
-
   getReviewerSuggestionsProvider(change?: ChangeInfo | ParsedChangeInfo) {
     if (!change) return;
     const provider = new GrReviewerSuggestionsProvider(
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 465f64d..126343e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -16,7 +16,11 @@
   stubRestApi,
   waitUntilVisible,
 } from '../../../test/test-utils';
-import {ChangeStatus, ReviewerState} from '../../../constants/constants';
+import {
+  ChangeStatus,
+  DraftsAction,
+  ReviewerState,
+} from '../../../constants/constants';
 import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {StandardLabels} from '../../../utils/label-util';
 import {
@@ -65,6 +69,7 @@
   CommentsModel,
   commentsModelToken,
 } from '../../../models/comments/comments-model';
+import {isOwner} from '../../../utils/change-util';
 
 function cloneableResponse(status: number, text: string) {
   return {
@@ -177,9 +182,9 @@
     );
   }
 
-  function interceptSaveReview() {
+  function interceptSaveReview(): Promise<ReviewInput> {
     let resolver: (review: ReviewInput) => void;
-    const promise = new Promise(resolve => {
+    const promise = new Promise<ReviewInput>(resolve => {
       resolver = resolve;
     });
     stubSaveReview((review: ReviewInput) => {
@@ -257,7 +262,7 @@
                     <span> No changes to the attention set. </span>
                     <gr-tooltip-content
                       has-tooltip=""
-                      title="Edit attention set changes"
+                      title="Modify the attention set by adding a comment or use the account hovercard in the change page."
                     >
                       <gr-button
                         aria-disabled="true"
@@ -459,14 +464,17 @@
 
     const review = await saveReviewPromise;
     assert.deepEqual(review, {
-      drafts: 'PUBLISH_ALL_REVISIONS',
+      drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
       labels: {
         'Code-Review': 0,
         Verified: 0,
       },
       reviewers: [],
       add_to_attention_set: [
-        {reason: '<GERRIT_ACCOUNT_1> replied on the change', user: 999},
+        {
+          reason: '<GERRIT_ACCOUNT_1> replied on the change',
+          user: 999 as UserId,
+        },
       ],
       remove_from_attention_set: [],
       ignore_automatic_attention_set_rules: true,
@@ -492,13 +500,16 @@
     const review = await saveReviewPromise;
 
     assert.deepEqual(review, {
-      drafts: 'PUBLISH_ALL_REVISIONS',
+      drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
       labels: {
         'Code-Review': 0,
         Verified: 0,
       },
       add_to_attention_set: [
-        {reason: '<GERRIT_ACCOUNT_123> replied on the change', user: 314},
+        {
+          reason: '<GERRIT_ACCOUNT_123> replied on the change',
+          user: 314 as UserId,
+        },
       ],
       reviewers: [],
       ready: true,
@@ -523,14 +534,17 @@
     const review = await saveReviewPromise;
 
     assert.deepEqual(review, {
-      drafts: 'PUBLISH_ALL_REVISIONS',
+      drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
       labels: {
         'Code-Review': 0,
         Verified: 0,
       },
       add_to_attention_set: [
         // Name coming from createUserConfig in test-data-generator
-        {reason: 'Name of user not set replied on the change', user: 314},
+        {
+          reason: 'Name of user not set replied on the change',
+          user: 314 as UserId,
+        },
       ],
       reviewers: [],
       ready: true,
@@ -598,6 +612,7 @@
     element._ccs = [];
     element.draftCommentThreads = draftThreads;
     element.includeComments = includeComments;
+    element.isOwner = isOwner(change, element.account);
 
     await element.updateComplete;
 
@@ -1067,6 +1082,7 @@
     // If the change is "work in progress" and the owner sends a reply, then
     // add all reviewers.
     element.canBeStarted = true;
+    element.isOwner = isOwner(element.change, element.account);
     element.computeNewAttention();
     await element.updateComplete;
     assert.sameMembers(
@@ -1076,6 +1092,7 @@
 
     // ... but not when someone else replies.
     element.account = {_account_id: 4 as AccountId};
+    element.isOwner = isOwner(element.change, element.account);
     element.computeNewAttention();
     assert.sameMembers([...element.newAttentionSet], []);
   });
@@ -1195,14 +1212,17 @@
     await waitUntil(() => element.disabled === false);
     assert.equal(element.patchsetLevelDraftMessage.length, 0);
     assert.deepEqual(review, {
-      drafts: 'PUBLISH_ALL_REVISIONS',
+      drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
       labels: {
         'Code-Review': -1,
         Verified: -1,
       },
       reviewers: [],
       add_to_attention_set: [
-        {user: 999, reason: '<GERRIT_ACCOUNT_1> replied on the change'},
+        {
+          user: 999 as UserId,
+          reason: '<GERRIT_ACCOUNT_1> replied on the change',
+        },
       ],
       remove_from_attention_set: [],
       ignore_automatic_attention_set_rules: true,
@@ -1231,14 +1251,17 @@
     const review = await saveReviewPromise;
     await element.updateComplete;
     assert.deepEqual(review, {
-      drafts: 'KEEP',
+      drafts: DraftsAction.KEEP,
       labels: {
         'Code-Review': 0,
         Verified: 0,
       },
       reviewers: [],
       add_to_attention_set: [
-        {reason: '<GERRIT_ACCOUNT_1> replied on the change', user: 999},
+        {
+          reason: '<GERRIT_ACCOUNT_1> replied on the change',
+          user: 999 as UserId,
+        },
       ],
       remove_from_attention_set: [],
       ignore_automatic_attention_set_rules: true,
@@ -1636,10 +1659,10 @@
   });
 
   test('chooseFocusTarget', () => {
-    element.account = undefined;
+    element.isOwner = false;
     assert.equal(element.chooseFocusTarget(), element.FocusTarget.BODY);
 
-    element.account = element.change!.owner;
+    element.isOwner = true;
     assert.equal(element.chooseFocusTarget(), element.FocusTarget.REVIEWERS);
 
     element.change!.reviewers.REVIEWER = [createAccountWithId(314)];
@@ -2087,16 +2110,26 @@
     pressKey(element, Key.ENTER);
   });
 
-  test('emit send on ctrl+enter key', async () => {
-    // required so that "Send" button is enabled
+  test('send and start review on ctrl+enter for owner', async () => {
     element.canBeStarted = true;
+    element.isOwner = true;
     await element.updateComplete;
 
-    stubSaveReview(() => undefined);
-    const promise = mockPromise();
-    element.addEventListener('send', () => promise.resolve());
+    const savePromise = interceptSaveReview();
     pressKey(element, Key.ENTER, Modifier.CTRL_KEY);
-    await promise;
+    const reviewInput = await savePromise;
+    assert.isTrue(reviewInput.ready);
+  });
+
+  test('save on ctrl+enter for reviewer', async () => {
+    element.canBeStarted = true;
+    element.isOwner = false;
+    await element.updateComplete;
+
+    const savePromise = interceptSaveReview();
+    pressKey(element, Key.ENTER, Modifier.CTRL_KEY);
+    const reviewInput = await savePromise;
+    assert.isUndefined(reviewInput.ready);
   });
 
   test('computeMessagePlaceholder', async () => {
@@ -2112,14 +2145,14 @@
     );
   });
 
-  test('computeSendButtonLabel', async () => {
+  test('sendButton text', async () => {
     element.canBeStarted = false;
     await element.updateComplete;
-    assert.equal(element.sendButtonLabel, 'Send');
+    assert.equal(element.sendButton?.innerText, 'SEND');
 
     element.canBeStarted = true;
     await element.updateComplete;
-    assert.equal(element.sendButtonLabel, 'Send and Start review');
+    assert.equal(element.sendButton?.innerText, 'SEND AND START REVIEW');
   });
 
   test('handle400Error reviewers and CCs', async () => {
@@ -2239,7 +2272,7 @@
     });
   });
 
-  test('computeSendButtonDisabled_canBeStarted', () => {
+  test('isSendDisabled_canBeStarted', () => {
     // Mock canBeStarted
     element.canBeStarted = true;
     element.draftCommentThreads = [];
@@ -2250,10 +2283,10 @@
     element.disabled = false;
     element.commentEditing = false;
     element.account = makeAccount();
-    assert.isFalse(element.computeSendButtonDisabled());
+    assert.isFalse(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_allFalse', () => {
+  test('isSendDisabled_allFalse', () => {
     // Mock everything false
     element.canBeStarted = false;
     element.draftCommentThreads = [];
@@ -2264,10 +2297,10 @@
     element.disabled = false;
     element.commentEditing = false;
     element.account = makeAccount();
-    assert.isTrue(element.computeSendButtonDisabled());
+    assert.isTrue(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_draftCommentsSend', () => {
+  test('isSendDisabled_draftCommentsSend', () => {
     // Mock nonempty comment draft array; with sending comments.
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2278,10 +2311,10 @@
     element.disabled = false;
     element.commentEditing = false;
     element.account = makeAccount();
-    assert.isFalse(element.computeSendButtonDisabled());
+    assert.isFalse(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_draftCommentsDoNotSend', () => {
+  test('isSendDisabled_draftCommentsDoNotSend', () => {
     // Mock nonempty comment draft array; without sending comments.
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2293,10 +2326,10 @@
     element.commentEditing = false;
     element.account = makeAccount();
 
-    assert.isTrue(element.computeSendButtonDisabled());
+    assert.isTrue(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_changeMessage', () => {
+  test('isSendDisabled_changeMessage', () => {
     // Mock nonempty change message.
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2308,10 +2341,10 @@
     element.commentEditing = false;
     element.account = makeAccount();
 
-    assert.isFalse(element.computeSendButtonDisabled());
+    assert.isFalse(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabledreviewersChanged', () => {
+  test('isSendDisabledreviewersChanged', () => {
     // Mock reviewers mutated.
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2323,10 +2356,10 @@
     element.commentEditing = false;
     element.account = makeAccount();
 
-    assert.isFalse(element.computeSendButtonDisabled());
+    assert.isFalse(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_labelsChanged', () => {
+  test('isSendDisabled_labelsChanged', () => {
     // Mock labels changed.
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2338,10 +2371,10 @@
     element.commentEditing = false;
     element.account = makeAccount();
 
-    assert.isFalse(element.computeSendButtonDisabled());
+    assert.isFalse(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_dialogDisabled', () => {
+  test('isSendDisabled_dialogDisabled', () => {
     // Whole dialog is disabled.
     element.canBeStarted = false;
     element.draftCommentThreads = [{...createCommentThread([createComment()])}];
@@ -2353,10 +2386,10 @@
     element.commentEditing = false;
     element.account = makeAccount();
 
-    assert.isTrue(element.computeSendButtonDisabled());
+    assert.isTrue(element.isSendDisabled());
   });
 
-  test('computeSendButtonDisabled_existingVote', async () => {
+  test('isSendDisabled_existingVote', async () => {
     const account = createAccountWithId();
     (
       element.change!.labels![StandardLabels.CODE_REVIEW]! as DetailedLabelInfo
@@ -2372,7 +2405,7 @@
     element.account = account;
 
     // User has already voted.
-    assert.isFalse(element.computeSendButtonDisabled());
+    assert.isFalse(element.isSendDisabled());
   });
 
   test('_submit blocked when no mutations exist', async () => {
@@ -2428,19 +2461,19 @@
     });
 
     test('send button updates state as text is typed in patchset comment', async () => {
-      assert.isTrue(element.computeSendButtonDisabled());
+      assert.isTrue(element.isSendDisabled());
 
       queryAndAssert<GrComment>(element, '#patchsetLevelComment').messageText =
         'hello';
       await waitUntil(() => element.patchsetLevelDraftMessage === 'hello');
 
-      assert.isFalse(element.computeSendButtonDisabled());
+      assert.isFalse(element.isSendDisabled());
 
       queryAndAssert<GrComment>(element, '#patchsetLevelComment').messageText =
         '';
       await waitUntil(() => element.patchsetLevelDraftMessage === '');
 
-      assert.isTrue(element.computeSendButtonDisabled());
+      assert.isTrue(element.isSendDisabled());
     });
 
     test('sending patchset level comment', async () => {
@@ -2468,14 +2501,17 @@
       assert.deepEqual(autoSaveStub.callCount, 1);
 
       assert.deepEqual(review, {
-        drafts: 'PUBLISH_ALL_REVISIONS',
+        drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
         labels: {
           'Code-Review': 0,
           Verified: 0,
         },
         reviewers: [],
         add_to_attention_set: [
-          {reason: '<GERRIT_ACCOUNT_1> replied on the change', user: 999},
+          {
+            reason: '<GERRIT_ACCOUNT_1> replied on the change',
+            user: 999 as UserId,
+          },
         ],
         remove_from_attention_set: [],
         ignore_automatic_attention_set_rules: true,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index de6a39c..c38e788 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -237,7 +237,7 @@
    * This is triggered when the user types into the editing textarea. We then
    * debounce it and call autoSave().
    */
-  private autoSaveTrigger$ = new Subject();
+  private autoSaveTrigger$ = new Subject<void>();
 
   /**
    * Set to the content of DraftInfo when entering editing mode.
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index c8574cd..b82c023 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -120,6 +120,7 @@
         .inputContainer {
           background-color: var(--dialog-background-color);
           padding: var(--spacing-m);
+          white-space: nowrap;
         }
         /* This makes inputContainer on one line. */
         .inputContainer gr-autocomplete,
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index da4e3f1..35a8465 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -139,6 +139,9 @@
     // emits at 'trailing' of throttle interval
     assert.equal(fetchSpy.callCount, 1);
 
+    // 600 ms is greater than the 500 ms throttle time.
+    clock.tick(600);
+
     model.reload('test-plugin');
     model.reload('test-plugin');
     model.reload('test-plugin');
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index edf0206..32aff91 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -42,7 +42,7 @@
     "polymer-bridges": "file:../../polymer-bridges/",
     "polymer-resin": "^2.0.1",
     "resemblejs": "^4.0.0",
-    "rxjs": "^6.6.7",
+    "rxjs": "^7.8.0",
     "safevalues": "^0.3.1",
     "web-vitals": "^3.0.0"
   },
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index e056a35..c021046 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -846,12 +846,12 @@
   dependencies:
     glob "^7.1.3"
 
-rxjs@^6.6.7:
-  version "6.6.7"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
-  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+rxjs@^7.8.0:
+  version "7.8.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
+  integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
   dependencies:
-    tslib "^1.9.0"
+    tslib "^2.1.0"
 
 safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
@@ -954,10 +954,10 @@
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
   integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
 
-tslib@^1.9.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
-  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+tslib@^2.1.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
+  integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
 
 util-deprecate@~1.0.1:
   version "1.0.2"