Merge "Log caller when submit rules are evaluated and tracing is enabled"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 9e73f6f..76e1f82 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5340,13 +5340,27 @@
 [[tracing.traceid.requestUriPattern]]tracing.<trace-id>.requestUriPattern::
 +
 Regular expression to match request URIs for which request tracing
-should be always enabled. Request URIs are only available for REST
-requests. Request URIs never include the '/a' prefix.
+should be enabled except if they match
+link:tracing.traceid.excludedRequestUriPattern[excludedRequestUriPattern].
+Request URIs are only available for REST requests. Request URIs never include
+the '/a' prefix.
 +
 May be specified multiple times.
 +
 By default, unset (all request URIs are matched).
 
+[[tracing.traceid.excludedRequestUriPattern]]tracing.<trace-id>.excludedRequestUriPattern::
++
+Regular expression to match request URIs for which request tracing
+should not be enabled even if they match
+link:#tracing.traceid.requestUriPattern[requestUriPattern].
+Request URIs are only available for REST requests. Request URIs never include
+the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (no request URIs are excluded).
+
 [[tracing.traceid.account]]tracing.<trace-id>.account::
 +
 Account ID of an account for which request tracing should be always
@@ -5413,7 +5427,9 @@
 
 [[deadline.id.requestUriPattern]]deadline.<id>.requestUriPattern::
 +
-Regular expression to match request URIs to which the deadline applies. Request
+Regular expression to match request URIs to which the deadline applies except if
+they match
+link:#deadline.id.excludedRequestUriPattern[excludedRequestUriPattern]. Request
 URIs are only available for REST requests. Request URIs never include the '/a'
 prefix.
 +
@@ -5421,6 +5437,17 @@
 +
 By default, unset (all request URIs are matched).
 
+[[deadline.id.excludedRequestUriPattern]]deadline.<id>.excludedRequestUriPattern::
++
+Regular expression to match request URIs to which the deadline should not be
+applied even if they match
+link:#deadline.id.requestUriPattern[requestUriPattern]. Request URIs are only
+available for REST requests. Request URIs never include the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (no request URIs are excluded).
+
 [[deadline.id.account]]deadline.<id>.account::
 +
 Account ID of an account to which the deadline applies.
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index a613c7e..ae0c0a6 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1343,6 +1343,7 @@
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "disable_keyboard_shortcuts": true,
+    "disable_token_highlighting": true,
     "diff_view": "SIDE_BY_SIDE",
     "mute_common_path_prefixes": true,
     "my": [
@@ -1394,6 +1395,7 @@
     "diff_view": "SIDE_BY_SIDE",
     "publish_comments_on_push": true,
     "disable_keyboard_shortcuts": true,
+    "disable_token_highlighting": true,
     "work_in_progress_by_default": true,
     "mute_common_path_prefixes": true,
     "my": [
@@ -2704,6 +2706,8 @@
 Allowed values are `AUTO_MERGE` and `FIRST_PARENT`.
 |`disable_keyboard_shortcuts`     |not set if `false`|
 Whether to disable all keyboard shortcuts.
+|`disable_token_highlighting`     [not set if `false`]
+Whether to disable token highlighting on hover.
 |`publish_comments_on_push`     |not set if `false`|
 Whether to link:user-upload.html#publish-comments[publish draft comments] on
 push by default.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 444c9ee..3977278 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -737,6 +737,10 @@
 to one of the fields in the
 link:rest-api-changes.html#submit-record[SubmitRecord] REST API entity.
 
+`label:Code-Review\<=-1`::
++
+Matches changes with either a -1, -2, or any lower score.
+
 `label:Code-Review=MAX`::
 +
 Matches changes with label voted with the highest possible score.
@@ -787,10 +791,6 @@
 Matches changes with a +1 code review where the reviewer is in the
 ldap/linux.workflow group.
 
-`label:Code-Review\<=-1`::
-+
-Matches changes with either a -1, -2, or any lower score.
-
 `is:open label:Code-Review+2 label:Verified+1 NOT label:Verified-1 NOT label:Code-Review-2`::
 `is:open label:Code-Review=ok label:Verified=ok`::
 +
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 21b319e..b26f435 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -148,6 +148,7 @@
   public DefaultBase defaultBaseForMerges;
   public Boolean publishCommentsOnPush;
   public Boolean disableKeyboardShortcuts;
+  public Boolean disableTokenHighlighting;
   public Boolean workInProgressByDefault;
   public List<MenuItem> my;
   public List<String> changeTable;
@@ -207,6 +208,7 @@
     p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
     p.publishCommentsOnPush = false;
     p.disableKeyboardShortcuts = false;
+    p.disableTokenHighlighting = false;
     p.workInProgressByDefault = false;
     return p;
   }
diff --git a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
index fbc6065..01c76c1 100644
--- a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
+++ b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -157,8 +158,15 @@
   private void recomputeExternalIdNoteId(ExternalIdNotes extIdNotes, ExternalId extId)
       throws DuplicateKeyException, IOException {
     if (extId.isScheme(SCHEME_GERRIT) || extId.isScheme(SCHEME_USERNAME)) {
-      ExternalId.Key updatedKey =
-          ExternalId.Key.create(extId.key().scheme(), extId.key().id(), !isUserNameCaseInsensitive);
+      ExternalIdKeyFactory keyFactory =
+          new ExternalIdKeyFactory(
+              new ExternalIdKeyFactory.Config() {
+                @Override
+                public boolean isUserNameCaseInsensitive() {
+                  return !isUserNameCaseInsensitive;
+                }
+              });
+      ExternalId.Key updatedKey = keyFactory.create(extId.key().scheme(), extId.key().id());
       if (!extId.key().sha1().getName().equals(updatedKey.sha1().getName())) {
         logger.atInfo().log("Converting note name of external ID: %s", extId.key());
         ExternalId updatedExtId =
diff --git a/java/com/google/gerrit/server/RequestConfig.java b/java/com/google/gerrit/server/RequestConfig.java
index 960907d..83cea5b 100644
--- a/java/com/google/gerrit/server/RequestConfig.java
+++ b/java/com/google/gerrit/server/RequestConfig.java
@@ -41,6 +41,7 @@
         RequestConfig.Builder requestConfig = RequestConfig.builder(cfg, section, id);
         requestConfig.requestTypes(parseRequestTypes(cfg, section, id));
         requestConfig.requestUriPatterns(parseRequestUriPatterns(cfg, section, id));
+        requestConfig.excludedRequestUriPatterns(parseExcludedRequestUriPatterns(cfg, section, id));
         requestConfig.accountIds(parseAccounts(cfg, section, id));
         requestConfig.projectPatterns(parseProjectPatterns(cfg, section, id));
         requestConfigs.add(requestConfig.build());
@@ -61,6 +62,11 @@
     return parsePatterns(cfg, section, id, "requestUriPattern");
   }
 
+  private static ImmutableSet<Pattern> parseExcludedRequestUriPatterns(
+      Config cfg, String section, String id) throws ConfigInvalidException {
+    return parsePatterns(cfg, section, id, "excludedRequestUriPattern");
+  }
+
   private static ImmutableSet<Account.Id> parseAccounts(Config cfg, String section, String id)
       throws ConfigInvalidException {
     ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
@@ -115,6 +121,9 @@
   /** pattern matching request URIs */
   abstract ImmutableSet<Pattern> requestUriPatterns();
 
+  /** pattern matching request URIs to be excluded */
+  abstract ImmutableSet<Pattern> excludedRequestUriPatterns();
+
   /** accounts IDs matching calling user */
   abstract ImmutableSet<Account.Id> accountIds();
 
@@ -154,6 +163,13 @@
       }
     }
 
+    // If the request URI matches an excluded request URI pattern, then the request is not matched.
+    if (requestInfo.requestUri().isPresent()
+        && excludedRequestUriPatterns().stream()
+            .anyMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
+      return false;
+    }
+
     // If in the request config accounts are set and none of them matches, then the request is not
     // matched.
     if (!accountIds().isEmpty()) {
@@ -200,6 +216,8 @@
 
     abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
 
+    abstract Builder excludedRequestUriPatterns(ImmutableSet<Pattern> excludedRequestUriPatterns);
+
     abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
 
     abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index 0c96f58..ee42d67 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -24,9 +24,9 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.HashedPassword;
 import com.google.gerrit.server.account.externalids.ExternalId.Key;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
index 8d56827..7d2f9de 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
@@ -18,8 +18,8 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.ImplementedBy;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
 @Singleton
 public class ExternalIdKeyFactory {
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index ec658ac..b7620e5 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.approval;
 
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
@@ -27,8 +28,10 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -77,13 +80,16 @@
    */
   public boolean match(
       Map<String, FileDiffOutput> modifiedFiles1, Map<String, FileDiffOutput> modifiedFiles2) {
-    if (modifiedFiles1.size() != modifiedFiles2.size()) {
-      return false;
-    }
-    for (String file : modifiedFiles1.keySet()) {
+    Set<String> allFiles = new HashSet<>();
+    allFiles.addAll(modifiedFiles1.keySet());
+    allFiles.addAll(modifiedFiles2.keySet());
+    for (String file : allFiles) {
+      if (Patch.isMagic(file)) {
+        continue;
+      }
       FileDiffOutput fileDiffOutput1 = modifiedFiles1.get(file);
       FileDiffOutput fileDiffOutput2 = modifiedFiles2.get(file);
-      if (fileDiffOutput2 == null) {
+      if (fileDiffOutput1 == null || fileDiffOutput2 == null) {
         return false;
       }
       if (!fileDiffOutput2.changeType().equals(fileDiffOutput1.changeType())) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index f966fc8..d502db2 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -31,6 +31,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -49,6 +50,7 @@
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
@@ -902,6 +904,52 @@
   }
 
   @Test
+  public void copyWithListOfFilesUnchangedButAddedMergeList() throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("has:unchanged-files"));
+      u.save();
+    }
+    Change.Id parent1ChangeId = changeOperations.newChange().create();
+    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id dummyParentChangeId = changeOperations.newChange().create();
+    Change.Id changeId =
+        changeOperations
+            .newChange()
+            .mergeOf()
+            .change(parent1ChangeId)
+            .and()
+            .change(parent2ChangeId)
+            .create();
+
+    Map<String, FileInfo> changedFilesFirstPatchset =
+        gApi.changes().id(changeId.get()).current().files();
+
+    assertThat(changedFilesFirstPatchset.keySet()).containsExactly("/COMMIT_MSG", "/MERGE_LIST");
+
+    // Make a Code-Review vote that should be sticky.
+    gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
+
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .parent()
+        .patchset(PatchSet.id(dummyParentChangeId, 1))
+        .create();
+
+    Map<String, FileInfo> changedFilesSecondPatchset =
+        gApi.changes().id(changeId.get()).current().files();
+
+    // Only "/MERGE_LIST" was removed.
+    assertThat(changedFilesSecondPatchset.keySet()).containsExactly("/COMMIT_MSG");
+    ApprovalInfo approvalInfo =
+        Iterables.getOnlyElement(
+            gApi.changes().id(changeId.get()).current().votes().get(LabelId.CODE_REVIEW));
+    assertThat(approvalInfo._accountId).isEqualTo(admin.id().get());
+    assertThat(approvalInfo.value).isEqualTo(2);
+  }
+
+  @Test
   public void deleteStickyVote() throws Exception {
     String label = LabelId.CODE_REVIEW;
     try (ProjectConfigUpdate u = updateProject(project)) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index 9f294b3..c5ceea0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -232,6 +232,32 @@
 
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(
+      name = "deadline.default.excludedRequestUriPattern",
+      value = "/projects/non-matching")
+  public void abortIfServerDeadlineExceeded_excludedRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(
+      name = "deadline.default.excludedRequestUriPattern",
+      value = "/projects/non-matching")
+  public void abortIfServerDeadlineExceeded_requestUriPatternAndExcludedRequestUriPattern()
+      throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = ".*new.*")
   public void abortIfServerDeadlineExceeded_projectPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
@@ -268,6 +294,24 @@
 
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*")
+  public void nonMatchingServerDeadlineIsIgnored_excludedRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*new")
+  public void nonMatchingServerDeadlineIsIgnored_requestUriPatternAndExcludedRequestUriPattern()
+      throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = ".*foo.*")
   public void nonMatchingServerDeadlineIsIgnored_projectPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
@@ -340,6 +384,14 @@
 
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "][")
+  public void invalidServerDeadlineIsIgnored_invalidExcludedRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = "][")
   public void invalidServerDeadlineIsIgnored_invalidProjectPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 530f2ec..7e40b2b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -711,6 +711,94 @@
   }
 
   @Test
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/.*")
+  public void traceExcludedRequestUriPattern() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz1");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz1");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/no-match")
+  public void traceExcludedRequestUriPatternNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz3");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz3");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/xyz2")
+  public void traceRequestUriPatternAndExcludedRequestUriPattern() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz2");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz2");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/no-match")
+  public void traceRequestUriPatternAndExcludedRequestUriPatternNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz3");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz3");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "][")
+  public void traceExcludedRequestUriInvalidRegEx() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz4");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz4");
+    }
+  }
+
+  @Test
   @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
   public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
     String changeId = createChange().getChangeId();
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 26701e8..1453fd0 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -209,6 +209,18 @@
   line_wrapping?: boolean;
 }
 
+/**
+ * Listens to changes in token highlighting - when a new token starts or stopped being highlighted.
+ * Examples:
+ * - Token highlighted: ('myFunctionName', 12, [Element]).
+ * - Token unhighlighted: (undefined, 0, undefined).
+ */
+export type TokenHighlightedListener = (
+  newHighlight: string | undefined,
+  newLineNumber: number,
+  hoveredElement?: Element
+) => void;
+
 export declare interface ImageDiffPreferences {
   automatic_blink?: boolean;
 }
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index ba378e2..fed724e 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -20,14 +20,24 @@
  * limitations under the License.
  */
 
-import {DiffLayer, GrAnnotation, GrDiffCursor} from './diff';
+import {
+  DiffLayer,
+  GrAnnotation,
+  GrDiffCursor,
+  TokenHighlightedListener,
+} from './diff';
 
 declare global {
   interface Window {
     grdiff: {
       GrAnnotation: GrAnnotation;
       GrDiffCursor: {new (): GrDiffCursor};
-      TokenHighlightLayer: {new (container?: HTMLElement): DiffLayer};
+      TokenHighlightLayer: {
+        new (
+          container?: HTMLElement,
+          listener?: TokenHighlightedListener
+        ): DiffLayer;
+      };
     };
   }
 }
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 9970dd5..fae518a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -116,6 +116,13 @@
     super();
     this.addEventListener('next-page', () => this._handleNextPage());
     this.addEventListener('previous-page', () => this._handlePreviousPage());
+    this.addEventListener('reload', () => {
+      this._loading = true;
+      this._getChanges().then(changes => {
+        this._changes = changes || [];
+        this._loading = false;
+      });
+    });
   }
 
   override connectedCallback() {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index 058007b..b081be7 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -58,8 +58,8 @@
       display: inline-block;
     }
     gr-vote-chip {
-      --gr-vote-chip-width: 16px;
-      --gr-vote-chip-height: 16px;
+      --gr-vote-chip-width: 14px;
+      --gr-vote-chip-height: 14px;
     }
   </style>
   <div class="container">
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 286f24a..724d803 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -30,6 +30,7 @@
   extractAssociatedLabels,
   iconForStatus,
 } from '../../../utils/label-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
 
 @customElement('gr-submit-requirements')
 export class GrSubmitRequirements extends LitElement {
@@ -44,43 +45,19 @@
 
   static override get styles() {
     return [
+      fontStyles,
       css`
-        :host {
-          display: table;
-          width: 100%;
-        }
         .metadata-title {
-          font-size: 100%;
           font-weight: var(--font-weight-bold);
           color: var(--deemphasized-text-color);
           padding-left: var(--metadata-horizontal-padding);
-        }
-        section {
-          display: table-row;
-        }
-        .title {
-          min-width: 10em;
-          padding: var(--spacing-s) 0 0 0;
-        }
-        .value {
-          padding: var(--spacing-s) 0 0 0;
-        }
-        .title,
-        .status {
-          display: table-cell;
-          vertical-align: top;
-        }
-        .value {
-          display: inline-flex;
-        }
-        .status {
-          width: var(--line-height-small);
-          padding: var(--spacing-s) var(--spacing-m) 0
-            var(--requirements-horizontal-padding);
+          margin: 0 0 var(--spacing-s);
+          border-top: 1px solid var(--border-color);
+          padding-top: var(--spacing-s);
         }
         iron-icon {
-          width: var(--line-height-small);
-          height: var(--line-height-small);
+          width: var(--line-height-normal, 20px);
+          height: var(--line-height-normal, 20px);
         }
         iron-icon.check {
           color: var(--success-foreground);
@@ -102,6 +79,20 @@
         .testing:hover * {
           visibility: visible;
         }
+        .requirements,
+        section.votes {
+          margin-left: var(--spacing-l);
+        }
+        gr-limited-text.name {
+          font-weight: var(--font-weight-bold);
+        }
+        table {
+          border-collapse: collapse;
+          border-spacing: 0;
+        }
+        td {
+          padding: var(--spacing-s);
+        }
       `,
     ];
   }
@@ -110,25 +101,46 @@
     const submit_requirements = (this.change?.submit_requirements ?? []).filter(
       req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
     );
-    return html`<h3 class="metadata-title">Submit Requirements</h3>
+    return html` <h2
+        class="metadata-title heading-3"
+        id="submit-requirements-caption"
+      >
+        Submit Requirements
+      </h2>
+      <table class="requirements" aria-labelledby="submit-requirements-caption">
+        <thead hidden>
+          <tr>
+            <th>Status</th>
+            <th>Name</th>
+            <th>Votes</th>
+          </tr>
+        </thead>
+        <tbody>
+          ${submit_requirements.map(
+            requirement => html`<tr id="requirement-${requirement.name}">
+              <td>${this.renderStatus(requirement.status)}</td>
+              <td class="name">
+                <gr-limited-text
+                  class="name"
+                  limit="25"
+                  .text="${requirement.name}"
+                ></gr-limited-text>
+              </td>
+              <td>${this.renderVotes(requirement)}</td>
+            </tr>`
+          )}
+        </tbody>
+      </table>
       ${submit_requirements.map(
-        requirement => html`<section>
+        requirement => html`
           <gr-submit-requirement-hovercard
+            for="requirement-${requirement.name}"
             .requirement="${requirement}"
             .change="${this.change}"
             .account="${this.account}"
             .mutable="${this.mutable}"
           ></gr-submit-requirement-hovercard>
-          <div class="status">${this.renderStatus(requirement.status)}</div>
-          <div class="title">
-            <gr-limited-text
-              class="name"
-              limit="25"
-              text="${requirement.name}"
-            ></gr-limited-text>
-          </div>
-          <div class="value">${this.renderVotes(requirement)}</div>
-        </section>`
+        `
       )}
       ${this.renderTriggerVotes(
         submit_requirements
@@ -140,6 +152,8 @@
     return html`<iron-icon
       class="${icon}"
       icon="gr-icons:${icon}"
+      role="img"
+      aria-label="${status.toLowerCase()}"
     ></iron-icon>`;
   }
 
@@ -183,17 +197,19 @@
       label => !labelAssociatedWithSubmitReqs.includes(label)
     );
     if (!triggerVotes.length) return;
-    return html`<h3 class="metadata-title">Trigger Votes</h3>
-      ${triggerVotes.map(
-        label => html`${label}:
-          <gr-label-info
-            .change="${this.change}"
-            .account="${this.account}"
-            .mutable="${this.mutable}"
-            label="${label}"
-            .labelInfo="${labels[label]}"
-          ></gr-label-info>`
-      )}`;
+    return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
+      <section class="votes">
+        ${triggerVotes.map(
+          label => html`${label}:
+            <gr-label-info
+              .change="${this.change}"
+              .account="${this.account}"
+              .mutable="${this.mutable}"
+              label="${label}"
+              .labelInfo="${labels[label]}"
+            ></gr-label-info>`
+        )}
+      </section>`;
   }
 
   renderFakeControls() {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 4d97fec..6ed5a2c 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -102,9 +102,9 @@
         items="[[getCommentsDropdownEntires(threads, loggedIn)]]"
       >
       </gr-dropdown-list>
-      <template is="dom-if" if="[[threads.length]]">
+      <template is="dom-if" if="[[_displayedThreads.length]]">
         <span class="author-text">From:</span>
-        <template is="dom-repeat" items="[[getCommentAuthors(threads, account)]]">
+        <template is="dom-repeat" items="[[getCommentAuthors(_displayedThreads, account)]]">
           <gr-account-label
             account="[[item]]"
             on-click="handleAccountClicked"
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
index b478ef5..aab5cee 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
@@ -495,15 +495,18 @@
   test('tapping single author chips', () => {
     element.account = createAccountDetailWithId(1);
     flush();
-    const chips = queryAll(element, 'gr-account-label');
-    const authors = Array.from(chips).map(
+    const chips = Array.from(queryAll(element, 'gr-account-label'));
+    const authors = chips.map(
         chip => accountOrGroupKey(chip.account))
         .sort();
     assert.deepEqual(authors, [1, 1000000, 1000001, 1000002, 1000003]);
     assert.equal(element.threads.length, 9);
     assert.equal(element._displayedThreads.length, 9);
 
-    tap(chips[0]); // accountId 1000001
+    // accountId 1000001
+    const chip = chips.find(chip => chip.account._account_id === 1000001);
+
+    tap(chip);
     flush();
 
     assert.equal(element.threads.length, 9);
@@ -511,7 +514,7 @@
     assert.equal(element._displayedThreads[0].comments[0].author._account_id,
         1000001);
 
-    tap(chips[0]); // tapping again resets
+    tap(chip); // tapping again resets
     flush();
     assert.equal(element.threads.length, 9);
     assert.equal(element._displayedThreads.length, 9);
@@ -520,10 +523,10 @@
   test('tapping multiple author chips', () => {
     element.account = createAccountDetailWithId(1);
     flush();
-    const chips = queryAll(element, 'gr-account-label');
+    const chips = Array.from(queryAll(element, 'gr-account-label'));
 
-    tap(chips[0]); // accountId 1000001
-    tap(chips[2]); // accountId 1000002
+    tap(chips.find(chip => chip.account._account_id === 1000001));
+    tap(chips.find(chip => chip.account._account_id === 1000002));
     flush();
 
     assert.equal(element.threads.length, 9);
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 6dd67e4..1c25755 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -38,7 +38,7 @@
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {appContext} from '../../../services/app-context';
 import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
-import {ParsedChangeInfo} from '../../../types/types';
+import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
 import {KnownExperimentId} from '../../../services/flags/flags';
@@ -99,16 +99,25 @@
   })
   _disableApplyFixButton = false;
 
-  layers = appContext.flagsService.isEnabled(
-    KnownExperimentId.TOKEN_HIGHLIGHTING
-  )
-    ? [new TokenHighlightLayer(this)]
-    : [];
+  @property({type: Array})
+  layers: DiffLayer[] = [];
 
   private refitOverlay?: () => void;
 
   private readonly restApiService = appContext.restApiService;
 
+  constructor() {
+    super();
+    this.restApiService.getPreferences().then(prefs => {
+      if (
+        !prefs?.disable_token_highlighting &&
+        appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
+      ) {
+        this.layers = [new TokenHighlightLayer(this)];
+      }
+    });
+  }
+
   /**
    * Given robot comment CustomEvent object, fetch diffs associated
    * with first robot comment suggested fix and open dialog.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index 56bb073..480e26c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 import {DiffLayer, DiffLayerListener} from '../../../types/types';
-import {GrDiffLine, Side} from '../../../api/diff';
+import {GrDiffLine, Side, TokenHighlightedListener} from '../../../api/diff';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {
@@ -65,6 +65,9 @@
   /** The currently highlighted token. */
   private currentHighlight?: string;
 
+  /** Trigger when a new token starts or stoped being highlighted.*/
+  private readonly tokenHighlightedListener?: TokenHighlightedListener;
+
   /**
    * The line of the currently highlighted token. We store this in order to
    * re-render only relevant lines of the diff. Only lines visible on the screen
@@ -95,7 +98,11 @@
 
   private updateTokenTask?: DelayedTask;
 
-  constructor(container: HTMLElement = document.documentElement) {
+  constructor(
+    container: HTMLElement = document.documentElement,
+    tokenHighlightedListener?: TokenHighlightedListener
+  ) {
+    this.tokenHighlightedListener = tokenHighlightedListener;
     container.addEventListener('click', e => {
       this.handleContainerClick(e);
     });
@@ -188,7 +195,7 @@
     this.updateTokenTask = debounce(
       this.updateTokenTask,
       () => {
-        this.updateTokenHighlight(newHighlight, line);
+        this.updateTokenHighlight(newHighlight, line, element);
       },
       HOVER_DELAY_MS
     );
@@ -203,7 +210,7 @@
     if (element) return;
     this.hoveredElement = undefined;
     this.updateTokenTask?.cancel();
-    this.updateTokenHighlight(undefined, 0);
+    this.updateTokenHighlight(undefined, 0, undefined);
   }
 
   private interferesWithSelection() {
@@ -241,7 +248,8 @@
 
   private updateTokenHighlight(
     newHighlight: string | undefined,
-    newLineNumber: number
+    newLineNumber: number,
+    newHoveredElement: Element | undefined
   ) {
     if (
       this.currentHighlight === newHighlight &&
@@ -253,6 +261,13 @@
     this.currentHighlight = newHighlight;
     this.currentHighlightLineNumber = newLineNumber;
 
+    if (this.tokenHighlightedListener) {
+      this.tokenHighlightedListener(
+        newHighlight,
+        newLineNumber,
+        newHoveredElement
+      );
+    }
     this.notifyForToken(oldHighlight, oldLineNumber);
     this.notifyForToken(newHighlight, newLineNumber);
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 9fc69b5..2cb08d6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -66,12 +66,22 @@
   let container: HTMLElement;
   let listener: MockListener;
   let highlighter: TokenHighlightLayer;
+  let tokenHighlightingCalls: any[] = [];
+
+  function tokenHighlightedListener(
+    newHighlight: string | undefined,
+    newLineNumber: number,
+    hoveredElement?: Element
+  ) {
+    tokenHighlightingCalls.push({newHighlight, newLineNumber, hoveredElement});
+  }
 
   setup(async () => {
     listener = new MockListener();
+    tokenHighlightingCalls = [];
     container = document.createElement('div');
     document.body.appendChild(container);
-    highlighter = new TokenHighlightLayer(container);
+    highlighter = new TokenHighlightLayer(container, tokenHighlightedListener);
     highlighter.addListener((...args) => listener.notify(...args));
   });
 
@@ -251,6 +261,37 @@
       assert.equal(_testOnly_allTasks.size, 0);
     });
 
+    test('triggers listener for applying and clearing highlighting', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words', 2);
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(tokenHighlightingCalls.length, 0);
+      clock.tick(HOVER_DELAY_MS);
+      assert.equal(tokenHighlightingCalls.length, 1);
+      assert.deepEqual(tokenHighlightingCalls[0], {
+        newHighlight: 'words',
+        newLineNumber: 1,
+        hoveredElement: words1,
+      });
+
+      MockInteractions.click(container);
+      assert.equal(tokenHighlightingCalls.length, 2);
+      assert.deepEqual(tokenHighlightingCalls[1], {
+        newHighlight: undefined,
+        newLineNumber: 0,
+        hoveredElement: undefined,
+      });
+    });
+
     test('clicking clears highlight', async () => {
       const clock = sinon.useFakeTimers();
       const line1 = createLine('two words');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index c4fed53..463163c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -223,6 +223,9 @@
   @property({type: Boolean})
   _loggedIn = false;
 
+  @property({type: Boolean})
+  disableTokenHighlighting = false;
+
   @property({type: String})
   _errorMessage: string | null = null;
 
@@ -300,6 +303,11 @@
     this.addEventListener('diff-context-expanded', event =>
       this._handleDiffContextExpanded(event)
     );
+    appContext.restApiService.getPreferences().then(prefs => {
+      if (prefs?.disable_token_highlighting) {
+        this.disableTokenHighlighting = prefs.disable_token_highlighting;
+      }
+    });
   }
 
   override ready() {
@@ -413,7 +421,8 @@
   private _getLayers(path: string): DiffLayer[] {
     const layers = [];
     if (
-      appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
+      appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
+      !this.disableTokenHighlighting
     ) {
       layers.push(new TokenHighlightLayer(this));
     }
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 453bc3f..da4bb0a 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -85,6 +85,7 @@
   'diff_view',
   'publish_comments_on_push',
   'disable_keyboard_shortcuts',
+  'disable_token_highlighting',
   'work_in_progress_by_default',
   'default_base_for_merges',
   'signed_off_by',
@@ -124,6 +125,7 @@
     showSizeBarsInFileList: HTMLInputElement;
     publishCommentsOnPush: HTMLInputElement;
     disableKeyboardShortcuts: HTMLInputElement;
+    disableTokenHighlighting: HTMLInputElement;
     relativeDateInChangeTable: HTMLInputElement;
     changesPerPageSelect: HTMLInputElement;
     dateTimeFormatSelect: HTMLInputElement;
@@ -408,6 +410,13 @@
     );
   }
 
+  _handleDisableTokenHighlightingChanged() {
+    this.set(
+      '_localPrefs.disable_token_highlighting',
+      this.$.disableTokenHighlighting.checked
+    );
+  }
+
   _handleWorkInProgressByDefault() {
     this.set(
       '_localPrefs.work_in_progress_by_default',
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 1ed0d57..78c4a62 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -309,6 +309,19 @@
           </span>
         </section>
         <section>
+          <label for="disableTokenHighlighting" class="title"
+            >Disable token highlighting on hover</label
+          >
+          <span class="value">
+            <input
+              id="disableTokenHighlighting"
+              type="checkbox"
+              checked$="[[_localPrefs.disable_token_highlighting]]"
+              on-change="_handleDisableTokenHighlightingChanged"
+            />
+          </span>
+        </section>
+        <section>
           <label for="insertSignedOff" class="title">
             Insert Signed-off-by Footer For Inline Edit Changes
           </label>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 26705ee..8194d5b 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -244,6 +244,13 @@
     );
     assert.equal(
       (
+        valueOf('Disable token highlighting on hover', 'preferences')!
+          .firstElementChild as HTMLInputElement
+      ).checked,
+      false
+    );
+    assert.equal(
+      (
         valueOf(
           'Insert Signed-off-by Footer For Inline Edit Changes',
           'preferences'
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index d6aae5c..82f6b46 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -204,6 +204,9 @@
   @property({type: Object})
   _selfAccount?: AccountDetailInfo;
 
+  @property({type: Boolean})
+  disableTokenHighlighting = false;
+
   get keyBindings() {
     return {
       'e shift+e': '_handleEKey',
@@ -227,6 +230,11 @@
     this.addEventListener('comment-update', e =>
       this._handleCommentUpdate(e as CustomEvent)
     );
+    appContext.restApiService.getPreferences().then(prefs => {
+      if (prefs?.disable_token_highlighting) {
+        this.disableTokenHighlighting = prefs.disable_token_highlighting;
+      }
+    });
   }
 
   override connectedCallback() {
@@ -360,7 +368,10 @@
   _getLayers(diff?: DiffInfo) {
     if (!diff) return [];
     const layers = [];
-    if (this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)) {
+    if (
+      this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
+      !this.disableTokenHighlighting
+    ) {
       layers.push(new TokenHighlightLayer(this));
     }
     layers.push(this.syntaxLayer);
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 5b9ad5c..73ca99d 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -81,13 +81,13 @@
         .vote-chip,
         .chip-angle {
           display: flex;
-          width: var(--gr-vote-chip-width, 18px);
-          height: var(--gr-vote-chip-height, 18px);
+          width: var(--gr-vote-chip-width, 16px);
+          height: var(--gr-vote-chip-height, 16px);
           justify-content: center;
           margin-right: var(--spacing-s);
           padding: 1px;
           border-radius: var(--border-radius);
-          line-height: var(--gr-vote-chip-width, 18px);
+          line-height: var(--gr-vote-chip-width, 16px);
         }
         .vote-chip {
           position: relative;
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index ef5fde2..2839874 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -27,6 +27,5 @@
   NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
   TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
   CHECKS_DEVELOPER = 'UiFeature__checks_developer',
-  NEW_REPLY_DIALOG = 'UiFeature__new_reply_dialog',
   SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
 }
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 98c60d2..1617aa3 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1143,6 +1143,7 @@
   default_base_for_merges: DefaultBase;
   publish_comments_on_push?: boolean;
   disable_keyboard_shortcuts?: boolean;
+  disable_token_highlighting?: boolean;
   work_in_progress_by_default?: boolean;
   // The email_format doesn't mentioned in doc, but exists in Java class GeneralPreferencesInfo
   email_format?: EmailFormat;