Merge "Remove new reply dialog experiment from KnownExperimentId"
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/ExternalIdKeyFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
index 37c2604..8d56827 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
@@ -14,18 +14,42 @@
 
 package com.google.gerrit.server.account.externalids;
 
+import com.google.common.annotations.VisibleForTesting;
 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;
 
 @Singleton
 public class ExternalIdKeyFactory {
+  @ImplementedBy(ConfigImpl.class)
+  public interface Config {
+    boolean isUserNameCaseInsensitive();
+  }
+
+  // Default implementation. Internally in google we are using different implementation
+  @Singleton
+  public static class ConfigImpl implements Config {
+    private final boolean isUserNameCaseInsensitive;
+
+    @VisibleForTesting
+    @Inject
+    public ConfigImpl(AuthConfig authConfig) {
+      this.isUserNameCaseInsensitive = authConfig.isUserNameCaseInsensitive();
+    }
+
+    @Override
+    public boolean isUserNameCaseInsensitive() {
+      return isUserNameCaseInsensitive;
+    }
+  }
+
   private final boolean isUserNameCaseInsensitive;
 
   @Inject
-  public ExternalIdKeyFactory(AuthConfig authConfig) {
-    this.isUserNameCaseInsensitive = authConfig.isUserNameCaseInsensitive();
+  public ExternalIdKeyFactory(Config config) {
+    this.isUserNameCaseInsensitive = config.isUserNameCaseInsensitive();
   }
 
   /**
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index 0383cdc..ca6c689 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
 import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -123,15 +124,18 @@
   }
 
   private static ImmutableList<String> toExpressionAtomList(LabelType lt) {
+    String ignoreSelfApproval =
+        lt.isIgnoreSelfApproval() ? ",user=" + ChangeQueryBuilder.ARG_ID_NON_UPLOADER : "";
     switch (lt.getFunction()) {
       case MAX_WITH_BLOCK:
         return ImmutableList.of(
-            String.format("label:%s=MAX", lt.getName()),
+            String.format("label:%s=MAX", lt.getName()) + ignoreSelfApproval,
             String.format("-label:%s=MIN", lt.getName()));
       case ANY_WITH_BLOCK:
         return ImmutableList.of(String.format(String.format("-label:%s=MIN", lt.getName())));
       case MAX_NO_BLOCK:
-        return ImmutableList.of(String.format(String.format("label:%s=MAX", lt.getName())));
+        return ImmutableList.of(
+            String.format(String.format("label:%s=MAX", lt.getName())) + ignoreSelfApproval);
       case NO_BLOCK:
       case NO_OP:
       case PATCH_SET_LOCK:
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 989b4bb..5f017fb 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -87,7 +87,7 @@
 
     try {
       MagicLabelVote mlv = MagicLabelVote.parseWithEquals(v);
-      return ImmutableList.of(new MagicLabelPredicate(args, mlv));
+      return ImmutableList.of(magicLabelPredicate(args, mlv));
     } catch (IllegalArgumentException e) {
       // Try next format.
     }
@@ -157,6 +157,17 @@
     return or(r);
   }
 
+  protected static Predicate<ChangeData> magicLabelPredicate(Args args, MagicLabelVote mlv) {
+    if (args.accounts == null || args.accounts.isEmpty()) {
+      return new MagicLabelPredicate(args, mlv, /* account= */ null);
+    }
+    List<Predicate<ChangeData>> r = new ArrayList<>();
+    for (Account.Id a : args.accounts) {
+      r.add(new MagicLabelPredicate(args, mlv, a));
+    }
+    return or(r);
+  }
+
   @Override
   public String toString() {
     return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 2c56322..3917c79 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelTypes;
@@ -28,9 +29,12 @@
 public class MagicLabelPredicate extends ChangeIndexPredicate {
   protected final LabelPredicate.Args args;
   private final MagicLabelVote magicLabelVote;
+  private final Account.Id account;
 
-  public MagicLabelPredicate(LabelPredicate.Args args, MagicLabelVote magicLabelVote) {
+  public MagicLabelPredicate(
+      LabelPredicate.Args args, MagicLabelVote magicLabelVote, Account.Id account) {
     super(ChangeField.LABEL, magicLabelVote.formatLabel());
+    this.account = account;
     this.args = args;
     this.magicLabelVote = magicLabelVote;
   }
@@ -83,7 +87,7 @@
   }
 
   private EqualsLabelPredicate numericPredicate(String label, short value) {
-    return new EqualsLabelPredicate(args, label, value, /* account= */ null);
+    return new EqualsLabelPredicate(args, label, value, account);
   }
 
   protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 95a8950..ac67444 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4059,6 +4059,46 @@
   }
 
   @Test
+  public void submitRequirement_withLabelEqualsMax_fromNonUploader() throws Exception {
+    configLabel("my-label", LabelFunction.NO_OP); // label function has no effect
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
+        .update();
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("my-label")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("label:my-label=MAX,user=non_uploader"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // Voting with a max vote as the uploader will not satisfy the submit requirement.
+    voteLabel(changeId, "my-label", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // Voting as a non-uploader will satisfy the submit requirement.
+    requestScopeOperations.setApiUser(user.id());
+    voteLabel(changeId, "my-label", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false);
+  }
+
+  @Test
   public void submitRequirement_withLabelEqualsMinBlockingSubmission() throws Exception {
     configSubmitRequirement(
         project,
@@ -4094,6 +4134,54 @@
   }
 
   @Test
+  public void submitRequirement_withMaxWithBlock_ignoringSelfApproval() throws Exception {
+    configLabel("my-label", LabelFunction.MAX_WITH_BLOCK);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
+        .update();
+
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("my-label")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create(
+                    "label:my-label=MAX,user=non_uploader -label:my-label=MIN"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    // Create the change as admin
+    requestScopeOperations.setApiUser(admin.id());
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+
+    // Admin (a.k.a uploader) adds a -1 min vote. This is going to block submission.
+    voteLabel(changeId, "my-label", -1);
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // user (i.e. non_uploader) votes 1. Requirement is still blocking because of -1 of uploader.
+    requestScopeOperations.setApiUser(user.id());
+    voteLabel(changeId, "my-label", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // Admin (a.k.a uploader) removes -1. Now requirement is fulfilled.
+    requestScopeOperations.setApiUser(admin.id());
+    voteLabel(changeId, "my-label", 0);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false);
+  }
+
+  @Test
   public void submitRequirement_withLabelEqualsAny() throws Exception {
     configSubmitRequirement(
         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/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
index 162a171..da6092b 100644
--- a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -101,7 +101,7 @@
     req = new FakeHttpServletRequest("gerrit.example.com", 80, "", "");
     res = new FakeHttpServletResponse();
 
-    extIdKeyFactory = new ExternalIdKeyFactory(authConfig);
+    extIdKeyFactory = new ExternalIdKeyFactory(new ExternalIdKeyFactory.ConfigImpl(authConfig));
     extIdFactory = new ExternalIdFactory(extIdKeyFactory);
     authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
     pwdVerifier = new PasswordVerifier(extIdKeyFactory);
diff --git a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
index 058384e..7d9db0b 100644
--- a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
@@ -25,24 +25,27 @@
 import com.google.gerrit.server.account.externalids.AllExternalIds.Serializer;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.Type;
 import java.util.Arrays;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.Mockito;
 
 public class AllExternalIdsTest {
-  private AuthConfig authConfig;
   private ExternalIdFactory externalIdFactory;
 
   @Before
   public void setUp() throws Exception {
-    authConfig = Mockito.mock(AuthConfig.class);
-    Mockito.when(authConfig.isUserNameCaseInsensitive()).thenReturn(false);
-    externalIdFactory = new ExternalIdFactory(new ExternalIdKeyFactory(authConfig));
+    externalIdFactory =
+        new ExternalIdFactory(
+            new ExternalIdKeyFactory(
+                new ExternalIdKeyFactory.Config() {
+                  @Override
+                  public boolean isUserNameCaseInsensitive() {
+                    return false;
+                  }
+                }));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
index fab90d4..4f8c559 100644
--- a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -57,14 +56,20 @@
   private GitRepositoryManager repoManager = new InMemoryRepositoryManager();
   private ExternalIdReader externalIdReader;
   private ExternalIdReader externalIdReaderSpy;
-  private AuthConfig authConfig;
+
   private ExternalIdFactory externalIdFactory;
 
   @Before
   public void setUp() throws Exception {
-    authConfig = Mockito.mock(AuthConfig.class);
-    Mockito.when(authConfig.isUserNameCaseInsensitive()).thenReturn(false);
-    externalIdFactory = new ExternalIdFactory(new ExternalIdKeyFactory(authConfig));
+    externalIdFactory =
+        new ExternalIdFactory(
+            new ExternalIdKeyFactory(
+                new ExternalIdKeyFactory.Config() {
+                  @Override
+                  public boolean isUserNameCaseInsensitive() {
+                    return false;
+                  }
+                }));
     externalIdCache = CacheBuilder.newBuilder().build();
     repoManager.createRepository(ALL_USERS).close();
     externalIdReader =
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
index 2212fe7..f6c4d6a 100644
--- a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
@@ -69,7 +69,18 @@
             .setFunction(LabelFunction.ANY_WITH_BLOCK)
             .build();
 
-    labelTypes = Arrays.asList(codeReview, verified, codeStyle);
+    LabelType ignoreSelfApprovalLabel =
+        LabelType.builder(
+                "ISA-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Looks good to me"),
+                    LabelValue.create((short) 0, "No score"),
+                    LabelValue.create((short) -1, "I would prefer this is not merged as is")))
+            .setFunction(LabelFunction.MAX_WITH_BLOCK)
+            .setIgnoreSelfApproval(true)
+            .build();
+
+    labelTypes = Arrays.asList(codeReview, verified, codeStyle, ignoreSelfApprovalLabel);
   }
 
   @Test
@@ -129,6 +140,46 @@
   }
 
   @Test
+  public void defaultSubmitRule_withLabelStatusNeed_labelHasIgnoreSelfApproval() throws Exception {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~DefaultSubmitRule",
+            Status.NOT_READY,
+            Arrays.asList(createLabel("ISA-Label", Label.Status.NEED)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "ISA-Label",
+        /* submitExpression= */ "label:ISA-Label=MAX,user=non_uploader -label:ISA-Label=MIN",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+  }
+
+  @Test
+  public void defaultSubmitRule_withLabelStatusOk_labelHasIgnoreSelfApproval() throws Exception {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~DefaultSubmitRule",
+            Status.OK,
+            Arrays.asList(createLabel("ISA-Label", Label.Status.OK)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "ISA-Label",
+        /* submitExpression= */ "label:ISA-Label=MAX,user=non_uploader -label:ISA-Label=MIN",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+  }
+
+  @Test
   public void customSubmitRule_noLabels_withStatusOk() {
     SubmitRecord submitRecord =
         createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, Arrays.asList());
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/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/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;