Merge "Remove error message from event_label."
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 685f9d9..94db15e 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3811,7 +3811,6 @@
 The `CommentLinkInput` entity describes the input for a
 link:config-gerrit.html#commentlink[commentlink].
 
-|==================================================
 [options="header",cols="1,^2,4"]
 |==================================================
 |Field Name |        |Description
diff --git a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
index 91fbf9e..fe845c0 100644
--- a/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractPluginFieldsTest.java
@@ -303,6 +303,7 @@
     return pluginInfoFromChangeInfo(changeInfos.get(0));
   }
 
+  @Nullable
   protected static List<PluginDefinedInfo> pluginInfoFromChangeInfo(ChangeInfo changeInfo) {
     List<PluginDefinedInfo> pluginInfo = changeInfo.plugins;
     if (pluginInfo == null) {
@@ -331,6 +332,7 @@
    * @param plugins list of {@code MyInfo} objects, each as a raw map returned from Gson.
    * @return decoded list of {@code MyInfo}s.
    */
+  @Nullable
   protected static List<PluginDefinedInfo> decodeRawPluginsList(
       Gson gson, @Nullable Object plugins) {
     if (plugins == null) {
diff --git a/java/com/google/gerrit/acceptance/HttpResponse.java b/java/com/google/gerrit/acceptance/HttpResponse.java
index 88079a4..76c0f04 100644
--- a/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -19,6 +19,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.Reader;
@@ -59,6 +60,7 @@
     return getHeader("X-FYI-Content-Type");
   }
 
+  @Nullable
   public String getHeader(String name) {
     Header hdr = response.getFirstHeader(name);
     return hdr != null ? hdr.getValue() : null;
diff --git a/java/com/google/gerrit/acceptance/config/BUILD b/java/com/google/gerrit/acceptance/config/BUILD
index a8ccc1f..0da68b0 100644
--- a/java/com/google/gerrit/acceptance/config/BUILD
+++ b/java/com/google/gerrit/acceptance/config/BUILD
@@ -7,6 +7,7 @@
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
         "//lib:jgit",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
index 24a2117..27ce857 100644
--- a/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
+++ b/java/com/google/gerrit/acceptance/config/ConfigAnnotationParser.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -27,6 +28,7 @@
 public class ConfigAnnotationParser {
   private static Splitter splitter = Splitter.on(".").trimResults();
 
+  @Nullable
   public static Config parse(Config base, GerritConfigs annotation) {
     if (annotation == null) {
       return null;
@@ -45,6 +47,7 @@
     return cfg;
   }
 
+  @Nullable
   public static Map<String, Config> parse(GlobalPluginConfigs annotation) {
     if (annotation == null || annotation.value().length < 1) {
       return null;
@@ -67,6 +70,7 @@
     return result;
   }
 
+  @Nullable
   public static Map<String, Config> parse(GlobalPluginConfig annotation) {
     if (annotation == null) {
       return null;
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
index 850a133..d34b79a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -8,6 +8,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/acceptance:function",
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index deeb843..b1cd506 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestCapability;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupReference;
@@ -213,6 +214,7 @@
                   as -> as.upsertPermission(key.name()).setExclusiveGroup(exclusive)));
     }
 
+    @Nullable
     private RevCommit headOrNull(String branch) {
       branch = RefNames.fullName(branch);
 
diff --git a/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java b/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java
index 8fb4d35..c40baba 100644
--- a/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java
+++ b/java/com/google/gerrit/auth/ldap/FakeLdapGroupBackend.java
@@ -39,6 +39,7 @@
     return uuid.get().startsWith(LDAP_UUID);
   }
 
+  @Nullable
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     if (!handles(uuid)) {
diff --git a/java/com/google/gerrit/auth/ldap/Helper.java b/java/com/google/gerrit/auth/ldap/Helper.java
index a939c72..c11d045 100644
--- a/java/com/google/gerrit/auth/ldap/Helper.java
+++ b/java/com/google/gerrit/auth/ldap/Helper.java
@@ -18,6 +18,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.metrics.Description;
@@ -224,6 +225,7 @@
     return ctx;
   }
 
+  @Nullable
   private DirContext kerberosOpen(Properties env)
       throws IOException, LoginException, NamingException {
     LoginContext ctx = new LoginContext("KerberosLogin");
diff --git a/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java b/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
index c3870f4..bb6480a 100644
--- a/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
+++ b/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
@@ -117,6 +117,7 @@
     return isLdapUUID(uuid);
   }
 
+  @Nullable
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     if (!handles(uuid)) {
diff --git a/java/com/google/gerrit/auth/ldap/LdapQuery.java b/java/com/google/gerrit/auth/ldap/LdapQuery.java
index 409c9f5..71dc141 100644
--- a/java/com/google/gerrit/auth/ldap/LdapQuery.java
+++ b/java/com/google/gerrit/auth/ldap/LdapQuery.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.auth.ldap;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.metrics.Timer0;
 import java.util.ArrayList;
@@ -114,6 +115,7 @@
       return get("dn");
     }
 
+    @Nullable
     String get(String attName) throws NamingException {
       final Attribute att = getAll(attName);
       return att != null && 0 < att.size() ? String.valueOf(att.get(0)) : null;
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index 7699799..7dc2b1b 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -20,6 +20,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
@@ -162,6 +163,7 @@
     return vlist;
   }
 
+  @Nullable
   static String optdef(Config c, String n, String d) {
     final String[] v = c.getStringList("ldap", null, n);
     if (v == null || v.length == 0) {
@@ -184,6 +186,7 @@
     return v;
   }
 
+  @Nullable
   static ParameterizedString paramString(Config c, String n, String d) {
     String expression = optdef(c, n, d);
     if (expression == null) {
@@ -209,6 +212,7 @@
     return !readOnlyAccountFields.contains(field);
   }
 
+  @Nullable
   static String apply(ParameterizedString p, LdapQuery.Result m) throws NamingException {
     if (p == null) {
       return null;
@@ -306,6 +310,7 @@
     usernameCache.put(who.getLocalUser(), Optional.of(account.id()));
   }
 
+  @Nullable
   @Override
   public Account.Id lookup(String accountName) {
     if (Strings.isNullOrEmpty(accountName)) {
diff --git a/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java
index b0c1f51..ab53cde 100644
--- a/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/auth/oauth/OAuthTokenCache.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Converter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
@@ -109,6 +110,7 @@
     this.encrypter = encrypter;
   }
 
+  @Nullable
   public OAuthToken get(Account.Id id) {
     OAuthToken accessToken = cache.getIfPresent(id);
     if (accessToken == null) {
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index 253266d..0a42d09 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.PermissionRange;
 import java.util.ArrayList;
@@ -190,6 +191,7 @@
   }
 
   /** Returns the valid range for the capability if it has one, otherwise null. */
+  @Nullable
   public static PermissionRange.WithDefaults getRange(String varName) {
     if (QUERY_LIMIT.equalsIgnoreCase(varName)) {
       return new PermissionRange.WithDefaults(
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 303e79f..93a4408 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -58,6 +58,7 @@
       return Optional.ofNullable(Ints.tryParse(str)).map(Account::id);
     }
 
+    @Nullable
     public static Id fromRef(String name) {
       if (name == null) {
         return null;
@@ -78,11 +79,13 @@
      * @param name a ref name with the following syntax: {@code "34/1234..."}. We assume that the
      *     caller has trimmed any prefix.
      */
+    @Nullable
     public static Id fromRefPart(String name) {
       Integer id = RefNames.parseShardedRefPart(name);
       return id != null ? Account.id(id) : null;
     }
 
+    @Nullable
     public static Id parseAfterShardedRefPart(String name) {
       Integer id = RefNames.parseAfterShardedRefPart(name);
       return id != null ? Account.id(id) : null;
@@ -98,6 +101,7 @@
      * @param name ref name
      * @return account ID, or null if not numeric.
      */
+    @Nullable
     public static Id fromRefSuffix(String name) {
       Integer id = RefNames.parseRefSuffix(name);
       return id != null ? Account.id(id) : null;
diff --git a/java/com/google/gerrit/entities/AccountGroup.java b/java/com/google/gerrit/entities/AccountGroup.java
index 001a544..b5c97da 100644
--- a/java/com/google/gerrit/entities/AccountGroup.java
+++ b/java/com/google/gerrit/entities/AccountGroup.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
 
 public final class AccountGroup {
   public static NameKey nameKey(String n) {
@@ -65,6 +66,7 @@
     }
 
     /** Parse an {@link AccountGroup.UUID} out of a ref-name. */
+    @Nullable
     public static UUID fromRef(String ref) {
       if (ref == null) {
         return null;
@@ -81,6 +83,7 @@
      * @param refPart a ref name with the following syntax: {@code "12/1234..."}. We assume that the
      *     caller has trimmed any prefix.
      */
+    @Nullable
     public static UUID fromRefPart(String refPart) {
       String uuid = RefNames.parseShardedUuidFromRefPart(refPart);
       return uuid != null ? AccountGroup.uuid(uuid) : null;
diff --git a/java/com/google/gerrit/entities/Address.java b/java/com/google/gerrit/entities/Address.java
index 5d63476..eb1da46 100644
--- a/java/com/google/gerrit/entities/Address.java
+++ b/java/com/google/gerrit/entities/Address.java
@@ -46,6 +46,7 @@
     throw new IllegalArgumentException("Invalid email address: " + in);
   }
 
+  @Nullable
   public static Address tryParse(String in) {
     try {
       return parse(in);
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index 66e1a96..55220f3 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -117,6 +117,7 @@
       return id != null ? Optional.of(Change.id(id)) : Optional.empty();
     }
 
+    @Nullable
     public static Id fromRef(String ref) {
       if (RefNames.isRefsEdit(ref)) {
         return fromEditRefPart(ref);
@@ -134,6 +135,7 @@
       return null;
     }
 
+    @Nullable
     public static Id fromAllUsersRef(String ref) {
       if (ref == null) {
         return null;
@@ -169,6 +171,7 @@
       return true;
     }
 
+    @Nullable
     public static Id fromEditRefPart(String ref) {
       int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) + RefNames.EDIT_PREFIX.length();
       int endChangeId = nextNonDigit(ref, startChangeId);
@@ -179,6 +182,7 @@
       return null;
     }
 
+    @Nullable
     public static Id fromRefPart(String ref) {
       Integer id = RefNames.parseShardedRefPart(ref);
       return id != null ? Change.id(id) : null;
@@ -404,6 +408,7 @@
       return changeStatus;
     }
 
+    @Nullable
     public static Status forCode(char c) {
       for (Status s : Status.values()) {
         if (s.code == c) {
@@ -414,6 +419,7 @@
       return null;
     }
 
+    @Nullable
     public static Status forChangeStatus(ChangeStatus cs) {
       for (Status s : Status.values()) {
         if (s.changeStatus == cs) {
@@ -599,6 +605,7 @@
   }
 
   /** Get the id of the most current {@link PatchSet} in this change. */
+  @Nullable
   public PatchSet.Id currentPatchSetId() {
     if (currentPatchSetId > 0) {
       return PatchSet.id(changeId, currentPatchSetId);
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index 65a1559..f82efc0 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -49,6 +49,7 @@
       return code;
     }
 
+    @Nullable
     public static Status forCode(char c) {
       for (Status s : Status.values()) {
         if (s.code == c) {
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index e31c764..f009872 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -132,6 +132,7 @@
     return psa.labelId().get().equalsIgnoreCase(getName());
   }
 
+  @Nullable
   public LabelValue getMin() {
     if (getValues().isEmpty()) {
       return null;
@@ -139,6 +140,7 @@
     return getValues().get(0);
   }
 
+  @Nullable
   public LabelValue getMax() {
     if (getValues().isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index 3c33490..bef6580 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -19,6 +19,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Splitter;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import java.util.List;
 
@@ -112,6 +113,7 @@
     }
 
     @UsedAt(UsedAt.Project.COLLABNET)
+    @Nullable
     public static ChangeType forCode(char c) {
       for (ChangeType s : ChangeType.values()) {
         if (s.code == c) {
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index 6c52368..3ee90e8 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 import com.google.errorprone.annotations.InlineMe;
+import com.google.gerrit.common.Nullable;
 import java.time.Instant;
 import java.util.List;
 import java.util.Optional;
@@ -83,6 +84,7 @@
     }
 
     /** Parse a PatchSet.Id from a {@link #refName()} result. */
+    @Nullable
     public static Id fromRef(String ref) {
       int cs = Change.Id.startIndex(ref);
       if (cs < 0) {
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 95164bd..e3e3a57 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -124,6 +124,7 @@
     return LABEL_AS + labelName;
   }
 
+  @Nullable
   public static String extractLabel(String varName) {
     if (isLabel(varName)) {
       return varName.substring(LABEL.length());
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 617b827..b587b1d 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -166,6 +166,7 @@
    * @return name key of the parent project, {@code null} if this project is the All-Projects
    *     project
    */
+  @Nullable
   public Project.NameKey getParent(Project.NameKey allProjectsName) {
     if (getParent() != null) {
       return getParent();
@@ -178,6 +179,7 @@
     return allProjectsName;
   }
 
+  @Nullable
   public String getParentName() {
     return getParent() != null ? getParent().get() : null;
   }
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index b9c1b3c..9745fc5 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import java.util.List;
 
@@ -222,6 +223,7 @@
     return REFS_CACHE_AUTOMERGE + hash.substring(0, 2) + '/' + hash.substring(2);
   }
 
+  @Nullable
   public static String shard(int id) {
     if (id < 0) {
       return null;
@@ -343,6 +345,7 @@
     return GERRIT_REFS.stream().anyMatch(internalRef -> ref.startsWith(internalRef));
   }
 
+  @Nullable
   static Integer parseShardedRefPart(String name) {
     if (name == null) {
       return null;
@@ -386,6 +389,7 @@
   }
 
   @UsedAt(UsedAt.Project.PLUGINS_ALL)
+  @Nullable
   public static String parseShardedUuidFromRefPart(String name) {
     if (name == null) {
       return null;
@@ -420,6 +424,7 @@
    * @return the rest of the name, {@code null} if the ref name part doesn't start with a valid
    *     sharded ID
    */
+  @Nullable
   static String skipShardedRefPart(String name) {
     if (name == null) {
       return null;
@@ -473,6 +478,7 @@
    *     ref name part doesn't start with a valid sharded ID or if no valid ID follows the sharded
    *     ref part
    */
+  @Nullable
   static Integer parseAfterShardedRefPart(String name) {
     String rest = skipShardedRefPart(name);
     if (rest == null || !rest.startsWith("/")) {
@@ -493,6 +499,7 @@
     return Integer.parseInt(rest.substring(0, ie));
   }
 
+  @Nullable
   public static Integer parseRefSuffix(String name) {
     if (name == null) {
       return null;
diff --git a/java/com/google/gerrit/extensions/client/Side.java b/java/com/google/gerrit/extensions/client/Side.java
index e077df2..a87b37a 100644
--- a/java/com/google/gerrit/extensions/client/Side.java
+++ b/java/com/google/gerrit/extensions/client/Side.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.extensions.client;
 
+import com.google.gerrit.common.Nullable;
+
 public enum Side {
   PARENT,
   REVISION;
 
+  @Nullable
   public static Side fromShort(short s) {
     if (s <= 0) {
       return PARENT;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index ad112d3..24182cc 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -21,6 +21,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.sql.Timestamp;
@@ -147,12 +148,14 @@
   }
 
   /** Returns {@code null} if nothing has been added to {@code oldCollection} */
+  @Nullable
   private static ImmutableList<?> getAddedForCollection(
       Collection<?> oldCollection, Collection<?> newCollection) {
     ImmutableList<?> notInOldCollection = getAdditions(oldCollection, newCollection);
     return notInOldCollection.isEmpty() ? null : notInOldCollection;
   }
 
+  @Nullable
   private static ImmutableList<Object> getAdditions(
       Collection<?> oldCollection, Collection<?> newCollection) {
     if (oldCollection == null)
@@ -169,6 +172,7 @@
   }
 
   /** Returns {@code null} if nothing has been added to {@code oldMap} */
+  @Nullable
   private static ImmutableMap<Object, Object> getAddedForMap(Map<?, ?> oldMap, Map<?, ?> newMap) {
     ImmutableMap.Builder<Object, Object> additionsBuilder = ImmutableMap.builder();
     for (Map.Entry<?, ?> entry : newMap.entrySet()) {
diff --git a/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
index d8dd1f9..7ed7077 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.registration;
 
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -39,6 +40,7 @@
     return new DynamicItem<>(key, find(injector, type), PluginName.GERRIT);
   }
 
+  @Nullable
   private static <T> Provider<T> find(Injector src, TypeLiteral<T> type) {
     List<Binding<T>> bindings = src.findBindingsByType(type);
     if (bindings != null && bindings.size() == 1) {
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index a0b2c6a..6dc8c6a 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -313,6 +314,7 @@
       return key;
     }
 
+    @Nullable
     @Override
     public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
       Extension<T> n = new Extension<>(item.getPluginName(), newItem);
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index fb520b4..67fc068 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -16,6 +16,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -79,6 +80,7 @@
       return key;
     }
 
+    @Nullable
     @Override
     public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
       if (items.replace(np, item, newItem)) {
diff --git a/java/com/google/gerrit/extensions/restapi/Url.java b/java/com/google/gerrit/extensions/restapi/Url.java
index 9c69376..09def84 100644
--- a/java/com/google/gerrit/extensions/restapi/Url.java
+++ b/java/com/google/gerrit/extensions/restapi/Url.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.common.Nullable;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -40,6 +41,7 @@
    * @param component a string containing text to encode.
    * @return a string with all invalid URL characters escaped.
    */
+  @Nullable
   public static String encode(String component) {
     if (component != null) {
       try {
@@ -52,6 +54,7 @@
   }
 
   /** Decode a URL encoded string, e.g. from {@code "%2F"} to {@code "/"}. */
+  @Nullable
   public static String decode(String str) {
     if (str != null) {
       try {
diff --git a/java/com/google/gerrit/gpg/BUILD b/java/com/google/gerrit/gpg/BUILD
index 0ee5212..b2173c4 100644
--- a/java/com/google/gerrit/gpg/BUILD
+++ b/java/com/google/gerrit/gpg/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
diff --git a/java/com/google/gerrit/gpg/PublicKeyChecker.java b/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 0a96212..5347398 100644
--- a/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -30,6 +30,7 @@
 import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import java.io.IOException;
 import java.time.Instant;
@@ -229,6 +230,7 @@
         || PushCertificateChecker.getCreationTime(revocation).isBefore(now);
   }
 
+  @Nullable
   private PGPSignature scanRevocations(
       PGPPublicKey key,
       Instant now,
@@ -264,6 +266,7 @@
     return null;
   }
 
+  @Nullable
   private RevocationKey getRevocationKey(PGPPublicKey key, PGPSignature sig) throws PGPException {
     if (sig.getKeyID() != key.getKeyID()) {
       return null;
@@ -320,6 +323,7 @@
     }
   }
 
+  @Nullable
   private static RevocationReason getRevocationReason(PGPSignature sig) {
     if (sig.getSignatureType() != KEY_REVOCATION) {
       throw new IllegalArgumentException(
@@ -425,6 +429,7 @@
     return CheckResult.create(OK, problems);
   }
 
+  @Nullable
   private static PGPPublicKey getSigner(
       PublicKeyStore store,
       PGPSignature sig,
@@ -455,6 +460,7 @@
     }
   }
 
+  @Nullable
   private String checkTrustSubpacket(PGPSignature sig, int depth) {
     SignatureSubpacket trustSub =
         sig.getHashedSubPackets().getSubpacket(SignatureSubpacketTags.TRUST_SIG);
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index 2cce480..d167ac8 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.ObjectIds;
 import java.io.ByteArrayInputStream;
@@ -92,6 +93,7 @@
    *     null} if none was found.
    * @throws PGPException if an error occurred verifying the signature.
    */
+  @Nullable
   public static PGPPublicKey getSigner(
       Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, byte[] data) throws PGPException {
     for (PGPPublicKeyRing kr : keyRings) {
@@ -126,6 +128,7 @@
    *     {@code null} if none was found.
    * @throws PGPException if an error occurred verifying the certification.
    */
+  @Nullable
   public static PGPPublicKey getSigner(
       Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, String userId, PGPPublicKey key)
       throws PGPException {
@@ -210,6 +213,7 @@
    * @throws PGPException if an error occurred parsing the key data.
    * @throws IOException if an error occurred reading the repository data.
    */
+  @Nullable
   public PGPPublicKeyRing get(byte[] fingerprint) throws PGPException, IOException {
     List<PGPPublicKeyRing> keyRings = get(Fingerprint.getId(fingerprint), fingerprint);
     return !keyRings.isEmpty() ? keyRings.get(0) : null;
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 17ca5a4..b9ff50b 100644
--- a/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -22,6 +22,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -176,6 +177,7 @@
     return CheckResult.ok();
   }
 
+  @Nullable
   private PGPSignature readSignature(PushCertificate cert) throws IOException {
     ArmoredInputStream in =
         new ArmoredInputStream(new ByteArrayInputStream(Constants.encode(cert.getSignature())));
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 3341806..d51ee6a 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -29,6 +29,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.StorageException;
@@ -299,6 +300,7 @@
     return externalIdKeyFactory.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
   }
 
+  @Nullable
   private Account getAccountByExternalId(ExternalId.Key extIdKey) {
     List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
 
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 5b62f96..9625039 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -116,6 +116,7 @@
     }
   }
 
+  @Nullable
   private static String readCookie(HttpServletRequest request) {
     Cookie[] all = request.getCookies();
     if (all != null) {
@@ -219,6 +220,7 @@
     }
   }
 
+  @Nullable
   @Override
   public String getSessionId() {
     return val != null ? val.getSessionId() : null;
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index fdbe6aa..86c514b 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -667,6 +668,7 @@
     public void destroy() {}
   }
 
+  @Nullable
   private static String getSessionIdOrNull(Provider<WebSession> sessionProvider) {
     WebSession session = sessionProvider.get();
     if (session.isSignedIn()) {
diff --git a/java/com/google/gerrit/httpd/HtmlDomUtil.java b/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 57f2664..881df46 100644
--- a/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -89,6 +90,7 @@
   }
 
   /** Find an element by its "id" attribute; null if no element is found. */
+  @Nullable
   public static Element find(Node parent, String name) {
     NodeList list = parent.getChildNodes();
     for (int i = 0; i < list.getLength(); i++) {
@@ -139,6 +141,7 @@
   }
 
   /** Parse an XHTML file from our CLASSPATH and return the instance. */
+  @Nullable
   public static Document parseFile(Class<?> context, String name) throws IOException {
     try (InputStream in = context.getResourceAsStream(name)) {
       if (in == null) {
@@ -168,6 +171,7 @@
   }
 
   /** Read a Read a UTF-8 text file from our CLASSPATH and return it. */
+  @Nullable
   public static String readFile(Class<?> context, String name) throws IOException {
     try (InputStream in = context.getResourceAsStream(name)) {
       if (in == null) {
@@ -180,6 +184,7 @@
   }
 
   /** Parse an XHTML file from the local drive and return the instance. */
+  @Nullable
   public static Document parseFile(Path path) throws IOException {
     try (InputStream in = Files.newInputStream(path)) {
       Document doc = newBuilder().parse(in);
@@ -193,6 +198,7 @@
   }
 
   /** Read a UTF-8 text file from the local drive. */
+  @Nullable
   public static String readFile(Path parentDir, String name) throws IOException {
     if (parentDir == null) {
       return null;
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index de6ae50..5a99cab 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -226,6 +227,7 @@
     }
   }
 
+  @Nullable
   private AuthInfo extractAuthInfo(String hdr, String encoding)
       throws UnsupportedEncodingException {
     byte[] decoded = BaseEncoding.base64().decode(hdr.substring(BASIC.length()));
@@ -241,6 +243,7 @@
         defaultAuthProvider);
   }
 
+  @Nullable
   private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
     String username =
         URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
@@ -272,6 +275,7 @@
     return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
   }
 
+  @Nullable
   private static Cookie findGitCookie(HttpServletRequest req) {
     Cookie[] cookies = req.getCookies();
     if (cookies != null) {
diff --git a/java/com/google/gerrit/httpd/RemoteUserUtil.java b/java/com/google/gerrit/httpd/RemoteUserUtil.java
index 84954dc..6f3e9c4 100644
--- a/java/com/google/gerrit/httpd/RemoteUserUtil.java
+++ b/java/com/google/gerrit/httpd/RemoteUserUtil.java
@@ -19,6 +19,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.io.BaseEncoding;
+import com.google.gerrit.common.Nullable;
 import javax.servlet.http.HttpServletRequest;
 
 public class RemoteUserUtil {
@@ -62,6 +63,7 @@
    * @param auth header value which is used for extracting.
    * @return username if available or null.
    */
+  @Nullable
   public static String extractUsername(String auth) {
     auth = emptyToNull(auth);
 
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index 87bf3a6..1137b65 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -30,6 +30,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
@@ -149,6 +150,7 @@
     return -1;
   }
 
+  @Nullable
   Val get(Key key) {
     Val val = self.getIfPresent(key.token);
     if (val != null && val.expiresAt <= nowMs()) {
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 2f760f0..bc8a01a 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -185,6 +186,7 @@
     return account.map(a -> new AuthResult(a.account().id(), null, false));
   }
 
+  @Nullable
   private AuthResult auth(Account.Id account) {
     if (account != null) {
       return new AuthResult(account, null, false);
@@ -192,6 +194,7 @@
     return null;
   }
 
+  @Nullable
   private AuthResult byUserName(String userName) {
     List<AccountState> accountStates = queryProvider.get().byExternalId(SCHEME_USERNAME, userName);
     if (accountStates.isEmpty()) {
@@ -223,6 +226,7 @@
     }
   }
 
+  @Nullable
   private AuthResult create() throws IOException {
     try {
       return accountManager.authenticate(
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index acb3282..be833ea 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -21,6 +21,7 @@
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.RemoteUserUtil;
@@ -143,6 +144,7 @@
         : remoteUser;
   }
 
+  @Nullable
   String getRemoteDisplayname(HttpServletRequest req) {
     if (displaynameHeader != null) {
       String raw = req.getHeader(displaynameHeader);
@@ -151,6 +153,7 @@
     return null;
   }
 
+  @Nullable
   String getRemoteEmail(HttpServletRequest req) {
     if (emailHeader != null) {
       return emptyToNull(req.getHeader(emailHeader));
@@ -158,6 +161,7 @@
     return null;
   }
 
+  @Nullable
   String getRemoteExternalIdToken(HttpServletRequest req) {
     if (externalIdHeader != null) {
       return emptyToNull(req.getHeader(externalIdHeader));
diff --git a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 0b6008c..e7057ad 100644
--- a/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -334,6 +334,7 @@
     form.appendChild(div);
   }
 
+  @Nullable
   private OAuthServiceProvider lookupOAuthServiceProvider(String providerId) {
     if (providerId.startsWith("http://")) {
       providerId = providerId.substring("http://".length());
@@ -350,6 +351,7 @@
     return null;
   }
 
+  @Nullable
   private static String getLastId(HttpServletRequest req) {
     Cookie[] cookies = req.getCookies();
     if (cookies != null) {
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index fcd16ae..0c71d68 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.auth.openid;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.entities.Account;
@@ -518,6 +519,7 @@
     rsp.sendRedirect(rdr.toString());
   }
 
+  @Nullable
   private State init(
       HttpServletRequest req,
       final String openidIdentifier,
diff --git a/java/com/google/gerrit/httpd/auth/restapi/BUILD b/java/com/google/gerrit/httpd/auth/restapi/BUILD
index d499768..9ab51c5 100644
--- a/java/com/google/gerrit/httpd/auth/restapi/BUILD
+++ b/java/com/google/gerrit/httpd/auth/restapi/BUILD
@@ -6,6 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/auth",
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java b/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java
index 3594c7c..2eee415 100644
--- a/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java
+++ b/java/com/google/gerrit/httpd/auth/restapi/GetOAuthToken.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.auth.oauth.OAuthTokenCache;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -70,6 +71,7 @@
     return Response.ok(accessTokenInfo);
   }
 
+  @Nullable
   private static String getHostName(String canonicalWebUrl) {
     if (canonicalWebUrl == null) {
       logger.atSevere().log(
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index a03aa36..9b8f4c6 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -35,6 +35,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.ByteStreams;
 import com.google.common.net.HttpHeaders;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.httpd.resources.ResourceKey;
 import com.google.gerrit.httpd.resources.SmallResource;
@@ -174,6 +175,7 @@
     plugins.put(name, holder);
   }
 
+  @Nullable
   private GuiceFilter load(Plugin plugin) {
     if (plugin.getHttpInjector() != null) {
       final String name = plugin.getName();
@@ -327,6 +329,7 @@
     }
   }
 
+  @Nullable
   private static Pattern makeAllowOrigin(Config cfg) {
     String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
     if (allow.length > 0) {
@@ -720,6 +723,7 @@
       this.docPrefix = getPrefix(plugin, "Gerrit-HttpDocumentationPrefix", "Documentation/");
     }
 
+    @Nullable
     private static String getPrefix(Plugin plugin, String attr, String def) {
       Path path = plugin.getSrcFile();
       PluginContentScanner scanner = plugin.getContentScanner();
diff --git a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
index fc0ec39..ed29629 100644
--- a/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/LfsPluginServlet.java
@@ -19,6 +19,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.resources.Resource;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.plugins.Plugin;
@@ -119,6 +120,7 @@
     filter.set(guiceFilter);
   }
 
+  @Nullable
   private GuiceFilter load(Plugin plugin) {
     if (plugin.getHttpInjector() != null) {
       final String name = plugin.getName();
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 1c6e058..5cf63d9 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -106,6 +106,7 @@
           ListChangesOption.SKIP_DIFFSTAT,
           ListChangesOption.SUBMIT_REQUIREMENTS);
 
+  @Nullable
   public static String getPath(@Nullable String requestedURL) throws URISyntaxException {
     if (requestedURL == null) {
       return null;
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 15dcf42..a8ff1ff 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -305,6 +305,7 @@
       sourceRoot = getSourceRootOrNull();
     }
 
+    @Nullable
     private static Path getSourceRootOrNull() {
       try {
         return GerritLauncher.resolveInSourceRoot(".");
@@ -313,6 +314,7 @@
       }
     }
 
+    @Nullable
     private FileSystem getDistributionArchive(File war) throws IOException {
       if (war == null) {
         return null;
@@ -320,6 +322,7 @@
       return GerritLauncher.getZipFileSystem(war.toPath());
     }
 
+    @Nullable
     private File getLauncherLoadedFrom() {
       File war;
       try {
@@ -441,6 +444,7 @@
       super(req);
     }
 
+    @Nullable
     @Override
     public String getPathInfo() {
       String uri = getRequestURI();
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 543e794..23de6db 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -315,6 +315,7 @@
       this.cancellationMetrics = cancellationMetrics;
     }
 
+    @Nullable
     private static Pattern makeAllowOrigin(Config cfg) {
       String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
       if (allow.length > 0) {
@@ -854,6 +855,7 @@
     }
   }
 
+  @Nullable
   private String getEtagWithRetry(
       HttpServletRequest req, TraceContext traceContext, RestResource.HasETag rsrc) {
     try (TraceTimer ignored =
@@ -1277,6 +1279,7 @@
     return ((ParameterizedType) supertype).getActualTypeArguments()[2];
   }
 
+  @Nullable
   private Object parseRequest(HttpServletRequest req, Type type)
       throws IOException, BadRequestException, SecurityException, IllegalArgumentException,
           NoSuchMethodException, IllegalAccessException, InstantiationException,
diff --git a/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java b/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
index 655f4ca..2065a31 100644
--- a/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
+++ b/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -125,6 +126,7 @@
       return cssFile.isStale() || headerFile.isStale() || footerFile.isStale();
     }
 
+    @Nullable
     private static Element readXml(FileInfo src) throws IOException {
       Document d = HtmlDomUtil.parseFile(src.path);
       return d != null ? d.getDocumentElement() : null;
diff --git a/java/com/google/gerrit/index/query/InternalQuery.java b/java/com/google/gerrit/index/query/InternalQuery.java
index 5c003bc..ef69b4c 100644
--- a/java/com/google/gerrit/index/query/InternalQuery.java
+++ b/java/com/google/gerrit/index/query/InternalQuery.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.FieldDef;
 import com.google.gerrit.index.Index;
@@ -118,6 +119,7 @@
     }
   }
 
+  @Nullable
   protected final Schema<T> schema() {
     Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
     return index != null ? index.getSchema() : null;
diff --git a/java/com/google/gerrit/index/query/LimitPredicate.java b/java/com/google/gerrit/index/query/LimitPredicate.java
index 23e0f6d..9196811 100644
--- a/java/com/google/gerrit/index/query/LimitPredicate.java
+++ b/java/com/google/gerrit/index/query/LimitPredicate.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.index.query;
 
+import com.google.gerrit.common.Nullable;
+
 public class LimitPredicate<T> extends IntPredicate<T> implements Matchable<T> {
   @SuppressWarnings("unchecked")
+  @Nullable
   public static Integer getLimit(String fieldName, Predicate<?> p) {
     IntPredicate<?> ip = QueryBuilder.find(p, IntPredicate.class, fieldName);
     return ip != null ? ip.intValue() : null;
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 086ad90..d18e9c3 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -151,6 +151,7 @@
       @Override
       public ResultSet<V> read() {
         return new ListResultSet<>(results) {
+          @Nullable
           @Override
           public Object searchAfter() {
             @Nullable V last = Iterables.getLast(results, null);
diff --git a/java/com/google/gerrit/json/EnumTypeAdapterFactory.java b/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
index 9c32aa8..b6cb5f9 100644
--- a/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
+++ b/java/com/google/gerrit/json/EnumTypeAdapterFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.json;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gson.Gson;
 import com.google.gson.JsonSyntaxException;
 import com.google.gson.TypeAdapter;
@@ -34,6 +35,7 @@
 public class EnumTypeAdapterFactory implements TypeAdapterFactory {
 
   @SuppressWarnings({"rawtypes", "unchecked"})
+  @Nullable
   @Override
   public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
     TypeAdapter<T> defaultEnumAdapter = TypeAdapters.ENUM_FACTORY.create(gson, typeToken);
diff --git a/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java b/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java
index d35b8fb..2557515 100644
--- a/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java
+++ b/java/com/google/gerrit/json/OptionalSubmitRequirementExpressionResultAdapterFactory.java
@@ -45,6 +45,7 @@
       TypeToken.get(SubmitRequirementExpressionResult.class);
 
   @SuppressWarnings({"unchecked"})
+  @Nullable
   @Override
   public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
     if (typeToken.equals(OPTIONAL_SR_EXPRESSION_RESULT_TOKEN)) {
diff --git a/java/com/google/gerrit/json/SqlTimestampDeserializer.java b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
index e1cf382..9aeda2b 100644
--- a/java/com/google/gerrit/json/SqlTimestampDeserializer.java
+++ b/java/com/google/gerrit/json/SqlTimestampDeserializer.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.json;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
@@ -30,6 +31,7 @@
 class SqlTimestampDeserializer implements JsonDeserializer<Timestamp>, JsonSerializer<Timestamp> {
   private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
 
+  @Nullable
   @Override
   public Timestamp deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
       throws JsonParseException {
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index a190ebf..6be78d9 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -533,6 +533,7 @@
     return myHome;
   }
 
+  @SuppressWarnings("ReturnMissingNullable")
   private static File tmproot() {
     File tmp;
     String gerritTemp = System.getenv("GERRIT_TMP");
@@ -572,6 +573,7 @@
     }
   }
 
+  @SuppressWarnings("ReturnMissingNullable")
   private static File locateHomeDirectory() {
     // Try to find the user's home directory. If we can't find it
     // return null so the JVM's default temporary directory is used
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 755ae7b..079380e 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -33,6 +33,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.converter.ChangeProtoConverter;
@@ -470,6 +471,7 @@
      * @param subIndex change sub-index
      * @return the score doc that can be used to page result sets
      */
+    @Nullable
     private ScoreDoc getSearchAfter(ChangeSubIndex subIndex) {
       if (isSearchAfterPagination
           && opts.searchAfter() != null
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index d475ab7..b741042 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.index.group.GroupField.UUID_FIELD_SPEC;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.InternalGroup;
 import com.google.gerrit.exceptions.StorageException;
@@ -151,6 +152,7 @@
         new Sort(new SortField(UUID_SORT_FIELD, SortField.Type.STRING, false)));
   }
 
+  @Nullable
   @Override
   protected InternalGroup fromDocument(Document doc) {
     AccountGroup.UUID uuid =
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 96b22db..6b2b693 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.index.project.ProjectField.NAME;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.QueryOptions;
@@ -151,6 +152,7 @@
         new Sort(new SortField(NAME_SORT_FIELD, SortField.Type.STRING, false)));
   }
 
+  @Nullable
   @Override
   protected ProjectData fromDocument(Document doc) {
     Project.NameKey nameKey = Project.nameKey(doc.getField(NAME.getName()).stringValue());
diff --git a/java/com/google/gerrit/lucene/LuceneStoredValue.java b/java/com/google/gerrit/lucene/LuceneStoredValue.java
index 1f8c039..58ae3e0 100644
--- a/java/com/google/gerrit/lucene/LuceneStoredValue.java
+++ b/java/com/google/gerrit/lucene/LuceneStoredValue.java
@@ -38,6 +38,7 @@
     this.field = field;
   }
 
+  @Nullable
   @Override
   public String asString() {
     return Iterables.getFirst(asStrings(), null);
@@ -48,6 +49,7 @@
     return field.stream().map(f -> f.stringValue()).collect(toImmutableList());
   }
 
+  @Nullable
   @Override
   public Integer asInteger() {
     return Iterables.getFirst(asIntegers(), null);
@@ -58,6 +60,7 @@
     return field.stream().map(f -> f.numericValue().intValue()).collect(toImmutableList());
   }
 
+  @Nullable
   @Override
   public Long asLong() {
     return Iterables.getFirst(asLongs(), null);
@@ -68,11 +71,13 @@
     return field.stream().map(f -> f.numericValue().longValue()).collect(toImmutableList());
   }
 
+  @Nullable
   @Override
   public Timestamp asTimestamp() {
     return asLong() == null ? null : new Timestamp(asLong());
   }
 
+  @Nullable
   @Override
   public byte[] asByteArray() {
     return Iterables.getFirst(asByteArrays(), null);
diff --git a/java/com/google/gerrit/lucene/WrappableSearcherManager.java b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
index c164b29..56cb220 100644
--- a/java/com/google/gerrit/lucene/WrappableSearcherManager.java
+++ b/java/com/google/gerrit/lucene/WrappableSearcherManager.java
@@ -17,6 +17,7 @@
  * limitations under the License.
  */
 
+import com.google.gerrit.common.Nullable;
 import java.io.IOException;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.FilterDirectoryReader;
@@ -132,6 +133,7 @@
     reference.getIndexReader().decRef();
   }
 
+  @Nullable
   @Override
   protected IndexSearcher refreshIfNeeded(IndexSearcher referenceToRefresh) throws IOException {
     final IndexReader r = referenceToRefresh.getIndexReader();
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index 5f4e0c0..0f80a0c 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
index d59a1d9..27e9377 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
@@ -23,6 +23,7 @@
 import com.codahale.metrics.Timer;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
 import java.util.ArrayList;
@@ -144,6 +145,7 @@
     }
   }
 
+  @Nullable
   private static Boolean toBool(ImmutableMap<String, String> atts, String key) {
     return Description.TRUE_VALUE.equals(atts.get(key)) ? true : null;
   }
diff --git a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
index ef0ced6..84f2320 100644
--- a/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
+++ b/java/com/google/gerrit/metrics/proc/OperatingSystemMXBeanFactory.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.metrics.proc;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.sun.management.UnixOperatingSystemMXBean;
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
@@ -25,6 +26,7 @@
 
   private OperatingSystemMXBeanFactory() {}
 
+  @Nullable
   static OperatingSystemMXBeanInterface create() {
     OperatingSystemMXBean sys = ManagementFactory.getOperatingSystemMXBean();
     if (sys instanceof UnixOperatingSystemMXBean) {
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 824a9a7..6dec2d8 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.SiteLibraryLoaderUtil;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.config.SitePaths;
@@ -185,6 +186,7 @@
     }
   }
 
+  @Nullable
   private Path findJarWithSecureStore(SitePaths sitePaths, String secureStoreClass)
       throws IOException {
     List<Path> jars = SiteLibraryLoaderUtil.listJars(sitePaths.lib_dir);
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index 4592cbb..b59b924 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -22,6 +22,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.IndexType;
 import com.google.gerrit.metrics.DisabledMetricMaker;
@@ -175,6 +176,7 @@
    */
   protected void afterInit(SiteRun run) throws Exception {}
 
+  @Nullable
   protected List<String> getInstallPlugins() {
     try {
       if (pluginsToInstall != null && pluginsToInstall.isEmpty()) {
@@ -304,6 +306,7 @@
     return ConsoleUI.getInstance(false);
   }
 
+  @Nullable
   private SecureStoreInitData discoverSecureStoreClass() {
     String secureStore = getSecureStoreLib();
     if (Strings.isNullOrEmpty(secureStore)) {
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 4e854b5..3dce974 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.InternalGroup;
@@ -182,6 +183,7 @@
     return email;
   }
 
+  @Nullable
   private AccountSshKey readSshKey(Account.Id id) throws IOException {
     String defaultPublicSshKeyFile = "";
     Path defaultPublicSshKeyPath = Paths.get(System.getProperty("user.home"), ".ssh", "id_rsa.pub");
diff --git a/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index a7f9c5d..16c4ce7 100644
--- a/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -62,6 +63,7 @@
     return pluginsInitSteps;
   }
 
+  @Nullable
   private InitStep loadInitStep(Path jar) {
     try {
       URLClassLoader pluginLoader =
diff --git a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index dffdde7..7666076 100644
--- a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -17,6 +17,7 @@
 import com.google.errorprone.annotations.FormatMethod;
 import com.google.errorprone.annotations.FormatString;
 import com.google.gerrit.common.Die;
+import com.google.gerrit.common.Nullable;
 import java.io.Console;
 import java.util.EnumSet;
 import java.util.Set;
@@ -179,6 +180,7 @@
 
     @Override
     @FormatMethod
+    @Nullable
     public String password(String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (; ; ) {
diff --git a/java/com/google/gerrit/pgm/init/api/InitUtil.java b/java/com/google/gerrit/pgm/init/api/InitUtil.java
index d038de7..7688728 100644
--- a/java/com/google/gerrit/pgm/init/api/InitUtil.java
+++ b/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -18,6 +18,7 @@
 
 import com.google.common.io.ByteStreams;
 import com.google.gerrit.common.Die;
+import com.google.gerrit.common.Nullable;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -127,6 +128,7 @@
     }
   }
 
+  @Nullable
   private static InputStream open(Class<?> sibling, String name) {
     final InputStream in = sibling.getResourceAsStream(name);
     if (in == null) {
diff --git a/java/com/google/gerrit/pgm/init/api/Section.java b/java/com/google/gerrit/pgm/init/api/Section.java
index b5d35f4..5cc4b5d 100644
--- a/java/com/google/gerrit/pgm/init/api/Section.java
+++ b/java/com/google/gerrit/pgm/init/api/Section.java
@@ -166,6 +166,7 @@
     return nv;
   }
 
+  @Nullable
   public String password(String username, String password) {
     final String ov = getSecure(password);
 
diff --git a/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 0a41db5..de08116 100644
--- a/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.rules;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -182,6 +183,7 @@
     }
   }
 
+  @Nullable
   private String getMyClasspath() {
     StringBuilder cp = new StringBuilder();
     appendClasspath(cp, getClass().getClassLoader());
diff --git a/java/com/google/gerrit/prettify/BUILD b/java/com/google/gerrit/prettify/BUILD
index 0a15fda..a5c8b77 100644
--- a/java/com/google/gerrit/prettify/BUILD
+++ b/java/com/google/gerrit/prettify/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["common/**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
         "//lib:jgit",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/prettify/common/SparseFileContent.java b/java/com/google/gerrit/prettify/common/SparseFileContent.java
index 1249b65..f40222a 100644
--- a/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 
 /**
  * A class to store subset of a file's lines in a memory efficient way. Internally, it stores lines
@@ -134,6 +135,7 @@
       return getSize();
     }
 
+    @Nullable
     private String getLine(int idx) {
       // Most requests are sequential in nature, fetching the next
       // line from the current range, or the next range.
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 8198ce4..285657e 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -104,6 +104,7 @@
     return PatchSet.id(changeId, comment.key.patchSetId);
   }
 
+  @Nullable
   public static String extractMessageId(@Nullable String tag) {
     if (tag == null || !tag.startsWith("mailMessageId=")) {
       return null;
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index cfecd8e..9ac85e0 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -79,6 +79,7 @@
   public abstract static class StarField {
     private static final String SEPARATOR = ":";
 
+    @Nullable
     public static StarField parse(String s) {
       int p = s.indexOf(SEPARATOR);
       if (p >= 0) {
diff --git a/java/com/google/gerrit/server/account/AccountLimits.java b/java/com/google/gerrit/server/account/AccountLimits.java
index 5549d28..d97563a 100644
--- a/java/com/google/gerrit/server/account/AccountLimits.java
+++ b/java/com/google/gerrit/server/account/AccountLimits.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.PermissionRange;
 import com.google.gerrit.entities.PermissionRule;
@@ -105,6 +106,7 @@
   }
 
   /** The range of permitted values associated with a label permission. */
+  @Nullable
   public PermissionRange getRange(String permission) {
     if (GlobalCapability.hasRange(permission)) {
       return toRange(permission, getRules(permission));
diff --git a/java/com/google/gerrit/server/account/AuthRequest.java b/java/com/google/gerrit/server/account/AuthRequest.java
index cceda70..b4fbcdb 100644
--- a/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/java/com/google/gerrit/server/account/AuthRequest.java
@@ -102,6 +102,7 @@
     return externalId;
   }
 
+  @Nullable
   public String getLocalUser() {
     if (externalId.isScheme(SCHEME_GERRIT)) {
       return externalId.id();
diff --git a/java/com/google/gerrit/server/account/CreateGroupArgs.java b/java/com/google/gerrit/server/account/CreateGroupArgs.java
index ba58c3f..9d9fe9d 100644
--- a/java/com/google/gerrit/server/account/CreateGroupArgs.java
+++ b/java/com/google/gerrit/server/account/CreateGroupArgs.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import java.util.Collection;
@@ -30,6 +31,7 @@
     return groupName;
   }
 
+  @Nullable
   public String getGroupName() {
     return groupName != null ? groupName.get() : null;
   }
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index 329825f..cfffceb 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -16,6 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.AccountFieldName;
@@ -79,6 +80,7 @@
   @Override
   public void onCreateAccount(AuthRequest who, Account account) {}
 
+  @Nullable
   @Override
   public Account.Id lookup(String accountName) throws IOException {
     if (emailExpander.canExpand(accountName)) {
diff --git a/java/com/google/gerrit/server/account/DestinationList.java b/java/com/google/gerrit/server/account/DestinationList.java
index 15c1e25..084a3ac 100644
--- a/java/com/google/gerrit/server/account/DestinationList.java
+++ b/java/com/google/gerrit/server/account/DestinationList.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
@@ -39,6 +40,7 @@
     destinations.replaceValues(label, toSet(parse(text, DIR_NAME + label, TRIM, null, errors)));
   }
 
+  @Nullable
   String asText(String label) {
     Set<BranchNameKey> dests = destinations.get(label);
     if (dests == null) {
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index eaec9ba..2d947ba 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.InternalGroup;
 import com.google.gerrit.entities.RefNames;
@@ -346,6 +347,7 @@
       return Protos.toByteArray(InternalGroupSerializer.serialize(value));
     }
 
+    @Nullable
     @Override
     public InternalGroup deserialize(byte[] in) {
       if (Strings.fromByteArray(in).isEmpty()) {
diff --git a/java/com/google/gerrit/server/account/InternalGroupBackend.java b/java/com/google/gerrit/server/account/InternalGroupBackend.java
index 91fe701..01254a0 100644
--- a/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.GroupReference;
@@ -60,6 +61,7 @@
     return ObjectId.isId(uuid.get()); // [0-9a-f]{40};
   }
 
+  @Nullable
   @Override
   public GroupDescription.Internal get(AccountGroup.UUID uuid) {
     if (!handles(uuid)) {
diff --git a/java/com/google/gerrit/server/account/ProjectWatches.java b/java/com/google/gerrit/server/account/ProjectWatches.java
index 42137c1..86132d3 100644
--- a/java/com/google/gerrit/server/account/ProjectWatches.java
+++ b/java/com/google/gerrit/server/account/ProjectWatches.java
@@ -201,6 +201,7 @@
 
   @AutoValue
   public abstract static class NotifyValue {
+    @Nullable
     public static NotifyValue parse(
         Account.Id accountId,
         String project,
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 1587bc5..476ca79 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -130,6 +130,7 @@
     return true;
   }
 
+  @Nullable
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     if (uuid == null) {
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 555a2c1..1fce3d5 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -20,6 +20,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.InvalidSshKeyException;
@@ -194,6 +195,7 @@
    * @return the SSH key, <code>null</code> if there is no SSH key with this sequence number, or if
    *     the SSH key with this sequence number has been deleted
    */
+  @Nullable
   private AccountSshKey getKey(int seq) {
     checkLoaded();
     return keys.get(seq - 1).orElse(null);
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index b0618ba..48c403c 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -1020,6 +1020,7 @@
    * @return the external ID that was removed, {@code null} if no external ID with the specified key
    *     exists
    */
+  @Nullable
   private ExternalId remove(
       RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
       throws IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index b23782f..828f868 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.accounts.AgreementInput;
@@ -575,6 +576,7 @@
     }
   }
 
+  @Nullable
   @Override
   public String generateHttpPassword() throws RestApiException {
     HttpPasswordInput input = new HttpPasswordInput();
@@ -589,6 +591,7 @@
     }
   }
 
+  @Nullable
   @Override
   public String setHttpPassword(String password) throws RestApiException {
     HttpPasswordInput input = new HttpPasswordInput();
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 1713171..ac63135 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -583,6 +583,7 @@
     }
   }
 
+  @Nullable
   @Override
   public AccountInfo getAssignee() throws RestApiException {
     try {
@@ -602,6 +603,7 @@
     }
   }
 
+  @Nullable
   @Override
   public AccountInfo deleteAssignee() throws RestApiException {
     try {
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index 8014e17..bd31356f 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -36,6 +36,7 @@
 import com.google.common.collect.Multimaps;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
@@ -724,6 +725,7 @@
     return filterApprovals(byPatchSet(notes, psId), accountId);
   }
 
+  @Nullable
   public PatchSetApproval getSubmitter(ChangeNotes notes, PatchSet.Id c) {
     if (c == null) {
       return null;
@@ -736,6 +738,7 @@
     }
   }
 
+  @Nullable
   public static PatchSetApproval getSubmitter(PatchSet.Id c, Iterable<PatchSetApproval> approvals) {
     if (c == null) {
       return null;
diff --git a/java/com/google/gerrit/server/cache/CacheInfo.java b/java/com/google/gerrit/server/cache/CacheInfo.java
index d6eb065..94a9e05 100644
--- a/java/com/google/gerrit/server/cache/CacheInfo.java
+++ b/java/com/google/gerrit/server/cache/CacheInfo.java
@@ -16,6 +16,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheStats;
+import com.google.gerrit.common.Nullable;
 
 public class CacheInfo {
 
@@ -53,6 +54,7 @@
     }
   }
 
+  @Nullable
   private static String duration(double ns) {
     if (ns < 0.5) {
       return null;
@@ -118,6 +120,7 @@
       disk = percent(value, total);
     }
 
+    @Nullable
     private static Integer percent(long value, long total) {
       if (total <= 0) {
         return null;
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java b/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
index ec527ba..e9b254b 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
@@ -18,6 +18,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import java.io.IOException;
@@ -80,6 +81,7 @@
     return !diskEnabled || diskLimit <= 0;
   }
 
+  @Nullable
   private static Path getCacheDir(SitePaths site, String name) {
     if (name == null) {
       return null;
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index aa62745..b744058 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -47,6 +47,7 @@
     return source.refreshAfterWrite();
   }
 
+  @Nullable
   @Override
   public Weigher<K, V> weigher() {
     Weigher<K, V> weigher = source.weigher();
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 0403408..8327b88 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -103,6 +103,7 @@
     this.mem = mem;
   }
 
+  @Nullable
   @Override
   public V getIfPresent(Object objKey) {
     if (!keyType.getRawType().isInstance(objKey)) {
@@ -423,6 +424,7 @@
       return b == null || b.mightContain(key);
     }
 
+    @Nullable
     private BloomFilter<K> buildBloomFilter() {
       SqlHandle c = null;
       try {
@@ -472,6 +474,7 @@
       }
     }
 
+    @Nullable
     ValueHolder<V> getIfPresent(K key) {
       SqlHandle c = null;
       try {
@@ -717,6 +720,7 @@
       }
     }
 
+    @Nullable
     private SqlHandle close(SqlHandle h) {
       if (h != null) {
         h.close();
@@ -776,6 +780,7 @@
       }
     }
 
+    @Nullable
     private PreparedStatement closeStatement(PreparedStatement ps) {
       if (ps != null) {
         try {
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 63e2c08..ed6d53d 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -106,6 +106,7 @@
     to.actions = toActionMap(rsrc, visitors, changeInfo, copy(visitors, to));
   }
 
+  @Nullable
   private ChangeInfo copy(List<ActionVisitor> visitors, ChangeInfo changeInfo) {
     if (visitors.isEmpty()) {
       return null;
@@ -152,6 +153,7 @@
     return copy;
   }
 
+  @Nullable
   private RevisionInfo copy(List<ActionVisitor> visitors, RevisionInfo revisionInfo) {
     if (visitors.isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index a041ff7..d575324 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -30,6 +30,7 @@
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
@@ -409,6 +410,7 @@
     return this;
   }
 
+  @Nullable
   public String getChangeMessage() {
     if (message == null) {
       return null;
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 0775647..38efc44 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -764,6 +764,7 @@
     return serverIdent.get();
   }
 
+  @Nullable
   private RevCommit parseCommit(ObjectId objId, String desc) {
     try {
       return rw.parseCommit(objId);
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
index 0f9194f..d9c30d7 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
@@ -42,6 +42,7 @@
     this.diffs = diffOperations;
   }
 
+  @Nullable
   @Override
   public Map<String, FileInfo> getFileInfoMap(
       Change change, ObjectId objectId, @Nullable PatchSet base)
@@ -63,6 +64,7 @@
     }
   }
 
+  @Nullable
   @Override
   public Map<String, FileInfo> getFileInfoMap(
       Project.NameKey project, ObjectId objectId, int parent)
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 69a84dd8..cfa15ae 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -80,6 +80,7 @@
    * lazily populate accounts. Callers have to call {@link AccountLoader#fill()} afterwards to
    * populate all accounts in the returned {@link LabelInfo}s.
    */
+  @Nullable
   Map<String, LabelInfo> labelsFor(
       AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
       throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 2d36df2..ba938ee 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -71,6 +72,7 @@
 
   @AutoValue
   public abstract static class Base {
+    @Nullable
     private static Base create(ChangeNotes notes, PatchSet ps) {
       if (notes == null) {
         return null;
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 496808a..d581675 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.CoreDownloadSchemes;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
 import com.google.gerrit.server.change.ArchiveFormatInternal;
@@ -87,6 +88,7 @@
     return list.size() == 1 && list.get(0) == null;
   }
 
+  @Nullable
   private static String toCoreScheme(String s) {
     try {
       Field f = CoreDownloadSchemes.class.getField(s.toUpperCase());
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index c477bb5..028e172 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -99,6 +99,7 @@
     return values.length > 0 && isNullOrEmpty(values[0]);
   }
 
+  @Nullable
   private static GitwebType typeFromConfig(Config cfg) {
     GitwebType defaultType = defaultType(cfg.getString("gitweb", null, "type"));
     if (defaultType == null) {
@@ -136,6 +137,7 @@
     return type;
   }
 
+  @Nullable
   private static GitwebType defaultType(String typeName) {
     GitwebType type = new GitwebType();
     switch (nullToEmpty(typeName)) {
@@ -283,6 +285,7 @@
       this.tag = parse(type.getTag());
     }
 
+    @Nullable
     @Override
     public WebLinkInfo getBranchWebLink(String projectName, String branchName) {
       if (branch != null) {
@@ -295,6 +298,7 @@
       return null;
     }
 
+    @Nullable
     @Override
     public WebLinkInfo getTagWebLink(String projectName, String tagName) {
       if (tag != null) {
@@ -304,6 +308,7 @@
       return null;
     }
 
+    @Nullable
     @Override
     public WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName) {
       if (fileHistory != null) {
@@ -317,6 +322,7 @@
       return null;
     }
 
+    @Nullable
     @Override
     public WebLinkInfo getFileWebLink(
         String projectName, String revision, String hash, String fileName) {
@@ -331,6 +337,7 @@
       return null;
     }
 
+    @Nullable
     @Override
     public WebLinkInfo getPatchSetWebLink(
         String projectName, String commit, String commitMessage, String branchName) {
@@ -359,6 +366,7 @@
       return getPatchSetWebLink(projectName, commit, commitMessage, branchName);
     }
 
+    @Nullable
     @Override
     public WebLinkInfo getProjectWeblink(String projectName) {
       if (project != null) {
@@ -378,6 +386,7 @@
       return new WebLinkInfo(type.getLinkName(), null, url + rest, null);
     }
 
+    @Nullable
     private static ParameterizedString parse(String pattern) {
       if (!isNullOrEmpty(pattern)) {
         return new ParameterizedString(pattern);
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index c09988e3..e11d6aa 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
@@ -360,6 +361,7 @@
       }
     }
 
+    @Nullable
     private ProjectConfig parseConfig(Project.NameKey p, String idStr)
         throws IOException, ConfigInvalidException, RepositoryNotFoundException {
       ObjectId id = ObjectId.fromString(idStr);
@@ -382,14 +384,17 @@
     }
   }
 
+  @Nullable
   private static Boolean toBoolean(String value) {
     return value != null ? Boolean.parseBoolean(value) : null;
   }
 
+  @Nullable
   private static Integer toInt(String value) {
     return value != null ? Integer.parseInt(value) : null;
   }
 
+  @Nullable
   private static Long toLong(String value) {
     return value != null ? Long.parseLong(value) : null;
   }
diff --git a/java/com/google/gerrit/server/config/RepositoryConfig.java b/java/com/google/gerrit/server/config/RepositoryConfig.java
index f722321..d569c87 100644
--- a/java/com/google/gerrit/server/config/RepositoryConfig.java
+++ b/java/com/google/gerrit/server/config/RepositoryConfig.java
@@ -55,6 +55,7 @@
         cfg.getStringList(SECTION_NAME, findSubSection(project.get()), OWNER_GROUP_NAME));
   }
 
+  @Nullable
   public Path getBasePath(Project.NameKey project) {
     String basePath = cfg.getString(SECTION_NAME, findSubSection(project.get()), BASE_PATH_NAME);
     return basePath != null ? Paths.get(basePath) : null;
diff --git a/java/com/google/gerrit/server/config/SitePaths.java b/java/com/google/gerrit/server/config/SitePaths.java
index 5e268da..2cd24ba 100644
--- a/java/com/google/gerrit/server/config/SitePaths.java
+++ b/java/com/google/gerrit/server/config/SitePaths.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -140,6 +141,7 @@
    * @param path the path string to resolve. May be null.
    * @return the resolved path; null if {@code path} was null or empty.
    */
+  @Nullable
   public Path resolve(String path) {
     if (path != null && !path.isEmpty()) {
       Path loc = site_path.resolve(path).normalize();
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index c2c0b05..0e911b9 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -21,6 +21,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.vladsch.flexmark.ast.Heading;
 import com.vladsch.flexmark.ast.util.TextCollectingVisitor;
 import com.vladsch.flexmark.html.HtmlRenderer;
@@ -126,6 +127,7 @@
     return findTitle(parseMarkdown(md));
   }
 
+  @Nullable
   private String findTitle(Node root) {
     if (root instanceof Heading) {
       Heading h = (Heading) root;
diff --git a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index 7d3ddf1..cd49ea6 100644
--- a/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -99,6 +100,7 @@
     }
   }
 
+  @Nullable
   protected Directory readIndexDirectory() throws IOException {
     Directory dir = new ByteBuffersDirectory();
     byte[] buffer = new byte[4096];
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 95f6d96..60e30bc 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
@@ -523,6 +524,7 @@
   }
 
   /** Create an AuthorAttribute for the given account suitable for serialization to JSON. */
+  @Nullable
   public AccountAttribute asAccountAttribute(Account.Id id) {
     if (id == null) {
       return null;
@@ -590,6 +592,7 @@
   }
 
   /** Get a link to the change; null if the server doesn't know its own address. */
+  @Nullable
   private String getChangeUrl(Change change) {
     if (change != null) {
       return urlFormatter.get().getChangeViewUrl(change.getProject(), change.getId()).orElse(null);
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index afe2a7c..18f3d7a 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Suppliers;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
@@ -234,6 +235,7 @@
         });
   }
 
+  @Nullable
   String[] hashtagArray(Collection<String> hashtags) {
     if (hashtags != null && !hashtags.isEmpty()) {
       return Sets.newHashSet(hashtags).toArray(new String[hashtags.size()]);
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index b669571..7c8777f 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -99,6 +99,7 @@
     return revisionJsonFactory.create(changeOptions).getRevisionInfo(cd, ps);
   }
 
+  @Nullable
   public AccountInfo accountInfo(@Nullable AccountState accountState) {
     if (accountState == null || accountState.account().id() == null) {
       return null;
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index 5bbe5e2..455b221 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.SortedSetMultimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.PatchSetUtil;
@@ -258,6 +259,7 @@
     return actual;
   }
 
+  @Nullable
   private ObjectId parseGroup(ObjectId forCommit, String group) {
     try {
       return ObjectId.fromString(group);
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index ae247ad..6922efb 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -1027,6 +1027,7 @@
     }
   }
 
+  @Nullable
   public static CodeReviewCommit findAnyMergedInto(
       CodeReviewRevWalk rw, Iterable<CodeReviewCommit> commits, CodeReviewCommit tip)
       throws IOException {
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index f66a089..fb34753 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -82,6 +82,7 @@
     throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
   }
 
+  @Nullable
   @Override
   public Ref exactRef(String name) throws IOException {
     Ref ref = getDelegate().getRefDatabase().exactRef(name);
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index f516a0c..e8b7c62 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.CaseFormat;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
@@ -241,6 +242,7 @@
   }
 
   /** Locate a task by its unique id, null if no task matches. */
+  @Nullable
   public Task<?> getTask(int id) {
     Task<?> result = null;
     for (Executor e : queues) {
@@ -256,6 +258,7 @@
     return result;
   }
 
+  @Nullable
   public ScheduledThreadPoolExecutor getExecutor(String queueName) {
     for (Executor e : queues) {
       if (e.queueName.equals(queueName)) {
diff --git a/java/com/google/gerrit/server/git/meta/TabFile.java b/java/com/google/gerrit/server/git/meta/TabFile.java
index 80570a5..5f76b39 100644
--- a/java/com/google/gerrit/server/git/meta/TabFile.java
+++ b/java/com/google/gerrit/server/git/meta/TabFile.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.git.ValidationError;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -84,6 +85,7 @@
     return map;
   }
 
+  @Nullable
   protected static String asText(String left, String right, Map<String, String> entries) {
     if (entries.isEmpty()) {
       return null;
@@ -96,6 +98,7 @@
     return asText(left, right, rows);
   }
 
+  @Nullable
   protected static String asText(String left, String right, List<Row> rows) {
     if (rows.isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index ae7c3a10..6e5cfff 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -441,6 +441,7 @@
         update, message.toString(), ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
   }
 
+  @Nullable
   private String changeKindMessage(ChangeKind changeKind) {
     switch (changeKind) {
       case MERGE_FIRST_PARENT_UPDATE:
@@ -624,6 +625,7 @@
     return cmd;
   }
 
+  @Nullable
   private static String findMergedInto(Context ctx, String first, RevCommit commit) {
     try {
       RevWalk rw = ctx.getRevWalk();
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index 546614c..001a153 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.GroupReference;
@@ -84,6 +85,7 @@
    * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
    * @return the group, null if no group is found for the given group ID
    */
+  @Nullable
   public GroupDescription.Basic parseId(String id) {
     logger.atFine().log("Parsing group %s", id);
 
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 5a9b9e5..0471acc 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -21,6 +21,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.GroupReference;
@@ -140,6 +141,7 @@
     return isSystemGroup(uuid);
   }
 
+  @Nullable
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     final GroupReference ref = uuids.get(uuid);
@@ -157,11 +159,13 @@
         return ref.getUUID();
       }
 
+      @Nullable
       @Override
       public String getUrl() {
         return null;
       }
 
+      @Nullable
       @Override
       public String getEmailAddress() {
         return null;
diff --git a/java/com/google/gerrit/server/group/testing/BUILD b/java/com/google/gerrit/server/group/testing/BUILD
index 77bb777..bb2b20d 100644
--- a/java/com/google/gerrit/server/group/testing/BUILD
+++ b/java/com/google/gerrit/server/group/testing/BUILD
@@ -7,6 +7,7 @@
     testonly = True,
     srcs = glob(["*.java"]),
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//lib:guava",
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
index 12d8c93..d81992a 100644
--- a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -18,6 +18,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
@@ -111,6 +112,7 @@
     return false;
   }
 
+  @Nullable
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
     return uuid == null ? null : groups.get(uuid);
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index a39cbe5..03f4c50 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -26,6 +26,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.SiteIndexer;
@@ -179,6 +180,7 @@
     return Result.create(sw, ok.get(), nDone, nFailed);
   }
 
+  @Nullable
   public Callable<Void> reindexProject(
       ChangeIndexer indexer, Project.NameKey project, Task done, Task failed) {
     try (Repository repo = repoManager.openRepository(project)) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 670f472a..68d3a3e 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -1393,6 +1393,7 @@
               },
               (cd, field) -> cd.setRefStatePatterns(field));
 
+  @Nullable
   private static String getTopic(ChangeData cd) {
     Change c = cd.change();
     if (c == null) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index a1756bf..973d691 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -184,6 +184,7 @@
    * @throws QueryParseException if the underlying index implementation does not support this
    *     predicate.
    */
+  @Nullable
   private Predicate<ChangeData> rewriteImpl(
       Predicate<ChangeData> in, ChangeIndex index, QueryOptions opts, MutableInteger leafTerms)
       throws QueryParseException {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 6849831..4b88919 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -407,6 +407,7 @@
       return future;
     }
 
+    @Nullable
     @Override
     public ChangeData callImpl() throws Exception {
       // Remove this task from queuedIndexTasks. This is done right at the beginning of this task so
@@ -460,6 +461,7 @@
       this.id = id;
     }
 
+    @Nullable
     @Override
     public ChangeData call() {
       logger.atFine().log("Delete change %d from index.", id.get());
diff --git a/java/com/google/gerrit/server/ioutil/BUILD b/java/com/google/gerrit/server/ioutil/BUILD
index fd0c4f1..6b9ecdf 100644
--- a/java/com/google/gerrit/server/ioutil/BUILD
+++ b/java/com/google/gerrit/server/ioutil/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//lib:automaton",
         "//lib:guava",
diff --git a/java/com/google/gerrit/server/ioutil/BasicSerialization.java b/java/com/google/gerrit/server/ioutil/BasicSerialization.java
index 296cf22..b43655a 100644
--- a/java/com/google/gerrit/server/ioutil/BasicSerialization.java
+++ b/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -31,6 +31,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.CodedEnum;
 import java.io.EOFException;
 import java.io.IOException;
@@ -129,6 +130,7 @@
   }
 
   /** Read a UTF-8 string, prefixed by its byte length in a varint. */
+  @Nullable
   public static String readString(InputStream input) throws IOException {
     final byte[] bin = readBytes(input);
     if (bin.length == 0) {
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 652766a..6fe3cbe 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.server.IdentifiedUser;
@@ -111,10 +112,12 @@
     return "Unknown";
   }
 
+  @Nullable
   private String getSshKey() {
     return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
   }
 
+  @Nullable
   private String getGpgKeys() {
     if (gpgKeys != null) {
       return Joiner.on("\n").join(gpgKeys);
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 3c821cc..79696fe 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -87,16 +87,19 @@
     public List<Comment> comments = new ArrayList<>();
 
     /** Returns a web link to a comment for a change. */
+    @Nullable
     public String getCommentLink(String uuid) {
       return args.urlFormatter.get().getInlineCommentView(change, uuid).orElse(null);
     }
 
     /** Returns a web link to the comment tab view of a change. */
+    @Nullable
     public String getCommentsTabLink() {
       return args.urlFormatter.get().getCommentsTabView(change).orElse(null);
     }
 
     /** Returns a web link to the findings tab view of a change. */
+    @Nullable
     public String getFindingsTabLink() {
       return args.urlFormatter.get().getFindingsTabView(change).orElse(null);
     }
@@ -505,6 +508,7 @@
     return false;
   }
 
+  @Nullable
   private Repository getRepository() {
     try {
       return args.server.openRepository(projectState.getNameKey());
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index d6d306c..64a01ff 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.server.IdentifiedUser;
@@ -109,10 +110,12 @@
     throw new IllegalStateException("key type is not SSH or GPG");
   }
 
+  @Nullable
   private String getSshKey() {
     return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
   }
 
+  @Nullable
   private String getGpgKeyFingerprints() {
     if (!gpgKeyFingerprints.isEmpty()) {
       return Joiner.on("\n").join(gpgKeyFingerprints);
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index 70676e3..bd79d3a 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
@@ -73,6 +74,7 @@
     }
   }
 
+  @Nullable
   public List<String> getReviewerNames() {
     if (reviewers.isEmpty() && reviewersByEmail.isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index e899fc5..800066e 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
@@ -96,6 +97,7 @@
     }
   }
 
+  @Nullable
   public List<String> getReviewerNames() {
     if (reviewers.isEmpty()) {
       return null;
@@ -107,6 +109,7 @@
     return names;
   }
 
+  @Nullable
   public List<String> getRemovedReviewerNames() {
     if (removedReviewers.isEmpty() && removedByEmailReviewers.isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 5b209ce..f023075 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -17,6 +17,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BranchNameKey;
@@ -92,6 +93,7 @@
 
   protected abstract void addWatcher(RecipientType type, Account.Id to);
 
+  @Nullable
   public String getSshHost() {
     String host = Iterables.getFirst(args.sshAddresses, null);
     if (host == null) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index bfc1f5b..55f82d4 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -380,10 +380,12 @@
     return SystemReader.getInstance().getHostname();
   }
 
+  @Nullable
   public String getSettingsUrl() {
     return args.urlFormatter.get().getSettingsUrl().orElse(null);
   }
 
+  @Nullable
   private String getGerritUrl() {
     return args.urlFormatter.get().getWebUrl().orElse(null);
   }
@@ -471,6 +473,7 @@
    * @param accountId user to fetch.
    * @return name/email of account, username, or null if unset or the accountId is null.
    */
+  @Nullable
   protected String getUserNameEmailFor(@Nullable Account.Id accountId) {
     if (accountId == null) {
       return null;
@@ -594,6 +597,7 @@
     }
   }
 
+  @Nullable
   private Address toAddress(Account.Id id) {
     Optional<Account> accountState = args.accountCache.get(id).map(AccountState::account);
     if (!accountState.isPresent()) {
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 0d32dd5..5f31c68 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -142,6 +142,7 @@
     }
   }
 
+  @Nullable
   public ImmutableList<String> getReviewerNames() {
     List<String> names = new ArrayList<>();
     for (Account.Id id : reviewers) {
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 158972f..e76af3f 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -199,6 +199,7 @@
     return load();
   }
 
+  @Nullable
   public ObjectId loadRevision() {
     if (loaded) {
       return getRevision();
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index e6f1622..ba91c68 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -101,6 +101,7 @@
         user);
   }
 
+  @Nullable
   private static Account.Id accountId(CurrentUser u) {
     checkUserType(u);
     return (u instanceof IdentifiedUser) ? u.getAccountId() : null;
@@ -206,6 +207,7 @@
    *     deleted.
    * @throws IOException if a lower-level error occurred.
    */
+  @Nullable
   final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException {
     if (isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 73161d7..0dcf786 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -19,6 +19,7 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
@@ -187,6 +188,7 @@
     return clonedUpdate;
   }
 
+  @Nullable
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
       throws ConfigInvalidException, IOException {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 26d5933..9334ad3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -673,6 +673,7 @@
     return change.getProject();
   }
 
+  @Nullable
   @Override
   protected ObjectId readRef(Repository repo) throws IOException {
     return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo);
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 6e5e19c..e8e2f17 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -323,6 +323,7 @@
     return result;
   }
 
+  @Nullable
   private PatchSet.Id buildCurrentPatchSetId() {
     // currentPatchSets are in parse order, i.e. newest first. Pick the first
     // patch set that was marked as current, excluding deleted patch sets.
@@ -576,6 +577,7 @@
     return parseOneFooter(commit, FOOTER_SUBMISSION_ID);
   }
 
+  @Nullable
   private String parseBranch(ChangeNotesCommit commit) throws ConfigInvalidException {
     String branch = parseOneFooter(commit, FOOTER_BRANCH);
     return branch != null ? RefNames.fullName(branch) : null;
@@ -603,6 +605,7 @@
     return parseOneFooter(commit, FOOTER_TOPIC);
   }
 
+  @Nullable
   private String parseOneFooter(ChangeNotesCommit commit, FooterKey footerKey)
       throws ConfigInvalidException {
     List<String> footerLines = commit.getFooterLineValues(footerKey);
@@ -623,6 +626,7 @@
     return line;
   }
 
+  @Nullable
   private ObjectId parseRevision(ChangeNotesCommit commit) throws ConfigInvalidException {
     String sha = parseOneFooter(commit, FOOTER_COMMIT);
     if (sha == null) {
@@ -760,6 +764,7 @@
     }
   }
 
+  @Nullable
   private Change.Status parseStatus(ChangeNotesCommit commit) throws ConfigInvalidException {
     List<String> statusLines = commit.getFooterLineValues(FOOTER_STATUS);
     if (statusLines.isEmpty()) {
@@ -798,6 +803,7 @@
     return PatchSet.id(id, psId);
   }
 
+  @Nullable
   private PatchSetState parsePatchSetState(ChangeNotesCommit commit) throws ConfigInvalidException {
     String psIdLine = parseExactlyOneFooter(commit, FOOTER_PATCH_SET);
     int s = psIdLine.indexOf(' ');
@@ -1138,6 +1144,7 @@
     }
   }
 
+  @Nullable
   private Account.Id parseIdent(ChangeNotesCommit commit) throws ConfigInvalidException {
     // Check if the author name/email is the same as the committer name/email,
     // i.e. was the server ident at the time this commit was made.
@@ -1223,6 +1230,7 @@
     throw invalidFooter(FOOTER_WORK_IN_PROGRESS, raw);
   }
 
+  @Nullable
   private Change.Id parseRevertOf(ChangeNotesCommit commit) throws ConfigInvalidException {
     String footer = parseOneFooter(commit, FOOTER_REVERT_OF);
     if (footer == null) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 62c734b..1cd0fb0 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -571,6 +571,7 @@
   }
 
   /** Returns the tree id for the updated tree */
+  @Nullable
   private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
       throws ConfigInvalidException, IOException {
     if (submitRequirementResults == null && comments.isEmpty() && pushCert == null) {
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index da20475..65c83c8 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -893,7 +893,7 @@
             commitMessageRange.get().subjectEnd());
     Optional<String> fixedChangeMessage = Optional.empty();
     String originalChangeMessage = null;
-    if (commitMessageRange.isPresent() && commitMessageRange.get().hasChangeMessage()) {
+    if (commitMessageRange.get().hasChangeMessage()) {
       originalChangeMessage =
           RawParseUtils.decode(
                   enc,
diff --git a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 5d8f57f..bdfe378 100644
--- a/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -141,6 +141,7 @@
     return args.allUsers;
   }
 
+  @Nullable
   @VisibleForTesting
   NoteMap getNoteMap() {
     return revisionNoteMap != null ? revisionNoteMap.noteMap : null;
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index ad1f4c5..0939ada 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -365,6 +365,7 @@
                 cu -> cu.getAttentionSetUpdates().stream()));
   }
 
+  @Nullable
   private BatchRefUpdate execute(OpenRepo or, boolean dryrun, @Nullable PushCertificate pushCert)
       throws IOException {
     if (or == null || or.cmds.isEmpty()) {
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 396e29b..7f10596 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import java.sql.Timestamp;
@@ -68,6 +69,7 @@
    * Returns the name of the REST API handler that is in the stack trace of the caller of this
    * method.
    */
+  @Nullable
   static String guessRestApiHandler() {
     StackTraceElement[] trace = Thread.currentThread().getStackTrace();
     int i = findRestApiServlet(trace);
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index 7f067f5..e1e6305 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -19,6 +19,7 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
@@ -100,6 +101,7 @@
     put.add(c);
   }
 
+  @Nullable
   private CommitBuilder storeCommentsInNotes(
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
       throws ConfigInvalidException, IOException {
diff --git a/java/com/google/gerrit/server/patch/BaseCommitUtil.java b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
index 56a01b9..9a103cd 100644
--- a/java/com/google/gerrit/server/patch/BaseCommitUtil.java
+++ b/java/com/google/gerrit/server/patch/BaseCommitUtil.java
@@ -91,6 +91,7 @@
    * @return Returns the parent commit of the commit represented by the commitId parameter. Note
    *     that auto-merge is not supported for commits having more than two parents.
    */
+  @Nullable
   RevObject getParentCommit(
       Repository repo,
       ObjectInserter ins,
diff --git a/java/com/google/gerrit/server/patch/FilePathAdapter.java b/java/com/google/gerrit/server/patch/FilePathAdapter.java
index 2c98f1a..d0b7ac6 100644
--- a/java/com/google/gerrit/server/patch/FilePathAdapter.java
+++ b/java/com/google/gerrit/server/patch/FilePathAdapter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Patch.ChangeType;
 import java.util.Optional;
 
@@ -30,6 +31,7 @@
   /**
    * Converts the old file path of the new diff cache output to the old diff cache representation.
    */
+  @Nullable
   public static String getOldPath(Optional<String> oldName, ChangeType changeType) {
     switch (changeType) {
       case DELETED:
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 33300e3..1612925 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.entities.FixReplacement;
@@ -209,6 +210,7 @@
     }
   }
 
+  @Nullable
   private static String oldName(PatchFileChange entry) {
     switch (entry.getChangeType()) {
       case ADDED:
@@ -224,6 +226,7 @@
     }
   }
 
+  @Nullable
   private static String newName(PatchFileChange entry) {
     switch (entry.getChangeType()) {
       case DELETED:
@@ -412,6 +415,7 @@
           treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode);
     }
 
+    @Nullable
     private TreeWalk find(ObjectReader reader, String path, ObjectId within) throws IOException {
       if (path == null || within == null) {
         return null;
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index e4fa1c4..c235012 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.util.MagicBranch.NEW_CHANGE;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BranchNameKey;
@@ -269,6 +270,7 @@
     return false;
   }
 
+  @Nullable
   private Boolean canPerform(String permissionName, AccessSection section, Permission permission) {
     for (PermissionRule rule : permission.getRules()) {
       if (rule.isBlock() || rule.isDeny() || !match(rule)) {
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 478ba5c..ba292e6 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.PermissionRange;
@@ -177,6 +178,7 @@
   }
 
   /** The range of permitted values associated with a label permission. */
+  @Nullable
   PermissionRange getRange(String permission, boolean isChangeOwner) {
     if (Permission.hasRange(permission)) {
       return toRange(permission, isChangeOwner);
diff --git a/java/com/google/gerrit/server/plugins/JarScanner.java b/java/com/google/gerrit/server/plugins/JarScanner.java
index e119bf1..122e3f4 100644
--- a/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
@@ -212,6 +213,7 @@
       this.superName = superName;
     }
 
+    @Nullable
     @Override
     public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
       if (!visible) {
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 8d17d85..3263636 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -713,6 +714,7 @@
     return Iterables.filter(paths, p -> !p.getFileName().toString().endsWith(".disabled"));
   }
 
+  @Nullable
   public String getGerritPluginName(Path srcPath) {
     String fileName = srcPath.getFileName().toString();
     if (isUiPlugin(fileName)) {
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 320b618..af948b0 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -110,6 +110,7 @@
     }
   }
 
+  @Nullable
   @SuppressWarnings("unchecked")
   protected static Class<? extends Module> load(@Nullable String name, ClassLoader pluginLoader)
       throws ClassNotFoundException {
diff --git a/java/com/google/gerrit/server/project/CreateProjectArgs.java b/java/com/google/gerrit/server/project/CreateProjectArgs.java
index c1b7b86..196873f 100644
--- a/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.InheritableBoolean;
@@ -56,6 +57,7 @@
     return projectName;
   }
 
+  @Nullable
   public String getProjectName() {
     return projectName != null ? projectName.get() : null;
   }
diff --git a/java/com/google/gerrit/server/project/GroupList.java b/java/com/google/gerrit/server/project/GroupList.java
index 98dc44a..1b0ba97 100644
--- a/java/com/google/gerrit/server/project/GroupList.java
+++ b/java/com/google/gerrit/server/project/GroupList.java
@@ -126,6 +126,7 @@
     byUUID.put(uuid, reference);
   }
 
+  @Nullable
   public String asText() {
     if (byUUID.isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
index 235eb34..f46c2b1 100644
--- a/java/com/google/gerrit/server/project/LabelDefinitionJson.java
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -16,6 +16,7 @@
 
 import static java.util.stream.Collectors.toMap;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.Project;
@@ -39,8 +40,9 @@
     return label;
   }
 
+  @Nullable
   private static Boolean toBoolean(boolean v) {
-    return v ? v : null;
+    return v ? Boolean.TRUE : null;
   }
 
   private LabelDefinitionJson() {}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 654b3a4..2dd7970 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -1203,6 +1203,7 @@
     return false;
   }
 
+  @Nullable
   private List<String> getStringListOrNull(
       Config rc, String section, String subSection, String name) {
     String[] ac = rc.getStringList(section, subSection, name);
@@ -1372,6 +1373,7 @@
     return true;
   }
 
+  @Nullable
   public static String validMaxObjectSizeLimit(String value) throws ConfigInvalidException {
     if (value == null) {
       return null;
diff --git a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
index ccb5651..929399a 100644
--- a/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
+++ b/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import java.util.Iterator;
@@ -63,6 +64,7 @@
     return n;
   }
 
+  @Nullable
   private ProjectState computeNext(ProjectState n) {
     Project.NameKey parentName = n.getProject().getParent();
     if (parentName != null && visit(parentName)) {
diff --git a/java/com/google/gerrit/server/project/SectionMatcher.java b/java/com/google/gerrit/server/project/SectionMatcher.java
index 3d7175f..eaebab2 100644
--- a/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.CurrentUser;
@@ -25,6 +26,7 @@
  * of which sections are relevant to any given input reference.
  */
 public class SectionMatcher extends RefPatternMatcher {
+  @Nullable
   static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
     if (AccessSection.isValidRefSectionName(ref)) {
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index ed950c8..203e318 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.exceptions.NotSignedInException;
 import com.google.gerrit.exceptions.StorageException;
@@ -98,6 +99,7 @@
       }
     }
 
+    @Nullable
     Schema<AccountState> schema() {
       Index<?, AccountState> index = indexes != null ? indexes.getSearchIndex() : null;
       return index != null ? index.getSchema() : null;
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 98a12d5..fa1758a 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.IndexConfig;
@@ -71,6 +72,7 @@
     return query(AccountPredicates.externalIdIncludingSecondaryEmails(externalId.toString()));
   }
 
+  @Nullable
   @UsedAt(UsedAt.Project.COLLABNET)
   public AccountState oneByExternalId(ExternalId.Key externalId) {
     List<AccountState> accountStates = byExternalId(externalId);
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 1349523..ec1fcad 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -580,6 +580,7 @@
     return notes;
   }
 
+  @Nullable
   public PatchSet currentPatchSet() {
     if (currentPatchSet == null) {
       Change c = change();
@@ -624,6 +625,7 @@
     currentApprovals = approvals;
   }
 
+  @Nullable
   public String commitMessage() {
     if (commitMessage == null) {
       if (!loadCommitData()) {
@@ -647,6 +649,7 @@
     return trackingFooters.extract(commitFooters());
   }
 
+  @Nullable
   public PersonIdent getAuthor() {
     if (author == null) {
       if (!loadCommitData()) {
@@ -656,6 +659,7 @@
     return author;
   }
 
+  @Nullable
   public PersonIdent getCommitter() {
     if (committer == null) {
       if (!loadCommitData()) {
@@ -751,6 +755,7 @@
   }
 
   /** Returns patch with the given ID, or null if it does not exist. */
+  @Nullable
   public PatchSet patchSet(PatchSet.Id psId) {
     if (currentPatchSet != null && currentPatchSet.id().equals(psId)) {
       return currentPatchSet;
@@ -892,6 +897,7 @@
     return robotComments;
   }
 
+  @Nullable
   public Integer unresolvedCommentCount() {
     if (unresolvedCommentCount == null) {
       if (!lazyload()) {
@@ -914,6 +920,7 @@
     this.unresolvedCommentCount = count;
   }
 
+  @Nullable
   public Integer totalCommentCount() {
     if (totalCommentCount == null) {
       if (!lazyload()) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 91ec74c..3c69fbf 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Address;
@@ -473,6 +474,7 @@
       }
     }
 
+    @Nullable
     Schema<ChangeData> getSchema() {
       return index != null ? index.getSchema() : null;
     }
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 6aacfc9..5e8674e 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -119,6 +119,7 @@
     return count == null ? matchingVotes >= 1 : matchingVotes == count;
   }
 
+  @Nullable
   protected static LabelType type(LabelTypes types, String toFind) {
     if (types.byLabel(toFind).isPresent()) {
       return types.byLabel(toFind).get();
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 5a81ca1..e890066 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -99,6 +99,7 @@
     return new EqualsLabelPredicate(args, label, value, account, count);
   }
 
+  @Nullable
   protected static LabelType type(LabelTypes types, String toFind) {
     if (types.byLabel(toFind).isPresent()) {
       return types.byLabel(toFind).get();
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index a3c48b9..d7a5da11 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -92,7 +93,8 @@
     return Response.ok(result);
   }
 
+  @Nullable
   private static Boolean toBoolean(boolean v) {
-    return v ? v : null;
+    return v ? Boolean.TRUE : null;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index 8d65aac..d8ad3cf 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
@@ -93,6 +94,7 @@
     return pwi;
   }
 
+  @Nullable
   private static Boolean toBoolean(boolean value) {
     return value ? true : null;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 81b6fb3..8ebe71f 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -263,6 +263,7 @@
       return rci;
     }
 
+    @Nullable
     private List<FixSuggestionInfo> toFixSuggestionInfos(
         @Nullable List<FixSuggestion> fixSuggestions) {
       if (fixSuggestions == null || fixSuggestions.isEmpty()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index d818210..66171c4 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -97,6 +98,7 @@
       return true;
     }
 
+    @Nullable
     public Account.Id getDeletedAssignee() {
       return deletedAssignee != null ? deletedAssignee.account().id() : null;
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index e996169..7699873 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -370,6 +370,7 @@
           : Iterables.getFirst(fileDiffList.values(), null).oldCommitId();
     }
 
+    @Nullable
     private ObjectId getNewId(Map<String, FileDiffOutput> fileDiffList) {
       return fileDiffList.isEmpty()
           ? null
diff --git a/java/com/google/gerrit/server/restapi/change/GetChange.java b/java/com/google/gerrit/server/restapi/change/GetChange.java
index a81171a..d126d8a 100644
--- a/java/com/google/gerrit/server/restapi/change/GetChange.java
+++ b/java/com/google/gerrit/server/restapi/change/GetChange.java
@@ -144,6 +144,7 @@
         cds, this, Streams.stream(pdiFactories.entries()));
   }
 
+  @Nullable
   private ObjectId verifyMetaId(Change change, @Nullable ObjectId id) throws RestApiException {
     if (id == null) {
       return null;
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 560f4e0..5fc4f41 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -234,6 +234,7 @@
    * @param user the user who is checking to submit
    * @return a reason why any of the changes is not submittable or null
    */
+  @Nullable
   private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) {
     try {
       if (cs.furtherHiddenChanges()) {
@@ -297,6 +298,7 @@
     return null;
   }
 
+  @Nullable
   @Override
   public UiAction.Description getDescription(RevisionResource resource)
       throws IOException, PermissionBackendException {
@@ -372,6 +374,7 @@
         .setEnabled(Boolean.TRUE.equals(enabled));
   }
 
+  @Nullable
   public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws IOException {
     Set<ChangeData> mergeabilityMap = new HashSet<>();
     Set<ObjectId> outDatedPatchsets = new HashSet<>();
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 09052a6..7604a8f 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -19,6 +19,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.ContributorAgreement;
 import com.google.gerrit.extensions.common.AccountDefaultDisplayName;
 import com.google.gerrit.extensions.common.AccountsInfo;
@@ -303,6 +304,7 @@
     return info;
   }
 
+  @Nullable
   private String getDocUrl() {
     String docUrl = config.getString("gerrit", null, "docUrl");
     if (Strings.isNullOrEmpty(docUrl)) {
@@ -328,6 +330,7 @@
   private static final String DEFAULT_THEME = "/static/" + SitePaths.THEME_FILENAME;
   private static final String DEFAULT_THEME_JS = "/static/" + SitePaths.THEME_JS_FILENAME;
 
+  @Nullable
   private String getDefaultTheme() {
     if (config.getString("theme", null, "enableDefault") == null) {
       // If not explicitly enabled or disabled, check for the existence of the theme file.
@@ -344,6 +347,7 @@
     return null;
   }
 
+  @Nullable
   private SshdInfo getSshdInfo() {
     String[] addr = config.getStringList("sshd", null, "listenAddress");
     if (addr.length == 1 && isOff(addr[0])) {
@@ -380,7 +384,8 @@
     return Arrays.asList(config.getStringList("dashboard", null, "submitRequirementColumns"));
   }
 
+  @Nullable
   private static Boolean toBoolean(boolean v) {
-    return v ? v : null;
+    return v ? Boolean.TRUE : null;
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index 23b7a80..34cf550 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.config;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.Response;
@@ -221,6 +222,7 @@
     return jvmSummary;
   }
 
+  @Nullable
   private static Integer toInteger(int i) {
     return i != 0 ? i : null;
   }
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index e617931..9d36aaa 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -17,6 +17,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
@@ -181,6 +182,7 @@
     return Response.created(json.format(new InternalGroupDescription(createGroup(args))));
   }
 
+  @Nullable
   private AccountGroup.UUID owner(GroupInput input) throws UnprocessableEntityException {
     if (input.ownerId != null) {
       GroupDescription.Internal d = groups.parseInternal(Url.decode(input.ownerId));
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index b94e44d..4d9a1e9 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -21,6 +21,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
@@ -408,6 +409,7 @@
     }
   }
 
+  @Nullable
   private Pattern getRegexPattern() {
     return Strings.isNullOrEmpty(matchRegex) ? null : Pattern.compile(matchRegex);
   }
diff --git a/java/com/google/gerrit/server/restapi/project/BanCommit.java b/java/com/google/gerrit/server/restapi/project/BanCommit.java
index eb5473d..2dd7bd8 100644
--- a/java/com/google/gerrit/server/restapi/project/BanCommit.java
+++ b/java/com/google/gerrit/server/restapi/project/BanCommit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.projects.BanCommitInput;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -64,6 +65,7 @@
     return Response.ok(r);
   }
 
+  @Nullable
   private static List<String> transformCommits(List<ObjectId> commits) {
     if (commits == null || commits.isEmpty()) {
       return null;
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java
index 904a16f..192e624 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoCreator.java
@@ -17,6 +17,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
@@ -125,6 +126,7 @@
     return info;
   }
 
+  @Nullable
   private static Map<String, Map<String, ConfigParameterInfo>> getPluginConfig(
       ProjectState project,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
diff --git a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
index ca48109..455358a 100644
--- a/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/DashboardsCollection.java
@@ -225,6 +225,7 @@
     return info;
   }
 
+  @Nullable
   private static String replace(String project, String input) {
     return input == null ? input : input.replace("${project}", project);
   }
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 651e7f0..e1a3c0c 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -26,6 +26,7 @@
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
@@ -340,6 +341,7 @@
     return accessSectionInfo;
   }
 
+  @Nullable
   private static Boolean toBoolean(boolean value) {
     return value ? true : null;
   }
diff --git a/java/com/google/gerrit/server/rules/RulesCache.java b/java/com/google/gerrit/server/rules/RulesCache.java
index 773c75e..710c734 100644
--- a/java/com/google/gerrit/server/rules/RulesCache.java
+++ b/java/com/google/gerrit/server/rules/RulesCache.java
@@ -191,6 +191,7 @@
     return pmc;
   }
 
+  @Nullable
   private PrologMachineCopy consultRules(String name, Reader rules) throws CompileException {
     BufferingPrologControl ctl = newEmptyMachine(systemLoader);
     PushbackReader in = new PushbackReader(rules, Prolog.PUSHBACK_SIZE);
diff --git a/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
index 02ff159..37e7278 100644
--- a/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
+++ b/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.securestore;
 
 import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
@@ -54,6 +55,7 @@
     return sec.getStringList(section, subsection, name);
   }
 
+  @Nullable
   @Override
   public synchronized String[] getListForPlugin(
       String pluginName, String section, String subsection, String name) {
diff --git a/java/com/google/gerrit/server/securestore/SecureStore.java b/java/com/google/gerrit/server/securestore/SecureStore.java
index b53e38c..855c978 100644
--- a/java/com/google/gerrit/server/securestore/SecureStore.java
+++ b/java/com/google/gerrit/server/securestore/SecureStore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.securestore;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import java.util.List;
 
 /**
@@ -53,6 +54,7 @@
    *
    * @return decrypted String value or {@code null} if not found
    */
+  @Nullable
   public final String get(String section, String subsection, String name) {
     String[] values = getList(section, subsection, name);
     if (values != null && values.length > 0) {
@@ -67,6 +69,7 @@
    *
    * @return decrypted String value or {@code null} if not found
    */
+  @Nullable
   public final String getForPlugin(
       String pluginName, String section, String subsection, String name) {
     String[] values = getListForPlugin(pluginName, section, subsection, name);
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index b218347..0471b67 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -19,6 +19,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetInfo;
@@ -142,6 +143,7 @@
       patchSetInfo = args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId);
     }
 
+    @Nullable
     @Override
     public PatchSet updateChangeImpl(ChangeContext ctx) throws NoSuchChangeException, IOException {
       if (newCommit == null && toMerge.getStatusCode() == SKIPPED_IDENTICAL_TREE) {
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 27eb0a4..a34aeac 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -929,11 +929,13 @@
     }
   }
 
+  @Nullable
   private SubmitType getSubmitType(ChangeData cd) {
     SubmitTypeRecord str = cd.submitTypeRecord();
     return str.isOk() ? str.type : null;
   }
 
+  @Nullable
   private OpenRepo openRepo(Project.NameKey project) {
     try {
       return orm.getRepo(project);
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index cfb2f88..5f58a74 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
@@ -238,6 +239,7 @@
       acceptMergeTip(args.mergeTip);
     }
 
+    @Nullable
     @Override
     public PatchSet updateChangeImpl(ChangeContext ctx)
         throws NoSuchChangeException, ResourceConflictException, IOException, BadRequestException {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index d06940c..bb2b1a4 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -22,6 +22,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelId;
@@ -158,6 +159,7 @@
     }
   }
 
+  @Nullable
   private CodeReviewCommit getAlreadyMergedCommit(RepoContext ctx) throws IOException {
     CodeReviewCommit tip = args.mergeTip.getInitialTip();
     if (tip == null) {
diff --git a/java/com/google/gerrit/server/submit/SubmoduleCommits.java b/java/com/google/gerrit/server/submit/SubmoduleCommits.java
index 37df66b..1fd3ad6 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleCommits.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleCommits.java
@@ -18,6 +18,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.SubmoduleSubscription;
 import com.google.gerrit.exceptions.StorageException;
@@ -212,6 +213,7 @@
     return newCommit;
   }
 
+  @Nullable
   private RevCommit updateSubmodule(
       DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s)
       throws SubmoduleConflictException, IOException {
diff --git a/java/com/google/gerrit/server/util/MagicBranch.java b/java/com/google/gerrit/server/util/MagicBranch.java
index 924c288..a5ce108 100644
--- a/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/java/com/google/gerrit/server/util/MagicBranch.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.entities.Project;
 import java.io.IOException;
@@ -38,6 +39,7 @@
   }
 
   /** Returns the ref name prefix for a magic branch, {@code null} if the branch is not magic */
+  @Nullable
   public static String getMagicRefNamePrefix(String refName) {
     if (refName.startsWith(NEW_CHANGE)) {
       return NEW_CHANGE;
diff --git a/java/com/google/gerrit/server/util/git/BUILD b/java/com/google/gerrit/server/util/git/BUILD
index bbc6bf0..83a230d 100644
--- a/java/com/google/gerrit/server/util/git/BUILD
+++ b/java/com/google/gerrit/server/util/git/BUILD
@@ -5,6 +5,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//lib:jgit",
         "//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
index 97132a3..201a9b7 100644
--- a/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
+++ b/java/com/google/gerrit/server/util/git/SubmoduleSectionParser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.util.git;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.SubmoduleSubscription;
@@ -65,6 +66,7 @@
     return parsedSubscriptions;
   }
 
+  @Nullable
   private SubmoduleSubscription parse(String id) {
     final String url = config.getString("submodule", id, "url");
     final String path = config.getString("submodule", id, "path");
diff --git a/java/com/google/gerrit/sshd/Commands.java b/java/com/google/gerrit/sshd/Commands.java
index b6d3401..5d641a0 100644
--- a/java/com/google/gerrit/sshd/Commands.java
+++ b/java/com/google/gerrit/sshd/Commands.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.auto.value.AutoAnnotation;
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Key;
 import java.lang.annotation.Annotation;
 import org.apache.sshd.server.command.Command;
@@ -78,6 +79,7 @@
     return false;
   }
 
+  @Nullable
   static CommandName parentOf(CommandName name) {
     if (name instanceof NestedCommandNameImpl) {
       return ((NestedCommandNameImpl) name).parent;
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 6997d96..401d31e 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -22,6 +22,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.account.AccountSshKey;
@@ -169,6 +170,7 @@
     return p.keys;
   }
 
+  @Nullable
   private SshKeyCacheEntry find(Iterable<SshKeyCacheEntry> keyList, PublicKey suppliedKey) {
     for (SshKeyCacheEntry k : keyList) {
       if (k.match(suppliedKey)) {
diff --git a/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index 5b6d8f9..7adcd24 100644
--- a/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.server.plugins.InvalidPluginException;
 import com.google.gerrit.server.plugins.ModuleGenerator;
@@ -84,6 +85,7 @@
     listeners.put(tl, clazz);
   }
 
+  @Nullable
   @Override
   public Module create() throws InvalidPluginException {
     checkState(command != null, "pluginName must be provided");
diff --git a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 6e8590c..8711fe6 100644
--- a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.plugins.Plugin;
@@ -57,6 +58,7 @@
     }
   }
 
+  @Nullable
   private Provider<Command> load(Plugin plugin) {
     if (plugin.getSshInjector() != null) {
       Key<Command> key = Commands.key(plugin.getName());
diff --git a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 4da55e2..2e29203 100644
--- a/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -17,6 +17,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -87,6 +88,7 @@
     }
   }
 
+  @Nullable
   private String readSshKey() throws IOException {
     if (sshKey == null) {
       return null;
diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java
index 7c42797..a37c027 100644
--- a/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -44,6 +44,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.StringWriter;
@@ -567,6 +568,7 @@
      *     and it needed to be exposed.
      */
     @SuppressWarnings("rawtypes")
+    @Nullable
     public OptionHandler findOptionByName(String name) {
       for (OptionHandler h : optionsList) {
         if (h.option instanceof NamedOptionDef) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index c2b779b..ec2e751 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -2064,6 +2064,7 @@
     return newEmailInput(email, true);
   }
 
+  @Nullable
   private String getMetaId(Account.Id accountId) throws IOException {
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo);
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 18eca27..462d0a5 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
 import com.google.gerrit.extensions.api.plugins.PluginApi;
@@ -198,6 +199,7 @@
     return pluginJarContent(plugin);
   }
 
+  @Nullable
   private String pluginVersion(String plugin) {
     String name = pluginName(plugin);
     if (name.endsWith("empty")) {
@@ -210,6 +212,7 @@
     return dash > 0 ? name.substring(dash + 1) : "";
   }
 
+  @Nullable
   private String pluginApiVersion(String plugin) {
     if (plugin.endsWith("normal.jar")) {
       return "2.16.19-SNAPSHOT";
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index 6d2c6dfa..a9e3cf6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
 import com.google.gson.reflect.TypeToken;
 import java.util.List;
@@ -41,6 +42,7 @@
     userRestSession.get("/config/server/tasks/" + getLogFileCompressorTaskId()).assertNotFound();
   }
 
+  @Nullable
   private String getLogFileCompressorTaskId() throws Exception {
     RestResponse r = adminRestSession.get("/config/server/tasks/");
     List<TaskInfo> result =
diff --git a/javatests/com/google/gerrit/extensions/BUILD b/javatests/com/google/gerrit/extensions/BUILD
index 2202a11..1bb39c8 100644
--- a/javatests/com/google/gerrit/extensions/BUILD
+++ b/javatests/com/google/gerrit/extensions/BUILD
@@ -5,6 +5,7 @@
     size = "small",
     srcs = glob(["**/*.java"]),
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/extensions/common/testing:common-test-util",
         "//lib:guava",
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 024e35e..7ed236a 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ReviewerState;
 import java.lang.reflect.Field;
 import java.lang.reflect.ParameterizedType;
@@ -344,6 +345,7 @@
     assertThat(diff.removed().reviewers).isNull();
   }
 
+  @Nullable
   private static Object buildObjectWithFullFields(Class<?> c) throws Exception {
     if (c == null) {
       return null;
@@ -365,6 +367,7 @@
     return toPopulate;
   }
 
+  @Nullable
   private static Class<?> getParameterizedType(Field field) {
     if (!Collection.class.isAssignableFrom(field.getType())) {
       return null;
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index 11f3528..2d90ab4 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -75,6 +75,7 @@
     allUsersRepo.close();
   }
 
+  @Nullable
   protected Instant getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
     try (RevWalk rw = new RevWalk(allUsersRepo)) {
       Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 8def660..ca53bad 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -37,6 +37,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
@@ -3938,6 +3939,7 @@
     return new String(rw.getObjectReader().open(dataId, OBJ_BLOB).getCachedBytes(), UTF_8);
   }
 
+  @Nullable
   private ObjectId exactRefAllUsers(String refName) throws Exception {
     try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
       Ref ref = allUsersRepo.exactRef(refName);
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 04e9367..a7edd0f 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -747,6 +748,7 @@
     return "\"" + s + "\"";
   }
 
+  @Nullable
   protected String name(String name) {
     if (name == null) {
       return null;
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index c255f5d..c781d8b 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -13,6 +13,7 @@
         "//prolog:gerrit-prolog-common",
     ],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index 4f48b9e..fe60119 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -109,7 +109,6 @@
 
   @Test
   @UseClockStep
-  @SuppressWarnings("unchecked")
   public void internalQueriesPaginate() throws Exception {
     // create 4 changes
     TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index e5c893d..540416f 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.InternalGroup;
@@ -560,6 +561,7 @@
     return groups.stream().map(g -> g.id).sorted().collect(toList());
   }
 
+  @Nullable
   protected String name(String name) {
     if (name == null) {
       return null;
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 0cc132d..e877c81 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -10,6 +10,7 @@
     visibility = ["//visibility:public"],
     runtime_deps = ["//java/com/google/gerrit/lucene"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index c06fcde..b119104 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -24,6 +24,7 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -469,6 +470,7 @@
     return projects.stream().map(p -> p.name).collect(toList());
   }
 
+  @Nullable
   protected String name(String name) {
     if (name == null) {
       return null;
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index f476ae6..53f9d9d 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -10,6 +10,7 @@
     visibility = ["//visibility:public"],
     runtime_deps = ["//java/com/google/gerrit/lucene"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/index",
diff --git a/javatests/com/google/gerrit/util/http/testutil/BUILD b/javatests/com/google/gerrit/util/http/testutil/BUILD
index 3a67d45..3b4817b 100644
--- a/javatests/com/google/gerrit/util/http/testutil/BUILD
+++ b/javatests/com/google/gerrit/util/http/testutil/BUILD
@@ -6,6 +6,7 @@
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
         "//lib:jgit",
         "//lib:servlet-api",
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index ebdf2d9..0347177 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -25,6 +25,7 @@
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
 import java.io.BufferedReader;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
@@ -105,6 +106,7 @@
     return -1;
   }
 
+  @Nullable
   @Override
   public String getContentType() {
     List<String> contentType = headers.get("Content-Type");
diff --git a/plugins/download-commands b/plugins/download-commands
index 71331e1..a16ebc6 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 71331e15af5a62ee7b13dee6ebdadf23d7e75a40
+Subproject commit a16ebc6cdaaa4db5e5a2b6d062bb0ebbb3d3d0f4
diff --git a/plugins/gitiles b/plugins/gitiles
index 24529d2..12e26b3 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 24529d232268ac51fd6850770f70dc0fcd732dd8
+Subproject commit 12e26b33ac55109bbb1d5eb56f198235552fb919
diff --git a/plugins/hooks b/plugins/hooks
index 20aeaa9..3007362 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 20aeaa9fcc6aa2665acb5e0f401039074037221c
+Subproject commit 30073628612bce23826f4be71bfdd159da521cbc
diff --git a/plugins/replication b/plugins/replication
index f1aefa2..ced31c0 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit f1aefa28f821699cc1ddd37bf0aa85177c775f17
+Subproject commit ced31c0c7d56cbb3e10a8da35a8ddd5db1bba550
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 4198fe8..10db2cf 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 4198fe8df1c1b86d812f32da63e891b1c2fc6f3e
+Subproject commit 10db2cf772989d031c6f3558010c51fe07cf9722
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 3239ce3..084a372 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 3239ce3a471f5aa9edd8f6f702bee655ea81f77d
+Subproject commit 084a37253dc94ac52cfaa1c9d516fcb8b0318b31
diff --git a/plugins/webhooks b/plugins/webhooks
index d8815bf..16110f3 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit d8815bf9660b6655696db242b8ad2801e866c036
+Subproject commit 16110f320dd5b6a40af87eaba4bf3af60cb0efd1
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
index 0afb273..9c53fea 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.ts
@@ -4,7 +4,6 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-icons/gr-icons';
 import {fireEvent} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {LitElement, css, html} from 'lit';
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 88bf45e..917b7ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -8,7 +8,6 @@
 import '../../shared/gr-dialog/gr-dialog';
 import '../../shared/gr-dropdown/gr-dropdown';
 import '../../shared/gr-icon/gr-icon';
-import '../../shared/gr-icons/gr-icons';
 import '../../shared/gr-overlay/gr-overlay';
 import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
 import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index e0fcd88..d6e1958 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -15,7 +15,7 @@
 import '../../shared/gr-change-star/gr-change-star';
 import '../../shared/gr-change-status/gr-change-status';
 import '../../shared/gr-editable-content/gr-editable-content';
-import '../../shared/gr-linked-text/gr-linked-text';
+import '../../shared/gr-formatted-text/gr-formatted-text';
 import '../../shared/gr-overlay/gr-overlay';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
 import '../gr-change-actions/gr-change-actions';
@@ -104,12 +104,7 @@
 import {GrIncludedInDialog} from '../gr-included-in-dialog/gr-included-in-dialog';
 import {GrDownloadDialog} from '../gr-download-dialog/gr-download-dialog';
 import {GrChangeMetadata} from '../gr-change-metadata/gr-change-metadata';
-import {
-  assertIsDefined,
-  assert,
-  query as queryEl,
-  queryAll,
-} from '../../../utils/common-util';
+import {assertIsDefined, assert, queryAll} from '../../../utils/common-util';
 import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
 import {
   CommentThread,
@@ -121,14 +116,12 @@
 import {GrFileList} from '../gr-file-list/gr-file-list';
 import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
 import {
-  ChecksTabState,
   CloseFixPreviewEvent,
   EditableContentSaveEvent,
   EventType,
   OpenFixPreviewEvent,
   ShowAlertEventDetail,
   SwitchTabEvent,
-  SwitchTabEventDetail,
   TabState,
   ValueChangedEvent,
 } from '../../../types/events';
@@ -486,9 +479,12 @@
   @state()
   private changeViewAriaHidden = false;
 
+  /**
+   * This can be a string only for plugin provided tabs.
+   */
   // visible for testing
   @state()
-  activeTab = Tab.FILES;
+  activeTab: Tab | string = Tab.FILES;
 
   @property({type: Boolean})
   unresolvedOnly = true;
@@ -702,6 +698,11 @@
     );
     subscribe(
       this,
+      () => this.getViewModel().tab$,
+      t => (this.activeTab = t ?? Tab.FILES)
+    );
+    subscribe(
+      this,
       () => this.getChecksModel().aPluginHasRegistered$,
       b => {
         this.showChecksTab = b;
@@ -962,7 +963,7 @@
           /* Account for border and padding and rounding errors. */
           max-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
         }
-        .commitMessage gr-linked-text {
+        .commitMessage gr-formatted-text {
           word-break: break-word;
         }
         #commitMessageEditor {
@@ -1463,12 +1464,10 @@
                 .commitCollapsible=${this.computeCommitCollapsible()}
                 remove-zero-width-space=""
               >
-                <gr-linked-text
-                  pre=""
-                  .content=${this.latestCommitMessage}
-                  .config=${this.projectConfig?.commentlinks}
-                  remove-zero-width-space=""
-                ></gr-linked-text>
+                <gr-formatted-text
+                  .content=${this.latestCommitMessage ?? ''}
+                  .markdown=${false}
+                ></gr-formatted-text>
               </gr-editable-content>
             </div>
             <h3 class="assistive-tech-only">Comments and Checks Summary</h3>
@@ -1499,7 +1498,10 @@
 
   private renderTabHeaders() {
     return html`
-      <paper-tabs id="tabs" @selected-changed=${this.setActiveTab}>
+      <paper-tabs
+        id="tabs"
+        @selected-changed=${this.onPaperTabSelectionChanged}
+      >
         <paper-tab @click=${this.onPaperTabClick} data-name=${Tab.FILES}
           ><span>Files</span></paper-tab
         >
@@ -1732,38 +1734,38 @@
     }
   }
 
-  setActiveTab(e: SwitchTabEvent) {
-    const paperTabs = queryEl<PaperTabsElement>(this, '#tabs');
-    if (!paperTabs) return;
-    const tabs = [...queryAll<HTMLElement>(paperTabs, 'paper-tab')];
+  onPaperTabSelectionChanged(e: ValueChangedEvent) {
+    if (!this.tabs) return;
+    const tabs = [...queryAll<HTMLElement>(this.tabs, 'paper-tab')];
     if (!tabs) return;
 
-    let tabName = e.detail.tab;
-    let tabIndex = e.detail.value;
+    const tabIndex = Number(e.detail.value);
+    assert(
+      Number.isInteger(tabIndex) && 0 <= tabIndex && tabIndex < tabs.length,
+      `${tabIndex} must be integer`
+    );
+    const tab = tabs[tabIndex].dataset['name'];
 
-    if (tabIndex === undefined) {
-      assert(tabName !== undefined, 'tabName or tabIndex must be defined');
-      tabIndex = tabs.findIndex(t => t.dataset['name'] === tabName);
-      assert(tabIndex !== -1, `tab ${tabName} not found`);
+    this.getViewModel().updateState({tab});
+  }
+
+  setActiveTab(e: SwitchTabEvent) {
+    if (!this.tabs) return;
+    const tabs = [...queryAll<HTMLElement>(this.tabs, 'paper-tab')];
+    if (!tabs) return;
+
+    const tab = e.detail.tab;
+    const tabIndex = tabs.findIndex(t => t.dataset['name'] === tab);
+    assert(tabIndex !== -1, `tab ${tab} not found`);
+
+    if (this.tabs.selected !== tabIndex) {
+      this.tabs.selected = tabIndex;
     }
 
-    if (tabName === undefined) {
-      tabName = tabs[tabIndex].dataset['name'];
-    }
-
-    if (paperTabs.selected !== tabIndex) {
-      // paperTabs.selected is undefined during rendering
-      if (paperTabs.selected !== undefined) {
-        const src = (e.composedPath()?.[0] as Element | undefined)?.tagName;
-        this.reporting.reportInteraction(Interaction.SHOW_TAB, {tabName, src});
-      }
-      paperTabs.selected = tabIndex;
-    }
-
-    this.activeTab = tabName as Tab;
+    this.getViewModel().updateState({tab});
 
     if (e.detail.tabState) this.tabState = e.detail.tabState;
-    if (e.detail.scrollIntoView) paperTabs.scrollIntoView({block: 'center'});
+    if (e.detail.scrollIntoView) this.tabs.scrollIntoView({block: 'center'});
   }
 
   /**
@@ -2249,19 +2251,7 @@
     } else if (this.viewState?.commentId) {
       tab = Tab.COMMENT_THREADS;
     }
-    const detail: SwitchTabEventDetail = {
-      tab,
-    };
-    if (tab === Tab.CHECKS) {
-      const state: ChecksTabState = {};
-      detail.tabState = {checksTab: state};
-    }
-
-    this.setActiveTab(
-      new CustomEvent(EventType.SHOW_TAB, {
-        detail,
-      })
-    );
+    this.setActiveTab(new CustomEvent(EventType.SHOW_TAB, {detail: {tab}}));
   }
 
   // Private but used in tests.
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index ad84fb0..f3017f8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -433,9 +433,7 @@
                         id="commitMessageEditor"
                         remove-zero-width-space=""
                       >
-                        <gr-linked-text pre="" remove-zero-width-space="">
-                          <span id="output" slot="insert"></span>
-                        </gr-linked-text>
+                        <gr-formatted-text></gr-formatted-text>
                       </gr-editable-content>
                     </div>
                     <h3 class="assistive-tech-only">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index 76335cb..a7d9e28 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -342,8 +342,8 @@
     });
 
     test('correct number of files are shown', async () => {
-      element.fileListIncrement = 300;
-      element.files = createFiles(500);
+      element.fileListIncrement = 100;
+      element.files = createFiles(250);
       await element.updateComplete;
       await waitEventLoop();
 
@@ -358,19 +358,19 @@
           element,
           '#incrementButton'
         ).textContent!.trim(),
-        'Show 300 more'
+        'Show 50 more'
       );
       assert.equal(
         queryAndAssert<GrButton>(element, '#showAllButton').textContent!.trim(),
-        'Show all 500 files'
+        'Show all 250 files'
       );
 
       queryAndAssert<GrButton>(element, '#showAllButton').click();
       await element.updateComplete;
       await waitEventLoop();
 
-      assert.equal(element.numFilesShown, 500);
-      assert.equal(element.shownFiles.length, 500);
+      assert.equal(element.numFilesShown, 250);
+      assert.equal(element.shownFiles.length, 250);
       assert.isTrue(controlRow.classList.contains('invisible'));
     });
 
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index a4e26fe..848948f 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -77,6 +77,7 @@
 import {DropdownItem} from '../shared/gr-dropdown-list/gr-dropdown-list';
 import './gr-checks-attempt';
 import {createDiffUrl} from '../../models/views/diff';
+import {changeViewModelToken} from '../../models/views/change';
 
 /**
  * Firing this event sets the regular expression of the results filter.
@@ -756,7 +757,7 @@
   filterInput?: HTMLInputElement;
 
   @state()
-  filterRegExp = new RegExp('');
+  filterRegExp = '';
 
   /** All runs. Shown should only the selected/filtered ones. */
   @property({attribute: false})
@@ -766,8 +767,8 @@
    * Check names of runs that are selected in the runs panel. When this array
    * is empty, then no run is selected and all runs should be shown.
    */
-  @property({attribute: false})
-  selectedRuns: string[] = [];
+  @state()
+  selectedRuns: Set<string> = new Set();
 
   @state()
   actions: Action[] = [];
@@ -808,6 +809,8 @@
    */
   private isSectionExpandedByUser = new Map<Category, boolean>();
 
+  private readonly getViewModel = resolve(this, changeViewModelToken);
+
   private readonly getChangeModel = resolve(this, changeModelToken);
 
   private readonly getChecksModel = resolve(this, checksModelToken);
@@ -853,6 +856,16 @@
       () => this.getChecksModel().someProvidersAreLoadingSelected$,
       x => (this.someProvidersAreLoading = x)
     );
+    subscribe(
+      this,
+      () => this.getViewModel().checksRunsSelected$,
+      x => (this.selectedRuns = x)
+    );
+    subscribe(
+      this,
+      () => this.getViewModel().checksResultsFilter$,
+      x => (this.filterRegExp = x)
+    );
   }
 
   static override get styles() {
@@ -1051,6 +1064,9 @@
 
   protected override updated(changedProperties: PropertyValues) {
     super.updated(changedProperties);
+    if (changedProperties.has('filterRegExp') && this.filterInput) {
+      this.filterInput.value = this.filterRegExp;
+    }
     if (changedProperties.has('tabState') && this.tabState) {
       const {statusOrCategory, checkName} = this.tabState;
       if (isCategory(statusOrCategory)) {
@@ -1224,11 +1240,10 @@
   }
 
   private handleFilter(e: ChecksResultsFilterEvent) {
-    if (!this.filterInput) return;
-    const oldValue = this.filterInput.value ?? '';
     const newValue = e.detail.filterRegExp ?? '';
-    this.filterInput.value = oldValue === newValue ? '' : newValue;
-    this.onFilterInputChange();
+    this.getViewModel().updateState({
+      checksResultsFilter: this.filterRegExp === newValue ? '' : newValue,
+    });
   }
 
   private renderAction(action?: Action) {
@@ -1289,10 +1304,7 @@
   }
 
   isRunSelected(run: {checkName: string}) {
-    return (
-      this.selectedRuns.length === 0 ||
-      this.selectedRuns.includes(run.checkName)
-    );
+    return this.selectedRuns.size === 0 || this.selectedRuns.has(run.checkName);
   }
 
   renderFilter() {
@@ -1300,10 +1312,11 @@
       run =>
         this.isRunSelected(run) && isAttemptSelected(this.selectedAttempt, run)
     );
-    if (this.selectedRuns.length === 0 && allResults(runs).length <= 3) {
-      if (this.filterRegExp.source.length > 0) {
-        this.filterRegExp = new RegExp('');
-      }
+    if (
+      this.selectedRuns.size === 0 &&
+      allResults(runs).length <= 3 &&
+      this.filterRegExp === ''
+    ) {
       return;
     }
     return html`
@@ -1325,7 +1338,9 @@
       {},
       {deduping: Deduping.EVENT_ONCE_PER_CHANGE}
     );
-    this.filterRegExp = new RegExp(this.filterInput.value, 'i');
+    this.getViewModel().updateState({
+      checksResultsFilter: this.filterInput.value,
+    });
   }
 
   renderSection(category: Category) {
@@ -1342,18 +1357,32 @@
       ],
       []
     );
-    const isSelection = this.selectedRuns.length > 0;
+    const isSelectionActive = this.selectedRuns.size > 0;
     const selected = all.filter(result => this.isRunSelected(result));
-    const filtered = selected.filter(result =>
-      matches(result, this.filterRegExp)
-    );
+    const re = new RegExp(this.filterRegExp, 'i');
+    const filtered = selected.filter(result => matches(result, re));
+    const isFilterActiveWithResults =
+      this.filterRegExp !== '' && filtered.length > 0;
+
+    // The logic for deciding whether to expand a section by default is a bit
+    // complicated, but we want to collapse empty and info/success sections by
+    // default for a clean and focused user experience. However, as soon as the
+    // user starts selecting or filtering we must take this into account and
+    // prefer to expand the sections.
     let expanded = this.isSectionExpanded.get(category);
     const expandedByUser = this.isSectionExpandedByUser.get(category) ?? false;
     if (!expandedByUser || expanded === undefined) {
-      expanded = selected.length > 0 && (isWarningOrError || isSelection);
+      // Note that we are using `selected` for `isEmpty` and not `filtered`,
+      // because if the filter is what makes a section empty, then we want to
+      // show an expanded section, which contains a message about this.
+      const isEmpty = selected.length === 0;
+      expanded =
+        !isEmpty &&
+        (isWarningOrError || isSelectionActive || isFilterActiveWithResults);
       this.isSectionExpanded.set(category, expanded);
     }
     const expandedClass = expanded ? 'expanded' : 'collapsed';
+
     const isShowAll = this.isShowAll.get(category) ?? false;
     const resultCount = filtered.length;
     const empty = resultCount === 0 ? 'empty' : '';
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 09a2558..128a9b0 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -44,7 +44,7 @@
 } from '../../models/checks/checks-fakes';
 import {assertIsDefined} from '../../utils/common-util';
 import {modifierPressed, whenVisible} from '../../utils/dom-util';
-import {fireRunSelected, fireRunSelectionReset} from './gr-checks-util';
+import {fireRunSelected, RunSelectedEvent} from './gr-checks-util';
 import {ChecksTabState} from '../../types/events';
 import {charsOnly} from '../../utils/string-util';
 import {getAppContext} from '../../services/app-context';
@@ -57,6 +57,7 @@
 import {Interaction} from '../../constants/reporting';
 import {Deduping} from '../../api/reporting';
 import {when} from 'lit/directives/when.js';
+import {changeViewModelToken} from '../../models/views/change';
 
 @customElement('gr-checks-run')
 export class GrChecksRun extends LitElement {
@@ -403,8 +404,8 @@
   @property({type: Boolean, reflect: true})
   collapsed = false;
 
-  @property({attribute: false})
-  selectedRuns: string[] = [];
+  @state()
+  selectedRuns: Set<string> = new Set();
 
   @state()
   selectedAttempt: AttemptChoice = LATEST_ATTEMPT;
@@ -424,6 +425,8 @@
 
   private getChecksModel = resolve(this, checksModelToken);
 
+  private readonly getViewModel = resolve(this, changeViewModelToken);
+
   private readonly reporting = getAppContext().reportingService;
 
   constructor() {
@@ -453,6 +456,11 @@
       () => this.getChecksModel().runFilterRegexp$,
       x => (this.filterRegExp = x)
     );
+    subscribe(
+      this,
+      () => this.getViewModel().checksRunsSelected$,
+      x => (this.selectedRuns = x)
+    );
     this.addEventListener('click', () => {
       if (this.collapsed) this.toggleCollapsed();
     });
@@ -660,8 +668,8 @@
 
   private renderTitleButtons() {
     if (this.collapsed) return;
-    if (this.selectedRuns.length < 2) return;
-    const actions = this.selectedRuns.map(selected => {
+    if (this.selectedRuns.size < 2) return;
+    const actions = [...this.selectedRuns].map(selected => {
       const run = this.runs.find(
         run => run.isLatestAttempt && run.checkName === selected
       );
@@ -676,7 +684,8 @@
       <gr-button
         class="font-normal"
         link
-        @click=${() => fireRunSelectionReset(this)}
+        @click=${() =>
+          this.getViewModel().updateState({checksRunsSelected: undefined})}
         >Unselect All</gr-button
       >
       <gr-tooltip-content
@@ -820,16 +829,23 @@
   }
 
   renderRun(run: CheckRun) {
-    const selectedRun = this.selectedRuns.includes(run.checkName);
-    const deselected = !selectedRun && this.selectedRuns.length > 0;
+    const selectedRun = this.selectedRuns.has(run.checkName);
+    const deselected = !selectedRun && this.selectedRuns.size > 0;
     return html`<gr-checks-run
       .run=${run}
       ?condensed=${this.collapsed}
       .selected=${selectedRun}
       .deselected=${deselected}
+      @run-selected=${this.handleRunSelected}
     ></gr-checks-run>`;
   }
 
+  handleRunSelected(e: RunSelectedEvent) {
+    if (e.detail.checkName) {
+      this.getViewModel().toggleSelectedCheckRun(e.detail.checkName);
+    }
+  }
+
   showFilter(): boolean {
     if (this.collapsed) return false;
     return this.runs.length > 10 || !!this.filterRegExp;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 82893bc..c1bcdc1 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -14,7 +14,6 @@
 import './gr-checks-runs';
 import './gr-checks-results';
 import {NumericChangeId, PatchSetNumber} from '../../types/common';
-import {RunSelectedEvent} from './gr-checks-util';
 import {TabState} from '../../types/events';
 import {getAppContext} from '../../services/app-context';
 import {subscribe} from '../lit/subscription-controller';
@@ -50,9 +49,6 @@
   @state()
   changeNum: NumericChangeId | undefined = undefined;
 
-  @state()
-  selectedRuns: string[] = [];
-
   private readonly getChangeModel = resolve(this, changeModelToken);
 
   private readonly getChecksModel = resolve(this, checksModelToken);
@@ -132,41 +128,16 @@
           class="runs"
           ?collapsed=${this.offsetWidth < 1000}
           .runs=${this.runs}
-          .selectedRuns=${this.selectedRuns}
           .tabState=${this.tabState?.checksTab}
-          @run-selected=${this.handleRunSelected}
         ></gr-checks-runs>
         <gr-checks-results
           class="results"
           .tabState=${this.tabState?.checksTab}
           .runs=${this.runs}
-          .selectedRuns=${this.selectedRuns}
         ></gr-checks-results>
       </div>
     `;
   }
-
-  handleRunSelected(e: RunSelectedEvent) {
-    this.reporting.reportInteraction(Interaction.CHECKS_RUN_SELECTED, {
-      checkName: e.detail.checkName,
-      reset: e.detail.reset,
-    });
-    if (e.detail.reset) {
-      this.selectedRuns = [];
-      return;
-    }
-    if (e.detail.checkName) {
-      this.toggleSelected(e.detail.checkName);
-    }
-  }
-
-  toggleSelected(checkName: string) {
-    if (this.selectedRuns.includes(checkName)) {
-      this.selectedRuns = this.selectedRuns.filter(r => r !== checkName);
-    } else {
-      this.selectedRuns = [...this.selectedRuns, checkName];
-    }
-  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util.ts b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
index 939a87e..c7477c4 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-util.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
@@ -11,7 +11,6 @@
 } from '../../models/checks/checks-util';
 
 export interface RunSelectedEventDetail {
-  reset: boolean;
   checkName?: string;
 }
 
@@ -33,16 +32,6 @@
   );
 }
 
-export function fireRunSelectionReset(target: EventTarget) {
-  target.dispatchEvent(
-    new CustomEvent('run-selected', {
-      detail: {reset: true},
-      composed: true,
-      bubbles: true,
-    })
-  );
-}
-
 export function isAttemptSelected(
   selectedAttempt: AttemptChoice,
   run: CheckRun
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 74589d3..f829313 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -1406,8 +1406,12 @@
     }
     const filter = queryMap.get('filter');
     if (filter) state.filter = filter;
+    const checksResultsFilter = queryMap.get('checksResultsFilter');
+    if (checksResultsFilter) state.checksResultsFilter = checksResultsFilter;
     const attempt = stringToAttemptChoice(queryMap.get('attempt'));
     if (attempt && attempt !== LATEST_ATTEMPT) state.attempt = attempt;
+    const selected = queryMap.get('checksRunsSelected');
+    if (selected) state.checksRunsSelected = new Set(selected.split(','));
 
     assertIsDefined(state.project, 'project');
     this.reporting.setRepoName(state.project);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index ce3ed0d..119ba48 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -1149,6 +1149,8 @@
           queryMap.set('filter', 'fff');
           queryMap.set('select', 'sss');
           queryMap.set('attempt', '1');
+          queryMap.set('checksRunsSelected', 'asdf,qwer');
+          queryMap.set('checksResultsFilter', 'asdf.*qwer');
           ctx.querystring = queryMap.toString();
           assertctxToParams(ctx, 'handleChangeRoute', {
             view: GerritView.CHANGE,
@@ -1159,6 +1161,8 @@
             attempt: 1,
             filter: 'fff',
             tab: 'checks',
+            checksRunsSelected: new Set(['asdf', 'qwer']),
+            checksResultsFilter: 'asdf.*qwer',
           });
         });
       });
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 262f1fd..17d7516 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
@@ -22,7 +22,7 @@
 import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {getAppContext} from '../../../services/app-context';
-import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
+import {fireCloseFixPreview} from '../../../utils/event-util';
 import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {TokenHighlightLayer} from '../../../embed/diff/gr-diff-builder/token-highlight-layer';
@@ -33,6 +33,7 @@
 import {assert} from '../../../utils/common-util';
 import {resolve} from '../../../models/dependency';
 import {createChangeUrl} from '../../../models/views/change';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 
 interface FilePreview {
   filepath: string;
@@ -44,6 +45,15 @@
   @query('#applyFixOverlay')
   applyFixOverlay?: GrOverlay;
 
+  @query('#applyFixDialog')
+  applyFixDialog?: GrDialog;
+
+  /** The currently observed dialog by `dialogOberserver`. */
+  observedDialog?: GrDialog;
+
+  /** The current observer observing the `observedDialog`. */
+  dialogObserver?: ResizeObserver;
+
   @query('#nextFix')
   nextFix?: GrButton;
 
@@ -102,9 +112,6 @@
         this.diffPrefs = diffPreferences;
       }
     );
-    this.addEventListener('diff-context-expanded', () => {
-      if (this.applyFixOverlay) fireEvent(this.applyFixOverlay, 'iron-resize');
-    });
   }
 
   static override styles = [
@@ -148,6 +155,39 @@
     `;
   }
 
+  override updated() {
+    this.updateDialogObserver();
+  }
+
+  override disconnectedCallback() {
+    this.removeDialogObserver();
+    super.disconnectedCallback();
+  }
+
+  private removeDialogObserver() {
+    this.dialogObserver?.disconnect();
+    this.dialogObserver = undefined;
+    this.observedDialog = undefined;
+  }
+
+  private updateDialogObserver() {
+    if (
+      this.applyFixDialog === this.observedDialog &&
+      this.dialogObserver !== undefined
+    ) {
+      return;
+    }
+
+    this.removeDialogObserver();
+    if (!this.applyFixDialog) return;
+
+    this.observedDialog = this.applyFixDialog;
+    this.dialogObserver = new ResizeObserver(() => {
+      this.applyFixOverlay?.refit();
+    });
+    this.dialogObserver.observe(this.observedDialog);
+  }
+
   private renderHeader() {
     return html`
       <div slot="header">${this.currentFix?.description ?? ''}</div>
@@ -202,7 +242,7 @@
    * Given event with fixSuggestions, fetch diffs associated with first
    * suggested fix and open dialog.
    */
-  async open(e: OpenFixPreviewEvent) {
+  open(e: OpenFixPreviewEvent) {
     this.patchNum = e.detail.patchNum;
     this.fixSuggestions = e.detail.fixSuggestions;
     assert(this.fixSuggestions.length > 0, 'no fix in the event');
@@ -212,9 +252,6 @@
       this.showSelectedFixSuggestion(this.fixSuggestions[0]),
       this.applyFixOverlay?.open()
     );
-    return Promise.all(promises).then(() => {
-      if (this.applyFixOverlay) fireEvent(this.applyFixOverlay, 'iron-resize');
-    });
   }
 
   private async showSelectedFixSuggestion(fixSuggestion: FixSuggestionInfo) {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index b6e4a95..4d2d454 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -52,7 +52,7 @@
   }
 
   async function open(detail: OpenFixPreviewEventDetail) {
-    await element.open(
+    element.open(
       new CustomEvent<OpenFixPreviewEventDetail>(EventType.OPEN_FIX_PREVIEW, {
         detail,
       })
diff --git a/polygerrit-ui/app/elements/lit/fit-controller.ts b/polygerrit-ui/app/elements/lit/fit-controller.ts
new file mode 100644
index 0000000..423a4f0
--- /dev/null
+++ b/polygerrit-ui/app/elements/lit/fit-controller.ts
@@ -0,0 +1,213 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {ReactiveController, ReactiveControllerHost} from 'lit';
+
+export interface FitControllerHost {
+  /**
+   * This offset will increase or decrease the distance to the left side
+   * of the screen, a negative offset will move the dropdown to the left
+   * a positive one, to the right.
+   *
+   */
+  horizontalOffset: number;
+
+  /**
+   * This offset will increase or decrease the distance to the top
+   * side of the screen: a negative offset will move the dropdown upwards
+   * , a positive one, downwards.
+   *
+   */
+  verticalOffset: number;
+}
+
+export interface PositionStyles {
+  top: string;
+  left: string;
+  position: string;
+  maxWidth: string;
+  maxHeight: string;
+  boxSizing: string;
+}
+
+/**
+ * `FitController` fits an element in another element using `max-height`
+ * and `max-width`.
+ *
+ * FitController overrides all properties defined in PositionStyles for the
+ * host.
+ * The element will only be sized and/or positioned if it has not already been
+ * sized and/or positioned by CSS.
+ *  CSS properties            | Action
+ * --------------------------|-------------------------------------------
+ * `position` set            | Element is not centered horizontally/vertically
+ * `top` or `bottom` set     | Element is not vertically centered
+ * `left` or `right` set     | Element is not horizontally centered
+ * `max-height` set          | Element respects `max-height`
+ * `max-width` set           | Element respects `max-width`
+ *
+ * `FitController` positions an element into another element and gives it
+ * a horizontalAlignment = left and verticalAlignment = top.
+ * This will override the element's css position.
+ *
+ * Use `horizontalOffset, verticalOffset` to offset the element from its
+ * `positionTarget`; `FitController` will collapse these in order to
+ * keep the element within `window` boundaries, while preserving the element's
+ * CSS margin values.
+ *
+ */
+export class FitController implements ReactiveController {
+  host: ReactiveControllerHost & HTMLElement & FitControllerHost;
+
+  private originalStyles?: PositionStyles;
+
+  private positionTarget?: HTMLElement;
+
+  constructor(host: ReactiveControllerHost & HTMLElement & FitControllerHost) {
+    (this.host = host).addController(this);
+  }
+
+  hostConnected() {
+    this.positionTarget = this.getPositionTarget();
+  }
+
+  hostDisconnected() {}
+
+  // private but used in tests
+  getPositionTarget() {
+    let parent = this.host.parentNode;
+
+    if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+      parent = (parent as ShadowRoot).host;
+    }
+
+    return parent as HTMLElement;
+  }
+
+  private saveOriginalStyles() {
+    // These properties are changed in position() hence keep the original
+    // values to reset the host styles later.
+    this.originalStyles = {
+      top: this.host.style.top || '',
+      left: this.host.style.left || '',
+      position: this.host.style.position || '',
+      maxWidth: this.host.style.maxWidth || '',
+      maxHeight: this.host.style.maxHeight || '',
+      boxSizing: this.host.style.boxSizing || '',
+    };
+  }
+
+  /**
+   * Reset the host style, and clear the memoized data.
+   */
+  private resetStyles() {
+    // It is necessary to clear the max-width:0px and max-height:0px.
+    // A component may call refit() multiple times, in which case we don't
+    // want the values assigned from the first call which may not be precisely
+    // correct to influence the second call.
+    // Hence we reset the styles here.
+    if (this.originalStyles !== undefined) {
+      Object.assign(this.host.style, this.originalStyles);
+    }
+    this.originalStyles = undefined;
+  }
+
+  setPositionTarget(target: HTMLElement) {
+    this.positionTarget = target;
+  }
+
+  /**
+   * Equivalent to calling `resetStyles()` and `position()`.
+   * Useful to call this after the element or the `window` element has
+   * been resized, or if any of the positioning properties
+   * (e.g. `horizontalOffset, verticalOffset`) are updated.
+   * It preserves the scroll position of the host.
+   */
+  refit() {
+    const scrollLeft = this.host.scrollLeft;
+    const scrollTop = this.host.scrollTop;
+    this.resetStyles();
+    this.position();
+    this.host.scrollLeft = scrollLeft;
+    this.host.scrollTop = scrollTop;
+  }
+
+  private position() {
+    this.saveOriginalStyles();
+
+    this.host.style.position = 'fixed';
+    // Need border-box for margin/padding.
+    this.host.style.boxSizing = 'border-box';
+
+    const hostRect = this.host.getBoundingClientRect();
+    const positionRect = this.getNormalizedRect(this.positionTarget!);
+    const windowRect = this.getNormalizedRect(window);
+
+    this.calculateAndSetPositions(hostRect, positionRect, windowRect);
+  }
+
+  // private but used in tests
+  calculateAndSetPositions(
+    hostRect: DOMRect,
+    positionRect: DOMRect,
+    windowRect: DOMRect
+  ) {
+    const hostStyles = (window as Window).getComputedStyle(this.host);
+    const hostMinWidth = parseInt(hostStyles.minWidth) || 0;
+    const hostMinHeight = parseInt(hostStyles.minHeight) || 0;
+
+    const hostMargin = {
+      top: parseInt(hostStyles.marginTop) || 0,
+      right: parseInt(hostStyles.marginRight) || 0,
+      bottom: parseInt(hostStyles.marginBottom) || 0,
+      left: parseInt(hostStyles.marginLeft) || 0,
+    };
+
+    let leftPosition =
+      positionRect.left + this.host.horizontalOffset + hostMargin.left;
+    let topPosition =
+      positionRect.top + this.host.verticalOffset + hostMargin.top;
+
+    // Limit right/bottom within window respecting the margin.
+    const rightPosition = Math.min(
+      windowRect.right - hostMargin.right,
+      leftPosition + hostRect.width
+    );
+    const bottomPosition = Math.min(
+      windowRect.bottom - hostMargin.bottom,
+      topPosition + hostRect.height
+    );
+
+    // Respect hostMinWidth and hostMinHeight
+    // Current width is rightPosition - leftPosition or hostRect.width
+    //    rightPosition - leftPosition >= hostMinWidth
+    // => leftPosition <= rightPosition - hostMinWidth
+    leftPosition = Math.min(leftPosition, rightPosition - hostMinWidth);
+    topPosition = Math.min(topPosition, bottomPosition - hostMinHeight);
+
+    // Limit left/top within window respecting the margin.
+    leftPosition = Math.max(windowRect.left + hostMargin.left, leftPosition);
+    topPosition = Math.max(windowRect.top + hostMargin.top, topPosition);
+
+    // Use right/bottom to set maxWidth/maxHeight and respect
+    // minWidth/minHeight.
+    const maxWidth = Math.max(rightPosition - leftPosition, hostMinWidth);
+    const maxHeight = Math.max(bottomPosition - topPosition, hostMinHeight);
+
+    this.host.style.maxWidth = `${maxWidth}px`;
+    this.host.style.maxHeight = `${maxHeight}px`;
+
+    this.host.style.left = `${leftPosition}px`;
+    this.host.style.top = `${topPosition}px`;
+  }
+
+  private getNormalizedRect(target: Window | HTMLElement): DOMRect {
+    if (target === document.documentElement || target === window) {
+      return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
+    }
+    return (target as HTMLElement).getBoundingClientRect();
+  }
+}
diff --git a/polygerrit-ui/app/elements/lit/fit-controller_test.ts b/polygerrit-ui/app/elements/lit/fit-controller_test.ts
new file mode 100644
index 0000000..2a60b83
--- /dev/null
+++ b/polygerrit-ui/app/elements/lit/fit-controller_test.ts
@@ -0,0 +1,143 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import '../../test/common-test-setup';
+import {fixture, html, assert} from '@open-wc/testing';
+import {FitController} from './fit-controller';
+import {LitElement} from 'lit';
+import {customElement} from 'lit/decorators.js';
+
+@customElement('fit-element')
+class FitElement extends LitElement {
+  fitController = new FitController(this);
+
+  horizontalOffset = 0;
+
+  verticalOffset = 0;
+
+  override render() {
+    return html`<div></div>`;
+  }
+}
+
+suite('fit controller', () => {
+  let element: FitElement;
+  setup(async () => {
+    element = await fixture(html`<fit-element></fit-element>`);
+  });
+
+  test('refit positioning', async () => {
+    const hostRect = new DOMRect(0, 0, 50, 50);
+
+    const positionRect = new DOMRect(37, 37, 300, 60);
+
+    const windowRect = new DOMRect(0, 0, 600, 600);
+
+    element.fitController.calculateAndSetPositions(
+      hostRect,
+      positionRect,
+      windowRect
+    );
+
+    assert.equal(element.style.top, '37px');
+    assert.equal(element.style.left, '37px');
+  });
+
+  test('refit positioning with offset', async () => {
+    const elementWithOffset: FitElement = await fixture(
+      html`<fit-element></fit-element>`
+    );
+    elementWithOffset.verticalOffset = 10;
+    elementWithOffset.horizontalOffset = 20;
+
+    const hostRect = new DOMRect(0, 0, 50, 50);
+
+    const positionRect = new DOMRect(37, 37, 300, 60);
+
+    const windowRect = new DOMRect(0, 0, 600, 600);
+
+    elementWithOffset.fitController.calculateAndSetPositions(
+      hostRect,
+      positionRect,
+      windowRect
+    );
+
+    assert.equal(elementWithOffset.style.top, '47px');
+    assert.equal(elementWithOffset.style.left, '57px');
+  });
+
+  test('host margin updates positioning', async () => {
+    const hostRect = new DOMRect(0, 0, 50, 50);
+
+    const positionRect = new DOMRect(37, 37, 300, 60);
+
+    const windowRect = new DOMRect(0, 0, 600, 600);
+
+    element.style.marginLeft = '10px';
+    element.style.marginTop = '10px';
+    element.fitController.calculateAndSetPositions(
+      hostRect,
+      positionRect,
+      windowRect
+    );
+
+    // is 10px extra from the previous test due to host margin
+    assert.equal(element.style.top, '47px');
+    assert.equal(element.style.left, '47px');
+  });
+
+  test('host minWidth, minHeight overrides positioning', async () => {
+    const hostRect = new DOMRect(0, 0, 50, 50);
+
+    const positionRect = new DOMRect(37, 37, 300, 60);
+
+    const windowRect = new DOMRect(0, 0, 600, 600);
+
+    element.style.marginLeft = '10px';
+    element.style.marginTop = '10px';
+
+    element.style.minHeight = '50px';
+    element.style.minWidth = '60px';
+
+    element.fitController.calculateAndSetPositions(
+      hostRect,
+      positionRect,
+      windowRect
+    );
+
+    assert.equal(element.style.top, '47px');
+
+    // Should be 47 like the previous test but that would make it overall
+    // smaller in width than the minWidth defined
+    assert.equal(element.style.left, '37px');
+    assert.equal(element.style.maxWidth, '60px');
+  });
+
+  test('positioning happens within window size ', async () => {
+    const hostRect = new DOMRect(0, 0, 50, 50);
+
+    const positionRect = new DOMRect(37, 37, 300, 60);
+
+    // window size is small hence limits the position
+    const windowRect = new DOMRect(0, 0, 50, 50);
+
+    element.style.marginLeft = '10px';
+    element.style.marginTop = '10px';
+
+    element.fitController.calculateAndSetPositions(
+      hostRect,
+      positionRect,
+      windowRect
+    );
+
+    assert.equal(element.style.top, '47px');
+    assert.equal(element.style.left, '47px');
+    // With the window size being 50, the element is styled with width 3px
+    // width = windowSize - leftPosition = 50 - 47 = 3px
+    // Without the window width restriction, in previous test maxWidth is 60px
+    assert.equal(element.style.maxWidth, '3px');
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index 1250d49..e459c05 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -3,24 +3,16 @@
  * Copyright 2017 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import '@polymer/iron-dropdown/iron-dropdown';
 import '../gr-cursor-manager/gr-cursor-manager';
 import '../../../styles/shared-styles';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-autocomplete-dropdown_html';
-import {IronFitMixin} from '../../../mixins/iron-fit-mixin/iron-fit-mixin';
-import {customElement, property, observe} from '@polymer/decorators';
-import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior';
 import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager';
 import {fireEvent} from '../../../utils/event-util';
 import {addShortcut, Key} from '../../../utils/dom-util';
-
-export interface GrAutocompleteDropdown {
-  $: {
-    suggestions: Element;
-  };
-}
+import {FitController} from '../../lit/fit-controller';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query} from 'lit/decorators.js';
+import {repeat} from 'lit/directives/repeat.js';
+import {sharedStyles} from '../../../styles/shared-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -41,19 +33,8 @@
   selected: HTMLElement | null;
 }
 
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = IronFitMixin(PolymerElement, IronFitBehavior as IronFitBehavior);
-
-/**
- * @attr {String} vertical-align - inherited from IronOverlay
- * @attr {String} horizontal-align - inherited from IronOverlay
- */
 @customElement('gr-autocomplete-dropdown')
-export class GrAutocompleteDropdown extends base {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrAutocompleteDropdown extends LitElement {
   /**
    * Fired when the dropdown is closed.
    *
@@ -69,24 +50,84 @@
   @property({type: Number})
   index: number | null = null;
 
-  @property({type: Boolean, reflectToAttribute: true})
+  @property({type: Boolean, reflect: true, attribute: 'is-hidden'})
   isHidden = true;
 
   @property({type: Number})
-  override verticalOffset: number | null = null;
+  verticalOffset = 0;
 
   @property({type: Number})
-  override horizontalOffset: number | null = null;
+  horizontalOffset = 0;
 
   @property({type: Array})
   suggestions: Item[] = [];
 
+  @query('#suggestions') suggestionsDiv?: HTMLDivElement;
+
   /** Called in disconnectedCallback. */
   private cleanups: (() => void)[] = [];
 
   // visible for testing
   cursor = new GrCursorManager();
 
+  // visible for testing
+  fitController = new FitController(this);
+
+  static override get styles() {
+    return [
+      sharedStyles,
+      css`
+        :host {
+          z-index: 100;
+        }
+        :host([is-hidden]) {
+          display: none;
+        }
+        ul {
+          list-style: none;
+        }
+        li {
+          border-bottom: 1px solid var(--border-color);
+          cursor: pointer;
+          display: flex;
+          justify-content: space-between;
+          padding: var(--spacing-m) var(--spacing-l);
+        }
+        li:last-of-type {
+          border: none;
+        }
+        li:focus {
+          outline: none;
+        }
+        li:hover {
+          background-color: var(--hover-background-color);
+        }
+        li.selected {
+          background-color: var(--hover-background-color);
+        }
+        .dropdown-content {
+          background: var(--dropdown-background-color);
+          box-shadow: var(--elevation-level-2);
+          border-radius: var(--border-radius);
+          max-height: 50vh;
+          overflow: auto;
+        }
+        @media only screen and (max-height: 35em) {
+          .dropdown-content {
+            max-height: 80vh;
+          }
+        }
+        .label {
+          color: var(--deemphasized-text-color);
+          padding-left: var(--spacing-l);
+        }
+        .hide {
+          display: none;
+        }
+      `,
+    ];
+  }
+
   constructor() {
     super();
     this.cursor.cursorTargetClass = 'selected';
@@ -95,20 +136,18 @@
 
   override connectedCallback() {
     super.connectedCallback();
+    this.cleanups.push(addShortcut(this, {key: Key.UP}, () => this.handleUp()));
     this.cleanups.push(
-      addShortcut(this, {key: Key.UP}, () => this._handleUp())
+      addShortcut(this, {key: Key.DOWN}, () => this.handleDown())
     );
     this.cleanups.push(
-      addShortcut(this, {key: Key.DOWN}, () => this._handleDown())
+      addShortcut(this, {key: Key.ENTER}, () => this.handleEnter())
     );
     this.cleanups.push(
-      addShortcut(this, {key: Key.ENTER}, () => this._handleEnter())
+      addShortcut(this, {key: Key.ESC}, () => this.handleEscape())
     );
     this.cleanups.push(
-      addShortcut(this, {key: Key.ESC}, () => this._handleEscape())
-    );
-    this.cleanups.push(
-      addShortcut(this, {key: Key.TAB}, () => this._handleTab())
+      addShortcut(this, {key: Key.TAB}, () => this.handleTab())
     );
   }
 
@@ -119,6 +158,51 @@
     super.disconnectedCallback();
   }
 
+  override willUpdate(changedProperties: PropertyValues) {
+    if (changedProperties.has('index')) {
+      this.setIndex();
+    }
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('suggestions')) {
+      this.onSuggestionsChanged();
+    }
+  }
+
+  override render() {
+    return html`
+      <div
+        class="dropdown-content"
+        slot="dropdown-content"
+        id="suggestions"
+        role="listbox"
+      >
+        <ul>
+          ${repeat(
+            this.suggestions,
+            (item, index) => html`
+              <li
+                data-index=${index}
+                data-value=${item.dataValue ?? ''}
+                tabindex="-1"
+                aria-label=${item.name ?? ''}
+                class="autocompleteOption"
+                role="option"
+                @click=${this.handleClickItem}
+              >
+                <span>${item.text}</span>
+                <span class="label ${this.computeLabelClass(item)}"
+                  >${item.label}</span
+                >
+              </li>
+            `
+          )}
+        </ul>
+      </div>
+    `;
+  }
+
   close() {
     this.isHidden = true;
   }
@@ -132,11 +216,15 @@
     return this.getCursorTarget()?.dataset['value'] || '';
   }
 
-  _handleUp() {
+  setPositionTarget(target: HTMLElement) {
+    this.fitController?.setPositionTarget(target);
+  }
+
+  private handleUp() {
     if (!this.isHidden) this.cursorUp();
   }
 
-  _handleDown() {
+  private handleDown() {
     if (!this.isHidden) this.cursorDown();
   }
 
@@ -148,7 +236,8 @@
     if (!this.isHidden) this.cursor.previous();
   }
 
-  _handleTab() {
+  // private but used in tests
+  handleTab() {
     this.dispatchEvent(
       new CustomEvent<ItemSelectedEvent>('item-selected', {
         detail: {
@@ -161,7 +250,8 @@
     );
   }
 
-  _handleEnter() {
+  // private but used in tests
+  handleEnter() {
     this.dispatchEvent(
       new CustomEvent<ItemSelectedEvent>('item-selected', {
         detail: {
@@ -174,12 +264,12 @@
     );
   }
 
-  _handleEscape() {
-    this._fireClose();
+  private handleEscape() {
+    this.fireClose();
     this.close();
   }
 
-  _handleClickItem(e: Event) {
+  private handleClickItem(e: Event) {
     e.preventDefault();
     e.stopPropagation();
     let selected = e.target! as HTMLElement;
@@ -201,7 +291,7 @@
     );
   }
 
-  _fireClose() {
+  private fireClose() {
     fireEvent(this, 'dropdown-closed');
   }
 
@@ -209,32 +299,29 @@
     return this.cursor.target;
   }
 
-  @observe('suggestions')
   onSuggestionsChanged() {
     if (this.suggestions.length > 0) {
       if (!this.isHidden) {
-        flush();
         this.cursor.stops = Array.from(
-          this.$.suggestions.querySelectorAll('li')
+          this.suggestionsDiv?.querySelectorAll('li') ?? []
         );
-        this._resetCursorIndex();
+        this.resetCursorIndex();
       }
     } else {
       this.cursor.stops = [];
     }
-    this.refit();
+    this.fitController?.refit();
   }
 
-  @observe('index')
-  _setIndex() {
+  private setIndex() {
     this.cursor.index = this.index || -1;
   }
 
-  _resetCursorIndex() {
+  private resetCursorIndex() {
     this.cursor.setCursorAtIndex(0);
   }
 
-  _computeLabelClass(item: Item) {
+  private computeLabelClass(item: Item) {
     return item.label ? '' : 'hide';
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.ts
deleted file mode 100644
index e10d356..0000000
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    :host {
-      z-index: 100;
-    }
-    :host([is-hidden]) {
-      display: none;
-    }
-    ul {
-      list-style: none;
-    }
-    li {
-      border-bottom: 1px solid var(--border-color);
-      cursor: pointer;
-      display: flex;
-      justify-content: space-between;
-      padding: var(--spacing-m) var(--spacing-l);
-    }
-    li:last-of-type {
-      border: none;
-    }
-    li:focus {
-      outline: none;
-    }
-    li:hover {
-      background-color: var(--hover-background-color);
-    }
-    li.selected {
-      background-color: var(--hover-background-color);
-    }
-    .dropdown-content {
-      background: var(--dropdown-background-color);
-      box-shadow: var(--elevation-level-2);
-      border-radius: var(--border-radius);
-      max-height: 50vh;
-      overflow: auto;
-    }
-    @media only screen and (max-height: 35em) {
-      .dropdown-content {
-        max-height: 80vh;
-      }
-    }
-    .label {
-      color: var(--deemphasized-text-color);
-      padding-left: var(--spacing-l);
-    }
-    .hide {
-      display: none;
-    }
-  </style>
-  <div
-    class="dropdown-content"
-    slot="dropdown-content"
-    id="suggestions"
-    role="listbox"
-  >
-    <ul>
-      <template is="dom-repeat" items="[[suggestions]]">
-        <li
-          data-index$="[[index]]"
-          data-value$="[[item.dataValue]]"
-          tabindex="-1"
-          aria-label$="[[item.name]]"
-          class="autocompleteOption"
-          role="option"
-          on-click="_handleClickItem"
-        >
-          <span>[[item.text]]</span>
-          <span class$="label [[_computeLabelClass(item)]]"
-            >[[item.label]]</span
-          >
-        </li>
-      </template>
-    </ul>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
index a75fca2..9a0ed5e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.ts
@@ -11,6 +11,7 @@
   queryAll,
   queryAndAssert,
   waitEventLoop,
+  waitUntil,
 } from '../../../test/test-utils';
 import {assertIsDefined} from '../../../utils/common-util';
 import {fixture, html, assert} from '@open-wc/testing';
@@ -70,9 +71,6 @@
               <span> 2 </span>
               <span class="hide label"> </span>
             </li>
-            <dom-repeat style="display: none;">
-              <template is="dom-repeat"> </template>
-            </dom-repeat>
           </ul>
         </div>
       `
@@ -93,7 +91,7 @@
   });
 
   test('tab key', () => {
-    const handleTabSpy = sinon.spy(element, '_handleTab');
+    const handleTabSpy = sinon.spy(element, 'handleTab');
     const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
     pressKey(element, Key.TAB);
@@ -107,7 +105,7 @@
   });
 
   test('enter key', () => {
-    const handleEnterSpy = sinon.spy(element, '_handleEnter');
+    const handleEnterSpy = sinon.spy(element, 'handleEnter');
     const itemSelectedStub = sinon.stub();
     element.addEventListener('item-selected', itemSelectedStub);
     pressKey(element, Key.ENTER);
@@ -171,9 +169,9 @@
     });
   });
 
-  test('updated suggestions resets cursor stops', () => {
+  test('updated suggestions resets cursor stops', async () => {
     const resetStopsSpy = sinon.spy(element, 'onSuggestionsChanged');
     element.suggestions = [];
-    assert.isTrue(resetStopsSpy.called);
+    await waitUntil(() => resetStopsSpy.called);
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 90fe32e..34e67b9 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -312,9 +312,7 @@
         </div>
       </paper-input>
       <gr-autocomplete-dropdown
-        vertical-align="top"
         .verticalOffset=${this.verticalOffset}
-        horizontal-align="left"
         id="suggestions"
         @item-selected=${this.handleItemSelect}
         @dropdown-closed=${this.focusWithoutDisplayingSuggestions}
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index 9be28e7..d991180 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -48,12 +48,10 @@
           </div>
         </paper-input>
         <gr-autocomplete-dropdown
-          horizontal-align="left"
           id="suggestions"
           is-hidden=""
           role="listbox"
           style="position: fixed; top: 300px; left: 392.5px; box-sizing: border-box; max-height: 600px; max-width: 785px;"
-          vertical-align="top"
         >
         </gr-autocomplete-dropdown>
       `,
@@ -99,12 +97,7 @@
             <slot name="suffix"> </slot>
           </div>
         </paper-input>
-        <gr-autocomplete-dropdown
-          horizontal-align="left"
-          id="suggestions"
-          role="listbox"
-          vertical-align="top"
-        >
+        <gr-autocomplete-dropdown id="suggestions" role="listbox">
         </gr-autocomplete-dropdown>
       `,
       {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 6ab0ec4..5a1db30 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -16,11 +16,7 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {configModelToken} from '../../../models/config/config-model';
 import {CommentLinks, EmailAddress} from '../../../api/rest-api';
-import {
-  applyHtmlRewritesFromConfig,
-  applyLinkRewritesFromConfig,
-  linkifyNormalUrls,
-} from '../../../utils/link-util';
+import {linkifyUrlsAndApplyRewrite} from '../../../utils/link-util';
 import '../gr-account-chip/gr-account-chip';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {getAppContext} from '../../../services/app-context';
@@ -125,7 +121,7 @@
   }
 
   private renderAsPlaintext() {
-    const linkedText = this.rewriteText(
+    const linkedText = linkifyUrlsAndApplyRewrite(
       htmlEscape(this.content).toString(),
       this.repoCommentLinks
     );
@@ -140,7 +136,7 @@
     // renderer so we wrap 'this.rewriteText' so that 'this' is preserved via
     // closure.
     const boundRewriteText = (text: string) =>
-      this.rewriteText(text, this.repoCommentLinks);
+      linkifyUrlsAndApplyRewrite(text, this.repoCommentLinks);
 
     // We are overriding some marked-element renderers for a few reasons:
     // 1. Disable inline images as a design/policy choice.
@@ -201,24 +197,6 @@
     return text;
   }
 
-  private rewriteText(text: string, repoCommentLinks: CommentLinks) {
-    // Turn universally identifiable URLs into links. Ex: www.google.com. The
-    // markdown library inside marked-element does this too, but is more
-    // conservative and misses some URLs like "google.com" without "www" prefix.
-    text = linkifyNormalUrls(text);
-
-    // Apply the host's config-specific regex replacements to create links. Ex:
-    // link "Bug 12345" to "google.com/bug/12345"
-    text = applyLinkRewritesFromConfig(text, repoCommentLinks);
-
-    // Apply the host's config-specific regex replacements to write arbitrary
-    // html. Most examples seen in the wild are also used for linking but with
-    // finer control over the rendered text. Ex: "Bug 12345" => "#12345"
-    text = applyHtmlRewritesFromConfig(text, repoCommentLinks);
-
-    return text;
-  }
-
   override updated() {
     // Look for @mentions and replace them with an account-label chip.
     if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 6391347..ca49831 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -77,6 +77,76 @@
       await element.updateComplete;
     });
 
+    test('does not apply rewrites within links', async () => {
+      element.content = 'google.com/LinkRewriteMe';
+      await element.updateComplete;
+
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <pre class="plaintext">
+            <a
+              href="http://google.com/LinkRewriteMe"
+              rel="noopener"
+              target="_blank"
+            >
+              google.com/LinkRewriteMe
+            </a>
+          </pre>
+        `
+      );
+    });
+
+    test('does not apply rewrites on rewritten text', async () => {
+      await setCommentLinks({
+        capitalizeFoo: {
+          match: 'foo',
+          html: 'FOO',
+        },
+        lowercaseFoo: {
+          match: 'FOO',
+          html: 'foo',
+        },
+      });
+      element.content = 'foo';
+      await element.updateComplete;
+
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <pre class="plaintext">
+          FOO
+        </pre
+          >
+        `
+      );
+    });
+
+    test('supports overlapping rewrites', async () => {
+      await setCommentLinks({
+        bracketNum: {
+          match: '(Start:) ([0-9]+)',
+          html: '$1 [$2]',
+        },
+        bracketNum2: {
+          match: '(Start: [0-9]+) ([0-9]+)',
+          html: '$1 [$2]',
+        },
+      });
+      element.content = 'Start: 123 456';
+      await element.updateComplete;
+
+      assert.shadowDom.equal(
+        element,
+        /* HTML */ `
+          <pre class="plaintext">
+            Start: [123] [456]
+          </pre
+          >
+        `
+      );
+    });
+
     test('renders text with links and rewrites', async () => {
       element.content = `text with plain link: google.com
         \ntext with config link: LinkRewriteMe
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts
deleted file mode 100644
index 16a60e7..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * @license
- * Copyright 2015 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../styles/shared-styles';
-import {GrLinkTextParser, LinkTextParserConfig} from './link-text-parser';
-import {LitElement, css, html, PropertyValues} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {assertIsDefined} from '../../../utils/common-util';
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'gr-linked-text': GrLinkedText;
-  }
-}
-
-@customElement('gr-linked-text')
-export class GrLinkedText extends LitElement {
-  private outputElement?: HTMLSpanElement;
-
-  @property({type: Boolean, attribute: 'remove-zero-width-space'})
-  removeZeroWidthSpace?: boolean;
-
-  @property({type: String})
-  content = '';
-
-  @property({type: Boolean, attribute: true})
-  pre = false;
-
-  @property({type: Boolean, attribute: true})
-  disabled = false;
-
-  @property({type: Boolean, attribute: true})
-  inline = false;
-
-  @property({type: Object})
-  config?: LinkTextParserConfig;
-
-  static override get styles() {
-    return css`
-      :host {
-        display: block;
-      }
-      :host([inline]) {
-        display: inline;
-      }
-      :host([pre]) ::slotted(span) {
-        white-space: var(--linked-text-white-space, pre-wrap);
-        word-wrap: var(--linked-text-word-wrap, break-word);
-      }
-    `;
-  }
-
-  override render() {
-    return html`<slot name="insert"></slot>`;
-  }
-
-  // NOTE: LinkTextParser dynamically creates HTML fragments based on backend
-  // configuration commentLinks. These commentLinks can contain arbitrary HTML
-  // fragments. This means that arbitrary HTML needs to be injected into the
-  // DOM-tree, where this HTML is is controlled on the server-side in the
-  // server-configuration rather than by arbitrary users.
-  // To enable this injection of 'unsafe' HTML, LinkTextParser generates
-  // HTML fragments. Lit does not support inserting html fragments directly
-  // into its DOM-tree as it controls the DOM-tree that it generates.
-  // Therefore, to get around this we create a single element that we slot into
-  // the Lit-owned DOM.  This element will not be part of this LitElement as
-  // it's slotted in and thus can be modified on the fly by handleParseResult.
-  override firstUpdated(_changedProperties: PropertyValues): void {
-    this.outputElement = document.createElement('span');
-    this.outputElement.id = 'output';
-    this.outputElement.slot = 'insert';
-    this.append(this.outputElement);
-  }
-
-  override updated(changedProperties: PropertyValues): void {
-    if (changedProperties.has('content') || changedProperties.has('config')) {
-      this._contentOrConfigChanged();
-    } else if (changedProperties.has('disabled')) {
-      this.styleLinks();
-    }
-  }
-
-  /**
-   * Because either the source text or the linkification config has changed,
-   * the content should be re-parsed.
-   * Private but used in tests.
-   *
-   * @param content The raw, un-linkified source string to parse.
-   * @param config The server config specifying commentLink patterns
-   */
-  _contentOrConfigChanged() {
-    if (!this.config) {
-      assertIsDefined(this.outputElement);
-      this.outputElement.textContent = this.content;
-      return;
-    }
-
-    assertIsDefined(this.outputElement);
-    this.outputElement.textContent = '';
-    const parser = new GrLinkTextParser(
-      this.config,
-      (text: string | null, href: string | null, fragment?: DocumentFragment) =>
-        this.handleParseResult(text, href, fragment),
-      this.removeZeroWidthSpace
-    );
-    parser.parse(this.content);
-
-    // Ensure that external links originating from HTML commentlink configs
-    // open in a new tab. @see Issue 5567
-    // Ensure links to the same host originating from commentlink configs
-    // open in the same tab. When target is not set - default is _self
-    // @see Issue 4616
-    this.outputElement.querySelectorAll('a').forEach(anchor => {
-      if (anchor.hostname === window.location.hostname) {
-        anchor.removeAttribute('target');
-      } else {
-        anchor.setAttribute('target', '_blank');
-      }
-      anchor.setAttribute('rel', 'noopener');
-    });
-
-    this.styleLinks();
-  }
-
-  /**
-   * Styles the links based on whether gr-linked-text is disabled or not
-   */
-  private styleLinks() {
-    assertIsDefined(this.outputElement);
-    this.outputElement.querySelectorAll('a').forEach(anchor => {
-      anchor.setAttribute('style', this.computeLinkStyle());
-    });
-  }
-
-  private computeLinkStyle() {
-    if (this.disabled) {
-      return `
-        color: inherit;
-        text-decoration: none;
-        pointer-events: none;
-      `;
-    } else {
-      return 'color: var(--link-color)';
-    }
-  }
-
-  /**
-   * This method is called when the GrLikTextParser emits a partial result
-   * (used as the "callback" parameter). It will be called in either of two
-   * ways:
-   * - To create a link: when called with `text` and `href` arguments, a link
-   *   element should be created and attached to the resulting DOM.
-   * - To attach an arbitrary fragment: when called with only the `fragment`
-   *   argument, the fragment should be attached to the resulting DOM as is.
-   */
-  private handleParseResult(
-    text: string | null,
-    href: string | null,
-    fragment?: DocumentFragment
-  ) {
-    assertIsDefined(this.outputElement);
-    const output = this.outputElement;
-    if (href) {
-      const a = document.createElement('a');
-      a.setAttribute('href', href);
-      // GrLinkTextParser either pass text and href together or
-      // only DocumentFragment - see LinkTextParserCallback
-      a.textContent = text!;
-      a.target = '_blank';
-      a.setAttribute('rel', 'noopener');
-      output.appendChild(a);
-    } else if (fragment) {
-      output.appendChild(fragment);
-    }
-  }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
deleted file mode 100644
index 00e0313..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.ts
+++ /dev/null
@@ -1,471 +0,0 @@
-/**
- * @license
- * Copyright 2015 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import './gr-linked-text';
-import {fixture, html, assert} from '@open-wc/testing';
-import {GrLinkedText} from './gr-linked-text';
-import {queryAndAssert} from '../../../test/test-utils';
-
-suite('gr-linked-text tests', () => {
-  let element: GrLinkedText;
-
-  let originalCanonicalPath: string | undefined;
-
-  setup(async () => {
-    originalCanonicalPath = window.CANONICAL_PATH;
-    element = await fixture<GrLinkedText>(html`
-      <gr-linked-text>
-        <div id="output"></div>
-      </gr-linked-text>
-    `);
-
-    element.config = {
-      ph: {
-        match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
-        link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
-      },
-      prefixsameinlinkandpattern: {
-        match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)',
-        link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2',
-      },
-      changeid: {
-        match: '(I[0-9a-f]{8,40})',
-        link: '#/q/$1',
-      },
-      changeid2: {
-        match: 'Change-Id: +(I[0-9a-f]{8,40})',
-        link: '#/q/$1',
-      },
-      googlesearch: {
-        match: 'google:(.+)',
-        link: 'https://bing.com/search?q=$1', // html should supersede link.
-        html: '<a href="https://google.com/search?q=$1">$1</a>',
-      },
-      hashedhtml: {
-        match: 'hash:(.+)',
-        html: '<a href="#/awesomesauce">$1</a>',
-      },
-      baseurl: {
-        match: 'test (.+)',
-        html: '<a href="/r/awesomesauce">$1</a>',
-      },
-      anotatstartwithbaseurl: {
-        match: 'a test (.+)',
-        html: '[Lookup: <a href="/r/awesomesauce">$1</a>]',
-      },
-      disabledconfig: {
-        match: 'foo:(.+)',
-        link: 'https://google.com/search?q=$1',
-        enabled: false,
-      },
-    };
-  });
-
-  teardown(() => {
-    window.CANONICAL_PATH = originalCanonicalPath;
-  });
-
-  test('render', async () => {
-    element.content =
-      'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-    await element.updateComplete;
-    assert.lightDom.equal(
-      element,
-      /* HTML */ `
-        <div id="output"></div>
-        <span id="output" slot="insert">
-          <a
-            href="https://bugs.chromium.org/p/gerrit/issues/detail?id=3650"
-            rel="noopener"
-            style="color: var(--link-color)"
-            target="_blank"
-          >
-            https://bugs.chromium.org/p/gerrit/issues/detail?id=3650
-          </a>
-        </span>
-      `
-    );
-  });
-
-  test('URL pattern was parsed and linked.', async () => {
-    // Regular inline link.
-    const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-    element.content = url;
-    await element.updateComplete;
-
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    assert.equal(linkEl.target, '_blank');
-    assert.equal(linkEl.rel, 'noopener');
-    assert.equal(linkEl.href, url);
-    assert.equal(linkEl.textContent, url);
-  });
-
-  test('Bug pattern was parsed and linked', async () => {
-    // "Issue/Bug" pattern.
-    element.content = 'Issue 3650';
-    await element.updateComplete;
-
-    let linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-    assert.equal(linkEl.target, '_blank');
-    assert.equal(linkEl.href, url);
-    assert.equal(linkEl.textContent, 'Issue 3650');
-
-    element.content = 'Bug 3650';
-    await element.updateComplete;
-
-    linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    assert.equal(linkEl.target, '_blank');
-    assert.equal(linkEl.rel, 'noopener');
-    assert.equal(linkEl.href, url);
-    assert.equal(linkEl.textContent, 'Bug 3650');
-  });
-
-  test('Pattern with same prefix as link was correctly parsed', async () => {
-    // Pattern starts with the same prefix (`http`) as the url.
-    element.content = 'httpexample 3650';
-    await element.updateComplete;
-
-    assert.equal(queryAndAssert(element, 'span#output').childNodes.length, 1);
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-    assert.equal(linkEl.target, '_blank');
-    assert.equal(linkEl.href, url);
-    assert.equal(linkEl.textContent, 'httpexample 3650');
-  });
-
-  test('Change-Id pattern was parsed and linked', async () => {
-    // "Change-Id:" pattern.
-    const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
-    const prefix = 'Change-Id: ';
-    element.content = prefix + changeID;
-    await element.updateComplete;
-
-    const textNode = queryAndAssert(element, 'span#output').childNodes[0];
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[1] as HTMLAnchorElement;
-    assert.equal(textNode.textContent, prefix);
-    const url = '/q/' + changeID;
-    assert.isFalse(linkEl.hasAttribute('target'));
-    // Since url is a path, the host is added automatically.
-    assert.isTrue(linkEl.href.endsWith(url));
-    assert.equal(linkEl.textContent, changeID);
-  });
-
-  test('Change-Id pattern was parsed and linked with base url', async () => {
-    window.CANONICAL_PATH = '/r';
-
-    // "Change-Id:" pattern.
-    const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
-    const prefix = 'Change-Id: ';
-    element.content = prefix + changeID;
-    await element.updateComplete;
-
-    const textNode = queryAndAssert(element, 'span#output').childNodes[0];
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[1] as HTMLAnchorElement;
-    assert.equal(textNode.textContent, prefix);
-    const url = '/r/q/' + changeID;
-    assert.isFalse(linkEl.hasAttribute('target'));
-    // Since url is a path, the host is added automatically.
-    assert.isTrue(linkEl.href.endsWith(url));
-    assert.equal(linkEl.textContent, changeID);
-  });
-
-  test('Multiple matches', async () => {
-    element.content = 'Issue 3650\nIssue 3450';
-    await element.updateComplete;
-
-    const linkEl1 = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    const linkEl2 = queryAndAssert(element, 'span#output')
-      .childNodes[2] as HTMLAnchorElement;
-
-    assert.equal(linkEl1.target, '_blank');
-    assert.equal(
-      linkEl1.href,
-      'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'
-    );
-    assert.equal(linkEl1.textContent, 'Issue 3650');
-
-    assert.equal(linkEl2.target, '_blank');
-    assert.equal(
-      linkEl2.href,
-      'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450'
-    );
-    assert.equal(linkEl2.textContent, 'Issue 3450');
-  });
-
-  test('Change-Id pattern parsed before bug pattern', async () => {
-    // "Change-Id:" pattern.
-    const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
-    const prefix = 'Change-Id: ';
-
-    // "Issue/Bug" pattern.
-    const bug = 'Issue 3650';
-
-    const changeUrl = '/q/' + changeID;
-    const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
-
-    element.content = prefix + changeID + bug;
-    await element.updateComplete;
-
-    const textNode = queryAndAssert(element, 'span#output').childNodes[0];
-    const changeLinkEl = queryAndAssert(element, 'span#output')
-      .childNodes[1] as HTMLAnchorElement;
-    const bugLinkEl = queryAndAssert(element, 'span#output')
-      .childNodes[2] as HTMLAnchorElement;
-
-    assert.equal(textNode.textContent, prefix);
-
-    assert.isFalse(changeLinkEl.hasAttribute('target'));
-    assert.isTrue(changeLinkEl.href.endsWith(changeUrl));
-    assert.equal(changeLinkEl.textContent, changeID);
-
-    assert.equal(bugLinkEl.target, '_blank');
-    assert.equal(bugLinkEl.href, bugUrl);
-    assert.equal(bugLinkEl.textContent, 'Issue 3650');
-  });
-
-  test('html field in link config', async () => {
-    element.content = 'google:do a barrel roll';
-    await element.updateComplete;
-
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    assert.equal(
-      linkEl.getAttribute('href'),
-      'https://google.com/search?q=do a barrel roll'
-    );
-    assert.equal(linkEl.textContent, 'do a barrel roll');
-  });
-
-  test('removing hash from links', async () => {
-    element.content = 'hash:foo';
-    await element.updateComplete;
-
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
-    assert.equal(linkEl.textContent, 'foo');
-  });
-
-  test('html with base url', async () => {
-    window.CANONICAL_PATH = '/r';
-
-    element.content = 'test foo';
-    await element.updateComplete;
-
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
-    assert.equal(linkEl.textContent, 'foo');
-  });
-
-  test('a is not at start', async () => {
-    window.CANONICAL_PATH = '/r';
-
-    element.content = 'a test foo';
-    await element.updateComplete;
-
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[1] as HTMLAnchorElement;
-    assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
-    assert.equal(linkEl.textContent, 'foo');
-  });
-
-  test('hash html with base url', async () => {
-    window.CANONICAL_PATH = '/r';
-
-    element.content = 'hash:foo';
-    await element.updateComplete;
-
-    const linkEl = queryAndAssert(element, 'span#output')
-      .childNodes[0] as HTMLAnchorElement;
-    assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
-    assert.equal(linkEl.textContent, 'foo');
-  });
-
-  test('disabled config', async () => {
-    element.content = 'foo:baz';
-    await element.updateComplete;
-
-    assert.equal(queryAndAssert(element, 'span#output').innerHTML, 'foo:baz');
-  });
-
-  test('R=email labels link correctly', async () => {
-    element.removeZeroWidthSpace = true;
-    element.content = 'R=\u200Btest@google.com';
-    await element.updateComplete;
-
-    assert.equal(
-      queryAndAssert(element, 'span#output').textContent,
-      'R=test@google.com'
-    );
-    assert.equal(
-      queryAndAssert(element, 'span#output').innerHTML.match(/(R=<a)/g)!.length,
-      1
-    );
-  });
-
-  test('CC=email labels link correctly', async () => {
-    element.removeZeroWidthSpace = true;
-    element.content = 'CC=\u200Btest@google.com';
-    await element.updateComplete;
-
-    assert.equal(
-      queryAndAssert(element, 'span#output').textContent,
-      'CC=test@google.com'
-    );
-    assert.equal(
-      queryAndAssert(element, 'span#output').innerHTML.match(/(CC=<a)/g)!
-        .length,
-      1
-    );
-  });
-
-  test('only {http,https,mailto} protocols are linkified', async () => {
-    element.content = 'xx mailto:test@google.com yy';
-    await element.updateComplete;
-
-    let links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
-    assert.equal(links[0].innerHTML, 'mailto:test@google.com');
-
-    element.content = 'xx http://google.com yy';
-    await element.updateComplete;
-
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'http://google.com');
-    assert.equal(links[0].innerHTML, 'http://google.com');
-
-    element.content = 'xx https://google.com yy';
-    await element.updateComplete;
-
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'https://google.com');
-    assert.equal(links[0].innerHTML, 'https://google.com');
-
-    element.content = 'xx ssh://google.com yy';
-    await element.updateComplete;
-
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 0);
-
-    element.content = 'xx ftp://google.com yy';
-    await element.updateComplete;
-
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 0);
-  });
-
-  test('links without leading whitespace are linkified', async () => {
-    element.content = 'xx abcmailto:test@google.com yy';
-    await element.updateComplete;
-
-    assert.equal(
-      queryAndAssert(element, 'span#output').innerHTML.substr(0, 6),
-      'xx abc'
-    );
-    let links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com');
-    assert.equal(links[0].innerHTML, 'mailto:test@google.com');
-
-    element.content = 'xx defhttp://google.com yy';
-    await element.updateComplete;
-
-    assert.equal(
-      queryAndAssert(element, 'span#output').innerHTML.substr(0, 6),
-      'xx def'
-    );
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'http://google.com');
-    assert.equal(links[0].innerHTML, 'http://google.com');
-
-    element.content = 'xx qwehttps://google.com yy';
-    await element.updateComplete;
-
-    assert.equal(
-      queryAndAssert(element, 'span#output').innerHTML.substr(0, 6),
-      'xx qwe'
-    );
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'https://google.com');
-    assert.equal(links[0].innerHTML, 'https://google.com');
-
-    // Non-latin character
-    element.content = 'xx абвhttps://google.com yy';
-    await element.updateComplete;
-
-    assert.equal(
-      queryAndAssert(element, 'span#output').innerHTML.substr(0, 6),
-      'xx абв'
-    );
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 1);
-    assert.equal(links[0].getAttribute('href'), 'https://google.com');
-    assert.equal(links[0].innerHTML, 'https://google.com');
-
-    element.content = 'xx ssh://google.com yy';
-    await element.updateComplete;
-
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 0);
-
-    element.content = 'xx ftp://google.com yy';
-    await element.updateComplete;
-
-    links = queryAndAssert(element, 'span#output').querySelectorAll('a');
-    assert.equal(links.length, 0);
-  });
-
-  test('overlapping links', async () => {
-    element.config = {
-      b1: {
-        match: '(B:\\s*)(\\d+)',
-        html: '$1<a href="ftp://foo/$2">$2</a>',
-      },
-      b2: {
-        match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)',
-        html: '$1<a href="ftp://foo/$2">$2</a>',
-      },
-    };
-    element.content = '- B: 123, 45';
-    await element.updateComplete;
-
-    const links = element.querySelectorAll('a');
-
-    assert.equal(links.length, 2);
-    assert.equal(
-      queryAndAssert<HTMLSpanElement>(element, 'span').textContent,
-      '- B: 123, 45'
-    );
-
-    assert.equal(links[0].href, 'ftp://foo/123');
-    assert.equal(links[0].textContent, '123');
-
-    assert.equal(links[1].href, 'ftp://foo/45');
-    assert.equal(links[1].textContent, '45');
-  });
-
-  test('_contentOrConfigChanged called with config', async () => {
-    const contentConfigStub = sinon.stub(element, '_contentOrConfigChanged');
-    element.content = 'some text';
-    await element.updateComplete;
-
-    assert.isTrue(contentConfigStub.called);
-  });
-});
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts
deleted file mode 100644
index 73cf58b..0000000
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts
+++ /dev/null
@@ -1,415 +0,0 @@
-/**
- * @license
- * Copyright 2015 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import 'ba-linkify/ba-linkify';
-import {getBaseUrl} from '../../../utils/url-util';
-import {CommentLinkInfo} from '../../../types/common';
-
-/**
- * Pattern describing URLs with supported protocols.
- */
-const URL_PROTOCOL_PATTERN = /^(.*)(https?:\/\/|mailto:)/;
-
-export type LinkTextParserCallback = ((text: string, href: string) => void) &
-  ((text: null, href: null, fragment: DocumentFragment) => void);
-
-export interface CommentLinkItem {
-  position: number;
-  length: number;
-  html: HTMLAnchorElement | DocumentFragment;
-}
-
-export type LinkTextParserConfig = {[name: string]: CommentLinkInfo};
-
-export class GrLinkTextParser {
-  private readonly baseUrl = getBaseUrl();
-
-  /**
-   * Construct a parser for linkifying text. Will linkify plain URLs that appear
-   * in the text as well as custom links if any are specified in the linkConfig
-   * parameter.
-   *
-   * @param linkConfig Comment links as specified by the commentlinks field on a
-   *     project config.
-   * @param callback The callback to be fired when an intermediate parse result
-   *     is emitted. The callback is passed text and href strings if a link is to
-   *     be created, or a document fragment otherwise.
-   * @param removeZeroWidthSpace If true, zero-width spaces will be removed from
-   *     R=<email> and CC=<email> expressions.
-   */
-  constructor(
-    private readonly linkConfig: LinkTextParserConfig,
-    private readonly callback: LinkTextParserCallback,
-    private readonly removeZeroWidthSpace?: boolean
-  ) {
-    Object.preventExtensions(this);
-  }
-
-  /**
-   * Emit a callback to create a link element.
-   *
-   * @param text The text of the link.
-   * @param href The URL to use as the href of the link.
-   */
-  addText(text: string, href: string) {
-    if (!text) {
-      return;
-    }
-    this.callback(text, href);
-  }
-
-  /**
-   * Given the source text and a list of CommentLinkItem objects that were
-   * generated by the commentlinks config, emit parsing callbacks.
-   *
-   * @param text The chuml of source text over which the outputArray items range.
-   * @param outputArray The list of items to add resulting from commentlink
-   *     matches.
-   */
-  processLinks(text: string, outputArray: CommentLinkItem[]) {
-    this.sortArrayReverse(outputArray);
-    const fragment = document.createDocumentFragment();
-    let cursor = text.length;
-
-    // Start inserting linkified URLs from the end of the String. That way, the
-    // string positions of the items don't change as we iterate through.
-    outputArray.forEach(item => {
-      // Add any text between the current linkified item and the item added
-      // before if it exists.
-      if (item.position + item.length !== cursor) {
-        fragment.insertBefore(
-          document.createTextNode(
-            text.slice(item.position + item.length, cursor)
-          ),
-          fragment.firstChild
-        );
-      }
-      fragment.insertBefore(item.html, fragment.firstChild);
-      cursor = item.position;
-    });
-
-    // Add the beginning portion at the end.
-    if (cursor !== 0) {
-      fragment.insertBefore(
-        document.createTextNode(text.slice(0, cursor)),
-        fragment.firstChild
-      );
-    }
-
-    this.callback(null, null, fragment);
-  }
-
-  /**
-   * Sort the given array of CommentLinkItems such that the positions are in
-   * reverse order.
-   */
-  sortArrayReverse(outputArray: CommentLinkItem[]) {
-    outputArray.sort((a, b) => b.position - a.position);
-  }
-
-  addItem(
-    text: string,
-    href: string,
-    html: null,
-    position: number,
-    length: number,
-    outputArray: CommentLinkItem[]
-  ): void;
-
-  addItem(
-    text: null,
-    href: null,
-    html: string,
-    position: number,
-    length: number,
-    outputArray: CommentLinkItem[]
-  ): void;
-
-  /**
-   * Create a CommentLinkItem and append it to the given output array. This
-   * method can be called in either of two ways:
-   * - With `text` and `href` parameters provided, and the `html` parameter
-   *   passed as `null`. In this case, the new CommentLinkItem will be a link
-   *   element with the given text and href value.
-   * - With the `html` paremeter provided, and the `text` and `href` parameters
-   *   passed as `null`. In this case, the string of HTML will be parsed and the
-   *   first resulting node will be used as the resulting content.
-   *
-   * @param text The text to use if creating a link.
-   * @param href The href to use as the URL if creating a link.
-   * @param html The html to parse and use as the result.
-   * @param  position The position inside the source text where the item
-   *     starts.
-   * @param length The number of characters in the source text
-   *     represented by the item.
-   * @param outputArray The array to which the
-   *     new item is to be appended.
-   */
-  addItem(
-    text: string | null,
-    href: string | null,
-    html: string | null,
-    position: number,
-    length: number,
-    outputArray: CommentLinkItem[]
-  ): void {
-    if (href) {
-      const a = document.createElement('a');
-      a.setAttribute('href', href);
-      a.textContent = text;
-      a.target = '_blank';
-      a.rel = 'noopener';
-      outputArray.push({
-        html: a,
-        position,
-        length,
-      });
-    } else if (html) {
-      // addItem has 2 overloads. If href is null, then html
-      // can't be null.
-      // TODO(TS): remove if(html) and keep else block without condition
-      const fragment = document.createDocumentFragment();
-      // Create temporary div to hold the nodes in.
-      const div = document.createElement('div');
-      div.innerHTML = html;
-      while (div.firstChild) {
-        fragment.appendChild(div.firstChild);
-      }
-      outputArray.push({
-        html: fragment,
-        position,
-        length,
-      });
-    }
-  }
-
-  /**
-   * Create a CommentLinkItem for a link and append it to the given output
-   * array.
-   *
-   * @param text The text for the link.
-   * @param href The href to use as the URL of the link.
-   * @param position The position inside the source text where the link
-   *     starts.
-   * @param length The number of characters in the source text
-   *     represented by the link.
-   * @param outputArray The array to which the
-   *     new item is to be appended.
-   */
-  addLink(
-    text: string,
-    href: string,
-    position: number,
-    length: number,
-    outputArray: CommentLinkItem[]
-  ) {
-    // TODO(TS): remove !test condition
-    if (!text || this.hasOverlap(position, length, outputArray)) {
-      return;
-    }
-    if (
-      !!this.baseUrl &&
-      href.startsWith('/') &&
-      !href.startsWith(this.baseUrl)
-    ) {
-      href = this.baseUrl + href;
-    }
-    this.addItem(text, href, null, position, length, outputArray);
-  }
-
-  /**
-   * Create a CommentLinkItem specified by an HTMl string and append it to the
-   * given output array.
-   *
-   * @param html The html to parse and use as the result.
-   * @param position The position inside the source text where the item
-   *     starts.
-   * @param length The number of characters in the source text
-   *     represented by the item.
-   * @param outputArray The array to which the
-   *     new item is to be appended.
-   */
-  addHTML(
-    html: string,
-    position: number,
-    length: number,
-    outputArray: CommentLinkItem[]
-  ) {
-    if (this.hasOverlap(position, length, outputArray)) {
-      return;
-    }
-    if (
-      !!this.baseUrl &&
-      html.match(/<a href="\//g) &&
-      !new RegExp(`<a href="${this.baseUrl}`, 'g').test(html)
-    ) {
-      html = html.replace(/<a href="\//g, `<a href="${this.baseUrl}/`);
-    }
-    this.addItem(null, null, html, position, length, outputArray);
-  }
-
-  /**
-   * Does the given range overlap with anything already in the item list.
-   */
-  hasOverlap(position: number, length: number, outputArray: CommentLinkItem[]) {
-    const endPosition = position + length;
-    for (let i = 0; i < outputArray.length; i++) {
-      const arrayItemStart = outputArray[i].position;
-      const arrayItemEnd = outputArray[i].position + outputArray[i].length;
-      if (
-        (position >= arrayItemStart && position < arrayItemEnd) ||
-        (endPosition > arrayItemStart && endPosition <= arrayItemEnd) ||
-        (position === arrayItemStart && position === arrayItemEnd)
-      ) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Parse the given source text and emit callbacks for the items that are
-   * parsed.
-   */
-  parse(text?: string | null) {
-    if (text) {
-      window.linkify(text, {
-        callback: (text: string, href?: string) => this.parseChunk(text, href),
-      });
-    }
-  }
-
-  /**
-   * Callback that is pased into the linkify function. ba-linkify will call this
-   * method in either of two ways:
-   * - With both a `text` and `href` parameter provided: this indicates that
-   *   ba-linkify has found a plain URL and wants it linkified.
-   * - With only a `text` parameter provided: this represents the non-link
-   *   content that lies between the links the library has found.
-   *
-   */
-  parseChunk(text: string, href?: string) {
-    // TODO(wyatta) switch linkify sequence, see issue 5526.
-    if (this.removeZeroWidthSpace) {
-      // Remove the zero-width space added in gr-change-view.
-      text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
-    }
-
-    // If the href is provided then ba-linkify has recognized it as a URL. If
-    // the source text does not include a protocol, the protocol will be added
-    // by ba-linkify. Create the link if the href is provided and its protocol
-    // matches the expected pattern.
-    if (href) {
-      const result = URL_PROTOCOL_PATTERN.exec(href);
-      if (result) {
-        const prefixText = result[1];
-        if (prefixText.length > 0) {
-          // Fix for simple cases from
-          // https://bugs.chromium.org/p/gerrit/issues/detail?id=11697
-          // When leading whitespace is missed before link,
-          // linkify add this text before link as a schema name to href.
-          // We suppose, that prefixText just a single word
-          // before link and add this word as is, without processing
-          // any patterns in it.
-          this.parseLinks(prefixText, {});
-          text = text.substring(prefixText.length);
-          href = href.substring(prefixText.length);
-        }
-        this.addText(text, href);
-        return;
-      }
-    }
-    // For the sections of text that lie between the links found by
-    // ba-linkify, we search for the project-config-specified link patterns.
-    this.parseLinks(text, this.linkConfig);
-  }
-
-  /**
-   * Walk over the given source text to find matches for comemntlink patterns
-   * and emit parse result callbacks.
-   *
-   * @param text The raw source text.
-   * @param config A comment links specification object.
-   */
-  parseLinks(text: string, config: LinkTextParserConfig) {
-    // The outputArray is used to store all of the matches found for all
-    // patterns.
-    const outputArray: CommentLinkItem[] = [];
-    for (const [configName, linkInfo] of Object.entries(config)) {
-      // TODO(TS): it seems, the following line can be rewritten as:
-      // if(enabled === false || enabled === 0 || enabled === '')
-      // Should be double-checked before update
-      // eslint-disable-next-line eqeqeq
-      if (linkInfo.enabled != null && linkInfo.enabled == false) {
-        continue;
-      }
-      // PolyGerrit doesn't use hash-based navigation like the GWT UI.
-      // Account for this.
-      const html = linkInfo.html;
-      const link = linkInfo.link;
-      if (html) {
-        linkInfo.html = html.replace(/<a href="#\//g, '<a href="/');
-      } else if (link) {
-        if (link[0] === '#') {
-          linkInfo.link = link.substr(1);
-        }
-      }
-
-      const pattern = new RegExp(linkInfo.match, 'g');
-
-      let match;
-      let textToCheck = text;
-      let susbtrIndex = 0;
-
-      while ((match = pattern.exec(textToCheck))) {
-        textToCheck = textToCheck.substr(match.index + match[0].length);
-        let result = match[0].replace(
-          pattern,
-          // Either html or link has a value. Otherwise an exception is thrown
-          // in the code below.
-          (linkInfo.html || linkInfo.link)!
-        );
-
-        if (linkInfo.html) {
-          let i;
-          // Skip portion of replacement string that is equal to original to
-          // allow overlapping patterns.
-          for (i = 0; i < result.length; i++) {
-            if (result[i] !== match[0][i]) {
-              break;
-            }
-          }
-          result = result.slice(i);
-
-          this.addHTML(
-            result,
-            susbtrIndex + match.index + i,
-            match[0].length - i,
-            outputArray
-          );
-        } else if (linkInfo.link) {
-          this.addLink(
-            match[0],
-            result,
-            susbtrIndex + match.index,
-            match[0].length,
-            outputArray
-          );
-        } else {
-          throw Error(
-            'linkconfig entry ' +
-              configName +
-              ' doesn’t contain a link or html attribute.'
-          );
-        }
-
-        // Update the substring location so we know where we are in relation to
-        // the initial full text string.
-        susbtrIndex = susbtrIndex + match.index + match[0].length;
-      }
-    }
-    this.processLinks(text, outputArray);
-  }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 7b5ce15..9a114e6 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -260,8 +260,6 @@
         .suggestions=${this.suggestions}
         .horizontalOffset=${20}
         .verticalOffset=${20}
-        vertical-align="top"
-        horizontal-align="left"
         @dropdown-closed=${this.resetDropdown}
         @item-selected=${this.handleDropdownItemSelect}
       >
@@ -275,8 +273,6 @@
     return html` <gr-autocomplete-dropdown
       id="mentionsSuggestions"
       .suggestions=${this.suggestions}
-      vertical-align="top"
-      horizontal-align="left"
       @dropdown-closed=${this.resetDropdown}
       @item-selected=${this.handleDropdownItemSelect}
       .horizontalOffset=${20}
@@ -525,7 +521,7 @@
       // Otherwise open the dropdown and set the position to be just below the
       // cursor.
       // Do not open dropdown if textarea is not focused
-      activeDropdown!.positionTarget = this.updateCaratPosition();
+      activeDropdown.setPositionTarget(this.updateCaratPosition());
       activate();
     }
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 6837a71..78c8aa3 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -33,8 +33,6 @@
         <span id="caratSpan"> </span>
         <gr-autocomplete-dropdown
           id="emojiSuggestions"
-          horizontal-align="left"
-          vertical-align="top"
           is-hidden=""
           style="position: fixed; top: 150px; left: 392.5px; box-sizing: border-box; max-height: 300px; max-width: 785px;"
         >
@@ -64,20 +62,16 @@
           <div id="hiddenText"></div>
           <span id="caratSpan"> </span>
           <gr-autocomplete-dropdown
-            horizontal-align="left"
             id="emojiSuggestions"
             is-hidden=""
             style="position: fixed; top: 478px; left: 321px; box-sizing: border-box; max-height: 956px; max-width: 642px;"
-            vertical-align="top"
           >
           </gr-autocomplete-dropdown>
           <gr-autocomplete-dropdown
-            horizontal-align="left"
             id="mentionsSuggestions"
             is-hidden=""
             role="listbox"
             style="position: fixed; top: 478px; left: 321px; box-sizing: border-box; max-height: 956px; max-width: 642px;"
-            vertical-align="top"
           >
           </gr-autocomplete-dropdown>
           <iron-autogrow-textarea
@@ -581,6 +575,7 @@
       element.textarea!.selectionStart = 1;
       element.textarea!.selectionEnd = 2;
       element.text = ':1';
+      await element.emojiSuggestions!.updateComplete;
       await element.updateComplete;
     }
 
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
index fe4632b..8a92bcc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -13,6 +13,7 @@
 import '@polymer/paper-listbox/paper-listbox';
 import './gr-overview-image';
 import './gr-zoomed-image';
+import '../../../elements/shared/gr-icons/gr-icons';
 
 import {GrLibLoader} from '../../../elements/shared/gr-lib-loader/gr-lib-loader';
 import {RESEMBLEJS_LIBRARY_CONFIG} from '../../../elements/shared/gr-lib-loader/resemblejs_config';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index f0697df..53c2780 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -5,7 +5,6 @@
  */
 import '../../../styles/shared-styles';
 import '../../../elements/shared/gr-button/gr-button';
-import '../../../elements/shared/gr-icons/gr-icons';
 import '../../../elements/shared/gr-icon/gr-icon';
 import '../gr-diff-builder/gr-diff-builder-element';
 import '../gr-diff-highlight/gr-diff-highlight';
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 09c9273..7ccdf91 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -8,12 +8,16 @@
   Category,
   CheckResult as CheckResultApi,
   CheckRun as CheckRunApi,
+  Fix,
   Link,
   LinkIcon,
+  Replacement,
   RunStatus,
 } from '../../api/checks';
 import {PatchSetNumber} from '../../api/rest-api';
+import {FixSuggestionInfo, FixReplacementInfo} from '../../types/common';
 import {OpenFixPreviewEventDetail} from '../../types/events';
+import {notUndefined} from '../../types/types';
 import {PROVIDED_FIX_ID} from '../../utils/comment-util';
 import {assert, assertNever} from '../../utils/common-util';
 import {fire} from '../../utils/event-util';
@@ -86,17 +90,15 @@
   target: EventTarget,
   result?: RunResult
 ): Action | undefined {
-  const fixes = result?.fixes;
-  if (!fixes || fixes?.length === 0 || !result?.patchset) return;
+  if (!result?.patchset) return;
+  if (!result?.fixes) return;
+  const fixSuggestions = result.fixes
+    .map(f => rectifyFix(f, result?.checkName))
+    .filter(notUndefined);
+  if (fixSuggestions.length === 0) return;
   const eventDetail: OpenFixPreviewEventDetail = {
-    patchNum: result?.patchset as PatchSetNumber,
-    fixSuggestions: fixes.map(fix => {
-      return {
-        description: `Fix provided by ${result?.checkName}`,
-        fix_id: PROVIDED_FIX_ID,
-        ...fix,
-      };
-    }),
+    patchNum: result.patchset as PatchSetNumber,
+    fixSuggestions,
   };
   return {
     name: 'Show Fix',
@@ -107,6 +109,36 @@
   };
 }
 
+export function rectifyFix(
+  fix: Fix | undefined,
+  checkName: string
+): FixSuggestionInfo | undefined {
+  if (!fix?.replacements) return undefined;
+  const replacements = fix.replacements
+    .map(rectifyReplacement)
+    .filter(notUndefined);
+  if (replacements.length === 0) return undefined;
+
+  return {
+    description: fix.description ?? `Fix provided by ${checkName}`,
+    fix_id: PROVIDED_FIX_ID,
+    replacements,
+  };
+}
+
+export function rectifyReplacement(
+  r: Replacement | undefined
+): FixReplacementInfo | undefined {
+  if (!r?.path) return undefined;
+  if (!r?.range) return undefined;
+  if (r?.replacement === undefined) return undefined;
+  if (!Number.isInteger(r.range.start_line)) return undefined;
+  if (!Number.isInteger(r.range.end_line)) return undefined;
+  if (!Number.isInteger(r.range.start_character)) return undefined;
+  if (!Number.isInteger(r.range.end_character)) return undefined;
+  return r;
+}
+
 export function worstCategory(run: CheckRun) {
   if (hasResultsOf(run, Category.ERROR)) return Category.ERROR;
   if (hasResultsOf(run, Category.WARNING)) return Category.WARNING;
diff --git a/polygerrit-ui/app/models/checks/checks-util_test.ts b/polygerrit-ui/app/models/checks/checks-util_test.ts
index 5a7bd75..c237c59 100644
--- a/polygerrit-ui/app/models/checks/checks-util_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-util_test.ts
@@ -10,9 +10,13 @@
   ALL_ATTEMPTS,
   AttemptChoice,
   LATEST_ATTEMPT,
+  rectifyFix,
   sortAttemptChoices,
   stringToAttemptChoice,
 } from './checks-util';
+import {Fix, Replacement} from '../../api/checks';
+import {CommentRange} from '../../api/core';
+import {PROVIDED_FIX_ID} from '../../utils/comment-util';
 
 suite('checks-util tests', () => {
   setup(() => {});
@@ -33,6 +37,55 @@
     assert.equal(stringToAttemptChoice('1x'), undefined);
   });
 
+  test('rectifyFix', () => {
+    assert.isUndefined(rectifyFix(undefined, 'name'));
+    assert.isUndefined(rectifyFix({} as Fix, 'name'));
+    assert.isUndefined(
+      rectifyFix({description: 'asdf', replacements: []}, 'name')
+    );
+    assert.isUndefined(
+      rectifyFix(
+        {description: 'asdf', replacements: [{} as Replacement]},
+        'test-check-name'
+      )
+    );
+    assert.isUndefined(
+      rectifyFix(
+        {
+          description: 'asdf',
+          replacements: [
+            {
+              path: 'test-path',
+              range: {} as CommentRange,
+              replacement: 'test-replacement-string',
+            },
+          ],
+        },
+        'test-check-name'
+      )
+    );
+    const rectified = rectifyFix(
+      {
+        replacements: [
+          {
+            path: 'test-path',
+            range: {
+              start_line: 1,
+              end_line: 1,
+              start_character: 0,
+              end_character: 1,
+            } as CommentRange,
+            replacement: 'test-replacement-string',
+          },
+        ],
+      },
+      'test-check-name'
+    );
+    assert.isDefined(rectified);
+    assert.equal(rectified?.description, 'Fix provided by test-check-name');
+    assert.equal(rectified?.fix_id, PROVIDED_FIX_ID);
+  });
+
   test('sortAttemptChoices', () => {
     const unsorted: (AttemptChoice | undefined)[] = [
       3,
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 826ec66..100c46b 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -11,8 +11,10 @@
   ChangeInfo,
   PatchSetNumber,
 } from '../../api/rest-api';
+import {Tab} from '../../constants/constants';
 import {GerritView} from '../../services/router/router-model';
 import {UrlEncodedCommentId} from '../../types/common';
+import {toggleSet} from '../../utils/common-util';
 import {select} from '../../utils/observable-util';
 import {
   encodeURL,
@@ -33,7 +35,8 @@
   patchNum?: RevisionPatchSetNum;
   basePatchNum?: BasePatchSetNum;
   commentId?: UrlEncodedCommentId;
-  tab?: string;
+  /** This can be a string only for plugin provided tabs. */
+  tab?: Tab | string;
 
   /** Checks related view state */
 
@@ -43,6 +46,10 @@
   filter?: string;
   /** selected attempt for check runs (undefined=latest) */
   attempt?: AttemptChoice;
+  /** selected check runs identified by `checkName` */
+  checksRunsSelected?: Set<string>;
+  /** regular expression for filtering check results */
+  checksResultsFilter?: string;
 
   /** State properties that trigger one-time actions */
 
@@ -107,6 +114,15 @@
   if (state.filter) {
     queries.push(`filter=${state.filter}`);
   }
+  if (state.checksResultsFilter) {
+    queries.push(`checksResultsFilter=${state.checksResultsFilter}`);
+  }
+  if (state.checksRunsSelected && state.checksRunsSelected.size > 0) {
+    queries.push(`checksRunsSelected=${[...state.checksRunsSelected].sort()}`);
+  }
+  if (state.tab && state.tab !== Tab.FILES) {
+    queries.push(`tab=${state.tab}`);
+  }
   if (state.forceReload) {
     queries.push('forceReload=true');
   }
@@ -130,9 +146,9 @@
   }
   if (state.project) {
     const encodedProject = encodeURL(state.project, true);
-    return getBaseUrl() + `/c/${encodedProject}/+/${state.changeNum}${suffix}`;
+    return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
   } else {
-    return getBaseUrl() + `/c/${state.changeNum}${suffix}`;
+    return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
   }
 }
 
@@ -151,6 +167,16 @@
 
   public readonly filter$ = select(this.state$, state => state?.filter);
 
+  public readonly checksResultsFilter$ = select(
+    this.state$,
+    state => state?.checksResultsFilter ?? ''
+  );
+
+  public readonly checksRunsSelected$ = select(
+    this.state$,
+    state => state?.checksRunsSelected ?? new Set<string>()
+  );
+
   constructor() {
     super(undefined);
     this.state$.subscribe(s => {
@@ -163,4 +189,11 @@
       }
     });
   }
+
+  toggleSelectedCheckRun(checkName: string) {
+    const current = this.getState()?.checksRunsSelected ?? new Set();
+    const next = new Set(current);
+    toggleSet(next, checkName);
+    this.updateState({checksRunsSelected: next});
+  }
 }
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index 2f05e9f..24ced82 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -14,13 +14,15 @@
 import '../../test/common-test-setup';
 import {createChangeUrl, ChangeViewState} from './change';
 
+const STATE: ChangeViewState = {
+  view: GerritView.CHANGE,
+  changeNum: 1234 as NumericChangeId,
+  project: 'test' as RepoName,
+};
+
 suite('change view state tests', () => {
   test('createChangeUrl()', () => {
-    const state: ChangeViewState = {
-      view: GerritView.CHANGE,
-      changeNum: 1234 as NumericChangeId,
-      project: 'test' as RepoName,
-    };
+    const state: ChangeViewState = {...STATE};
 
     assert.equal(createChangeUrl(state), '/c/test/+/1234');
 
@@ -34,6 +36,37 @@
     assert.equal(createChangeUrl(state), '/c/test/+/1234/5..10#123');
   });
 
+  test('createChangeUrl() baseUrl', () => {
+    window.CANONICAL_PATH = '/base';
+    const state: ChangeViewState = {...STATE};
+    assert.equal(createChangeUrl(state).substring(0, 5), '/base');
+    window.CANONICAL_PATH = undefined;
+  });
+
+  test('createChangeUrl() checksRunsSelected', () => {
+    const state: ChangeViewState = {
+      ...STATE,
+      checksRunsSelected: new Set(['asdf']),
+    };
+
+    assert.equal(
+      createChangeUrl(state),
+      '/c/test/+/1234?checksRunsSelected=asdf'
+    );
+  });
+
+  test('createChangeUrl() checksResultsFilter', () => {
+    const state: ChangeViewState = {
+      ...STATE,
+      checksResultsFilter: 'asdf.*qwer',
+    };
+
+    assert.equal(
+      createChangeUrl(state),
+      '/c/test/+/1234?checksResultsFilter=asdf.*qwer'
+    );
+  });
+
   test('createChangeUrl() with repo name encoding', () => {
     const state: ChangeViewState = {
       view: GerritView.CHANGE,
diff --git a/polygerrit-ui/app/models/views/dashboard.ts b/polygerrit-ui/app/models/views/dashboard.ts
index 7141637..d9ff2d2 100644
--- a/polygerrit-ui/app/models/views/dashboard.ts
+++ b/polygerrit-ui/app/models/views/dashboard.ts
@@ -7,7 +7,7 @@
 import {GerritView} from '../../services/router/router-model';
 import {DashboardId} from '../../types/common';
 import {DashboardSection} from '../../utils/dashboard-util';
-import {encodeURL} from '../../utils/url-util';
+import {encodeURL, getBaseUrl} from '../../utils/url-util';
 import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
@@ -46,14 +46,14 @@
       queryParams.push('title=' + encodeURIComponent(state.title));
     }
     const user = state.user ? state.user : '';
-    return `/dashboard/${user}?${queryParams.join('&')}`;
+    return `${getBaseUrl()}/dashboard/${user}?${queryParams.join('&')}`;
   } else if (repoName) {
     // Project dashboard.
     const encodedRepo = encodeURL(repoName, true);
-    return `/p/${encodedRepo}/+/dashboard/${state.dashboard}`;
+    return `${getBaseUrl()}/p/${encodedRepo}/+/dashboard/${state.dashboard}`;
   } else {
     // User dashboard.
-    return `/dashboard/${state.user || 'self'}`;
+    return `${getBaseUrl()}/dashboard/${state.user || 'self'}`;
   }
 }
 
diff --git a/polygerrit-ui/app/models/views/dashboard_test.ts b/polygerrit-ui/app/models/views/dashboard_test.ts
index 9deed72..86bb5c0 100644
--- a/polygerrit-ui/app/models/views/dashboard_test.ts
+++ b/polygerrit-ui/app/models/views/dashboard_test.ts
@@ -15,6 +15,12 @@
       assert.equal(createDashboardUrl({}), '/dashboard/self');
     });
 
+    test('baseUrl', () => {
+      window.CANONICAL_PATH = '/base';
+      assert.equal(createDashboardUrl({}).substring(0, 5), '/base');
+      window.CANONICAL_PATH = undefined;
+    });
+
     test('user dashboard', () => {
       assert.equal(createDashboardUrl({user: 'user'}), '/dashboard/user');
     });
diff --git a/polygerrit-ui/app/models/views/diff.ts b/polygerrit-ui/app/models/views/diff.ts
index 63df521..3cc107a 100644
--- a/polygerrit-ui/app/models/views/diff.ts
+++ b/polygerrit-ui/app/models/views/diff.ts
@@ -12,7 +12,11 @@
 } from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
 import {UrlEncodedCommentId} from '../../types/common';
-import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
+import {
+  encodeURL,
+  getBaseUrl,
+  getPatchRangeExpression,
+} from '../../utils/url-util';
 import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
@@ -85,9 +89,9 @@
 
   if (state.project) {
     const encodedProject = encodeURL(state.project, true);
-    return `/c/${encodedProject}/+/${state.changeNum}${suffix}`;
+    return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
   } else {
-    return `/c/${state.changeNum}${suffix}`;
+    return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
   }
 }
 
diff --git a/polygerrit-ui/app/models/views/diff_test.ts b/polygerrit-ui/app/models/views/diff_test.ts
index ec20037..b0f91bb 100644
--- a/polygerrit-ui/app/models/views/diff_test.ts
+++ b/polygerrit-ui/app/models/views/diff_test.ts
@@ -25,6 +25,10 @@
     };
     assert.equal(createDiffUrl(params), '/c/42/12/x%252By/path.cpp');
 
+    window.CANONICAL_PATH = '/base';
+    assert.equal(createDiffUrl(params).substring(0, 5), '/base');
+    window.CANONICAL_PATH = undefined;
+
     params.project = 'test' as RepoName;
     assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
 
diff --git a/polygerrit-ui/app/models/views/edit.ts b/polygerrit-ui/app/models/views/edit.ts
index d8f4770..c63c8ce 100644
--- a/polygerrit-ui/app/models/views/edit.ts
+++ b/polygerrit-ui/app/models/views/edit.ts
@@ -10,7 +10,11 @@
   RevisionPatchSetNum,
 } from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
-import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
+import {
+  encodeURL,
+  getBaseUrl,
+  getPatchRangeExpression,
+} from '../../utils/url-util';
 import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
@@ -41,9 +45,9 @@
 
   if (state.project) {
     const encodedProject = encodeURL(state.project, true);
-    return `/c/${encodedProject}/+/${state.changeNum}${suffix}`;
+    return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
   } else {
-    return `/c/${state.changeNum}${suffix}`;
+    return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
   }
 }
 
diff --git a/polygerrit-ui/app/models/views/edit_test.ts b/polygerrit-ui/app/models/views/edit_test.ts
index e1a05c7..2912063 100644
--- a/polygerrit-ui/app/models/views/edit_test.ts
+++ b/polygerrit-ui/app/models/views/edit_test.ts
@@ -27,5 +27,9 @@
       createEditUrl(params),
       '/c/test-project/+/42/12/x%252By/path.cpp,edit#31'
     );
+
+    window.CANONICAL_PATH = '/base';
+    assert.equal(createEditUrl(params).substring(0, 5), '/base');
+    window.CANONICAL_PATH = undefined;
   });
 });
diff --git a/polygerrit-ui/app/models/views/search.ts b/polygerrit-ui/app/models/views/search.ts
index 58cb8f7..13de8f3 100644
--- a/polygerrit-ui/app/models/views/search.ts
+++ b/polygerrit-ui/app/models/views/search.ts
@@ -6,7 +6,7 @@
 import {RepoName, BranchName, TopicName} from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
 import {addQuotesWhen} from '../../utils/string-util';
-import {encodeURL} from '../../utils/url-util';
+import {encodeURL, getBaseUrl} from '../../utils/url-util';
 import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
@@ -35,7 +35,7 @@
   }
 
   if (params.query) {
-    return '/q/' + encodeURL(params.query, true) + offsetExpr;
+    return `${getBaseUrl()}/q/${encodeURL(params.query, true)}${offsetExpr}`;
   }
 
   const operators: string[] = [];
@@ -80,7 +80,7 @@
     }
   }
 
-  return '/q/' + operators.join('+') + offsetExpr;
+  return `${getBaseUrl()}/q/${operators.join('+')}${offsetExpr}`;
 }
 
 export const searchViewModelToken =
diff --git a/polygerrit-ui/app/models/views/search_test.ts b/polygerrit-ui/app/models/views/search_test.ts
index 138ce1e..d48667b 100644
--- a/polygerrit-ui/app/models/views/search_test.ts
+++ b/polygerrit-ui/app/models/views/search_test.ts
@@ -23,6 +23,10 @@
         'topic:g%2525h+status:op%2525en'
     );
 
+    window.CANONICAL_PATH = '/base';
+    assert.equal(createSearchUrl(options).substring(0, 5), '/base');
+    window.CANONICAL_PATH = undefined;
+
     options.offset = 100;
     assert.equal(
       createSearchUrl(options),
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 597bb6f..496513d 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -209,9 +209,7 @@
 // Type for the custom event to switch tab.
 export interface SwitchTabEventDetail {
   // name of the tab to set as active, from custom event
-  tab?: string;
-  // index of tab to set as active, from paper-tabs event
-  value?: number;
+  tab: string;
   // scroll into the tab afterwards, from custom event
   scrollIntoView?: boolean;
   // define state of tab after opening
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 05c054b..183d167 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -117,7 +117,7 @@
 /**
  * Add value, if the set does not contain it. Otherwise remove it.
  */
-export function toggleSetMembership<T>(set: Set<T>, value: T): void {
+export function toggleSet<T>(set: Set<T>, value: T): void {
   if (set.has(value)) {
     set.delete(value);
   } else {
@@ -125,6 +125,14 @@
   }
 }
 
+export function toggle<T>(array: T[], item: T): T[] {
+  if (array.includes(item)) {
+    return array.filter(r => r !== item);
+  } else {
+    return array.concat([item]);
+  }
+}
+
 export function unique<T>(item: T, index: number, array: T[]) {
   return array.indexOf(item) === index;
 }
diff --git a/polygerrit-ui/app/utils/common-util_test.ts b/polygerrit-ui/app/utils/common-util_test.ts
index 0d85f34..76c8a6c 100644
--- a/polygerrit-ui/app/utils/common-util_test.ts
+++ b/polygerrit-ui/app/utils/common-util_test.ts
@@ -11,6 +11,7 @@
   containsAll,
   intersection,
   difference,
+  toggle,
 } from './common-util';
 
 suite('common-util tests', () => {
@@ -97,4 +98,11 @@
     assert.deepEqual(difference([1, 2, 3], [1, 2, 3]), []);
     assert.deepEqual(difference([1, 2, 3], [4, 5, 6]), [1, 2, 3]);
   });
+
+  test('toggle', () => {
+    assert.deepEqual(toggle([], 1), [1]);
+    assert.deepEqual(toggle([1], 1), []);
+    assert.deepEqual(toggle([1, 2, 3], 1), [2, 3]);
+    assert.deepEqual(toggle([2, 3], 1), [2, 3, 1]);
+  });
 });
diff --git a/polygerrit-ui/app/utils/link-util.ts b/polygerrit-ui/app/utils/link-util.ts
index 9079f4c..b5b9025 100644
--- a/polygerrit-ui/app/utils/link-util.ts
+++ b/polygerrit-ui/app/utils/link-util.ts
@@ -4,76 +4,164 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import 'ba-linkify/ba-linkify';
-import {CommentLinks} from '../types/common';
+import {CommentLinkInfo, CommentLinks} from '../types/common';
 import {getBaseUrl} from './url-util';
 
-export function linkifyNormalUrls(base: string): string {
-  // Some tools are known to look for reviewers/CCs by finding lines such as
-  // "R=foo@gmail.com, bar@gmail.com". However, "=" is technically a valid email
-  // character, so ba-linkify interprets the entire string "R=foo@gmail.com" as
-  // an email address. To fix this, we insert a zero width space character
-  // \u200B before linking that prevents ba-linkify from associating the prefix
-  // with the email. After linking we remove the zero width space.
-  const baseWithZeroWidthSpace = base.replace(/^(R=|CC=)/g, '$&\u200B');
+/**
+ * Finds links within the base string and convert them to HTML. Config-based
+ * rewrites are only applied on text that is not linked by the default linking
+ * library.
+ */
+export function linkifyUrlsAndApplyRewrite(
+  base: string,
+  repoCommentLinks: CommentLinks
+): string {
   const parts: string[] = [];
-  window.linkify(baseWithZeroWidthSpace, {
+  window.linkify(insertZeroWidthSpace(base), {
     callback: (text, href) => {
-      const result = href ? createLinkTemplate(href, text) : text;
-      const resultWithoutZeroWidthSpace = result.replace(/\u200B/g, '');
-      parts.push(resultWithoutZeroWidthSpace);
+      if (href) {
+        parts.push(removeZeroWidthSpace(createLinkTemplate(href, text)));
+      } else {
+        const rewriteResults = getRewriteResultsFromConfig(
+          text,
+          repoCommentLinks
+        );
+        parts.push(removeZeroWidthSpace(applyRewrites(text, rewriteResults)));
+      }
     },
   });
   return parts.join('');
 }
 
-export function applyLinkRewritesFromConfig(
+/**
+ * Generates a list of rewrites that would be applied to a base string. They are
+ * not applied immediately to the base text because one rewrite may interfere or
+ * overlap with a later rewrite. Only after all rewrites are known they are
+ * carefully merged with `applyRewrites`.
+ */
+function getRewriteResultsFromConfig(
   base: string,
   repoCommentLinks: CommentLinks
-) {
-  const linkRewritesFromConfig = Object.values(repoCommentLinks).filter(
-    commentLinkInfo => commentLinkInfo.enabled !== false && commentLinkInfo.link
+): RewriteResult[] {
+  const enabledRewrites = Object.values(repoCommentLinks).filter(
+    commentLinkInfo =>
+      commentLinkInfo.enabled !== false &&
+      (commentLinkInfo.link !== undefined || commentLinkInfo.html !== undefined)
   );
-  const rewrites = linkRewritesFromConfig.map(rewrite => {
-    const replacementHref = rewrite.link!.startsWith('/')
-      ? `${getBaseUrl()}${rewrite.link!}`
-      : rewrite.link!;
-    return {
-      match: new RegExp(rewrite.match, 'g'),
-      replace: createLinkTemplate(
+  return enabledRewrites.flatMap(rewrite => {
+    const regexp = new RegExp(rewrite.match, 'g');
+    const partialResults: RewriteResult[] = [];
+    let match: RegExpExecArray | null;
+
+    while ((match = regexp.exec(base)) !== null) {
+      const fullReplacementText = getReplacementText(match[0], rewrite);
+      // The replacement may not be changing the entire matched substring so we
+      // "trim" the replacement position and text to the part that is actually
+      // different. This makes sure that unchanged portions are still eligible
+      // for other rewrites without being rejected as overlaps during
+      // `applyRewrites`. The new `replacementText` is not eligible for other
+      // rewrites since it would introduce unexpected interactions between
+      // rewrites depending on their order of definition/execution.
+      const sharedPrefixLength = getSharedPrefixLength(
+        match[0],
+        fullReplacementText
+      );
+      const sharedSuffixLength = getSharedSuffixLength(
+        match[0],
+        fullReplacementText
+      );
+      const prefixIndex = sharedPrefixLength;
+      const matchSuffixIndex = match[0].length - sharedSuffixLength;
+      const fullReplacementSuffixIndex =
+        fullReplacementText.length - sharedSuffixLength;
+      partialResults.push({
+        replacedTextStartPosition: match.index + prefixIndex,
+        replacedTextEndPosition: match.index + matchSuffixIndex,
+        replacementText: fullReplacementText.substring(
+          prefixIndex,
+          fullReplacementSuffixIndex
+        ),
+      });
+    }
+    return partialResults;
+  });
+}
+
+/**
+ * Applies all the rewrites to the given base string. To resolve cases where
+ * multiple rewrites target overlapping pieces of the base string, the rewrite
+ * that ends latest is kept and the rest are not applied and discarded.
+ */
+function applyRewrites(base: string, rewriteResults: RewriteResult[]): string {
+  const rewritesByEndPosition = [...rewriteResults].sort((a, b) => {
+    if (b.replacedTextEndPosition !== a.replacedTextEndPosition) {
+      return b.replacedTextEndPosition - a.replacedTextEndPosition;
+    }
+    return a.replacedTextStartPosition - b.replacedTextStartPosition;
+  });
+  const filteredSortedRewrites: RewriteResult[] = [];
+  let latestReplace = base.length;
+  for (const rewrite of rewritesByEndPosition) {
+    // Only accept rewrites that do not overlap with any previously accepted
+    // rewrites.
+    if (rewrite.replacedTextEndPosition <= latestReplace) {
+      filteredSortedRewrites.push(rewrite);
+      latestReplace = rewrite.replacedTextStartPosition;
+    }
+  }
+  return filteredSortedRewrites.reduce(
+    (text, rewrite) =>
+      text
+        .substring(0, rewrite.replacedTextStartPosition)
+        .concat(rewrite.replacementText)
+        .concat(text.substring(rewrite.replacedTextEndPosition)),
+    base
+  );
+}
+
+/**
+ * For a given regexp match, apply the rewrite based on the rewrite's type and
+ * return the resulting string.
+ */
+function getReplacementText(
+  matchedText: string,
+  rewrite: CommentLinkInfo
+): string {
+  if (rewrite.link !== undefined) {
+    const replacementHref = rewrite.link.startsWith('/')
+      ? `${getBaseUrl()}${rewrite.link}`
+      : rewrite.link;
+    const regexp = new RegExp(rewrite.match, 'g');
+    return matchedText.replace(
+      regexp,
+      createLinkTemplate(
         replacementHref,
         rewrite.text ?? '$&',
         rewrite.prefix,
         rewrite.suffix
-      ),
-    };
-  });
-  return applyRewrites(base, rewrites);
+      )
+    );
+  } else if (rewrite.html !== undefined) {
+    return matchedText.replace(new RegExp(rewrite.match, 'g'), rewrite.html);
+  } else {
+    throw new Error('commentLinkInfo is not a link or html rewrite');
+  }
 }
 
-export function applyHtmlRewritesFromConfig(
-  base: string,
-  repoCommentLinks: CommentLinks
-) {
-  const htmlRewritesFromConfig = Object.values(repoCommentLinks).filter(
-    commentLinkInfo => commentLinkInfo.enabled !== false && commentLinkInfo.html
-  );
-  const rewrites = htmlRewritesFromConfig.map(rewrite => {
-    return {
-      match: new RegExp(rewrite.match, 'g'),
-      replace: rewrite.html!,
-    };
-  });
-  return applyRewrites(base, rewrites);
+/**
+ * Some tools are known to look for reviewers/CCs by finding lines such as
+ * "R=foo@gmail.com, bar@gmail.com". However, "=" is technically a valid email
+ * character, so ba-linkify interprets the entire string "R=foo@gmail.com" as an
+ * email address. To fix this, we insert a zero width space character \u200B
+ * before linking that prevents ba-linkify from associating the prefix with the
+ * email. After linking we remove the zero width space.
+ */
+function insertZeroWidthSpace(base: string) {
+  return base.replace(/^(R=|CC=)/g, '$&\u200B');
 }
 
-function applyRewrites(
-  base: string,
-  rewrites: {match: RegExp | string; replace: string}[]
-) {
-  return rewrites.reduce(
-    (text, rewrite) => text.replace(rewrite.match, rewrite.replace),
-    base
-  );
+function removeZeroWidthSpace(base: string) {
+  return base.replace(/\u200B/g, '');
 }
 
 function createLinkTemplate(
@@ -88,3 +176,41 @@
     suffix ?? ''
   }`;
 }
+
+/**
+ * Returns the number of characters that are identical at the start of both
+ * strings.
+ *
+ * For example, `getSharedPrefixLength('12345678', '1234zz78')` would return 4
+ */
+function getSharedPrefixLength(a: string, b: string) {
+  let i = 0;
+  for (; i < a.length && i < b.length; ++i) {
+    if (a[i] !== b[i]) {
+      return i;
+    }
+  }
+  return i;
+}
+
+/**
+ * Returns the number of characters that are identical at the end of both
+ * strings.
+ *
+ * For example, `getSharedSuffixLength('12345678', '1234zz78')` would return 2
+ */
+function getSharedSuffixLength(a: string, b: string) {
+  let i = a.length;
+  for (let j = b.length; i !== 0 && j !== 0; --i, --j) {
+    if (a[i] !== b[j]) {
+      return a.length - 1 - i;
+    }
+  }
+  return a.length - i;
+}
+
+interface RewriteResult {
+  replacedTextStartPosition: number;
+  replacedTextEndPosition: number;
+  replacementText: string;
+}
diff --git a/polygerrit-ui/app/utils/link-util_test.ts b/polygerrit-ui/app/utils/link-util_test.ts
index a1ec2fa..61d6bff 100644
--- a/polygerrit-ui/app/utils/link-util_test.ts
+++ b/polygerrit-ui/app/utils/link-util_test.ts
@@ -3,11 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {
-  applyHtmlRewritesFromConfig,
-  applyLinkRewritesFromConfig,
-  linkifyNormalUrls,
-} from './link-util';
+import {linkifyUrlsAndApplyRewrite} from './link-util';
 import {assert} from '@open-wc/testing';
 
 suite('link-util tests', () => {
@@ -15,55 +11,220 @@
     return `<a href="${href}" rel="noopener" target="_blank">${text}</a>`;
   }
 
-  test('applyHtmlRewritesFromConfig', () => {
-    assert.equal(
-      applyHtmlRewritesFromConfig('#12345 foo', {
-        'number-emphasizer': {
-          match: '#(\\d+)',
-          html: '<h1>Change $1 is the best change</h1>',
-        },
-        'foo-capitalizer': {
-          match: 'foo',
-          html: '<div>FOO</div>',
-        },
-      }),
-      '<h1>Change 12345 is the best change</h1> <div>FOO</div>'
-    );
+  suite('link rewrites', () => {
+    test('without text', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo', {
+          fooLinkWithoutText: {
+            match: 'foo',
+            link: 'foo.gov',
+          },
+        }),
+        link('foo', 'foo.gov')
+      );
+    });
+
+    test('with text', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo', {
+          fooLinkWithText: {
+            match: 'foo',
+            link: 'foo.gov',
+            text: 'foo site',
+          },
+        }),
+        link('foo site', 'foo.gov')
+      );
+    });
+
+    test('with prefix and suffix', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('there are 12 foos here', {
+          fooLinkWithText: {
+            match: '(.*)(bug|foo)s(.*)',
+            link: '$2.gov',
+            text: '$2 list',
+            prefix: '$1on the ',
+            suffix: '$3',
+          },
+        }),
+        `there are 12 on the ${link('foo list', 'foo.gov')} here`
+      );
+    });
+
+    test('multiple matches', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo foo', {
+          foo: {
+            match: 'foo',
+            link: 'foo.gov',
+          },
+        }),
+        `${link('foo', 'foo.gov')} ${link('foo', 'foo.gov')}`
+      );
+    });
+
+    test('does not apply within normal links', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('google.com', {
+          ogle: {
+            match: 'ogle',
+            link: 'gerritcodereview.com',
+          },
+        }),
+        link('google.com', 'http://google.com')
+      );
+    });
+  });
+  suite('html rewrites', () => {
+    test('basic case', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo', {
+          foo: {
+            match: '(foo)',
+            html: '<div>$1</div>',
+          },
+        }),
+        '<div>foo</div>'
+      );
+    });
+
+    test('only inserts', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo', {
+          foo: {
+            match: 'foo',
+            html: 'foo bar',
+          },
+        }),
+        'foo bar'
+      );
+    });
+
+    test('only deletes', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo bar baz', {
+          bar: {
+            match: 'bar',
+            html: '',
+          },
+        }),
+        'foo  baz'
+      );
+    });
+
+    test('multiple matches', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('foo foo', {
+          foo: {
+            match: '(foo)',
+            html: '<div>$1</div>',
+          },
+        }),
+        '<div>foo</div> <div>foo</div>'
+      );
+    });
+
+    test('does not apply within normal links', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('google.com', {
+          ogle: {
+            match: 'ogle',
+            html: '<div>gerritcodereview.com<div>',
+          },
+        }),
+        link('google.com', 'http://google.com')
+      );
+    });
   });
 
-  test('applyLinkRewritesFromConfig', () => {
-    const linkedNumber = link('#12345', 'google.com/12345');
-    const linkedFoo = link('foo', 'foo.gov');
-    const linkedBar = link('Bar Page: 300', 'bar.com/page?id=300');
+  test('for overlapping rewrites prefer the latest ending', () => {
     assert.equal(
-      applyLinkRewritesFromConfig('#12345 foo crowbar:12 bar:300', {
-        'number-linker': {
-          match: '#(\\d+)',
-          link: 'google.com/$1',
-        },
-        'foo-linker': {
+      linkifyUrlsAndApplyRewrite('foobarbaz', {
+        foo: {
           match: 'foo',
           link: 'foo.gov',
         },
-        'advanced-link': {
-          match: '(^|\\s)bar:(\\d+)($|\\s)',
-          link: 'bar.com/page?id=$2',
-          text: 'Bar Page: $2',
-          prefix: '$1',
-          suffix: '$3',
+        foobarbaz: {
+          match: 'foobarbaz',
+          html: '<div>foobarbaz.gov</div>',
+        },
+        foobar: {
+          match: 'foobar',
+          link: 'foobar.gov',
         },
       }),
-      `${linkedNumber} ${linkedFoo} crowbar:12 ${linkedBar}`
+      '<div>foobarbaz.gov</div>'
     );
   });
 
-  suite('linkifyNormalUrls', () => {
+  test('overlapping rewrites with same ending prefers earliest start', () => {
+    assert.equal(
+      linkifyUrlsAndApplyRewrite('foobarbaz', {
+        foo: {
+          match: 'baz',
+          link: 'Baz.gov',
+        },
+        foobarbaz: {
+          match: 'foobarbaz',
+          html: '<div>FooBarBaz.gov</div>',
+        },
+        foobar: {
+          match: 'barbaz',
+          link: 'BarBaz.gov',
+        },
+      }),
+      '<div>FooBarBaz.gov</div>'
+    );
+  });
+
+  test('removed overlapping rewrites do not prevent other rewrites', () => {
+    assert.equal(
+      linkifyUrlsAndApplyRewrite('foobarbaz', {
+        foo: {
+          match: 'foo',
+          html: 'FOO',
+        },
+        oobarba: {
+          match: 'oobarba',
+          html: 'OOBARBA',
+        },
+        baz: {
+          match: 'baz',
+          html: 'BAZ',
+        },
+      }),
+      'FOObarBAZ'
+    );
+  });
+
+  test('rewrites do not interfere with each other matching', () => {
+    assert.equal(
+      linkifyUrlsAndApplyRewrite('bugs: 123 234 345', {
+        bug1: {
+          match: '(bugs:) (\\d+)',
+          html: '$1 <div>bug/$2</div>',
+        },
+        bug2: {
+          match: '(bugs:) (\\d+) (\\d+)',
+          html: '$1 $2 <div>bug/$3</div>',
+        },
+        bug3: {
+          match: '(bugs:) (\\d+) (\\d+) (\\d+)',
+          html: '$1 $2 $3 <div>bug/$4</div>',
+        },
+      }),
+      'bugs: <div>bug/123</div> <div>bug/234</div> <div>bug/345</div>'
+    );
+  });
+
+  suite('normal links', () => {
     test('links urls', () => {
       const googleLink = link('google.com', 'http://google.com');
       const mapsLink = link('maps.google.com', 'http://maps.google.com');
 
       assert.equal(
-        linkifyNormalUrls('google.com, maps.google.com'),
+        linkifyUrlsAndApplyRewrite('google.com, maps.google.com', {}),
         `${googleLink}, ${mapsLink}`
       );
     });
@@ -72,7 +233,7 @@
       const fooEmail = link('foo@gmail.com', 'mailto:foo@gmail.com');
       const barEmail = link('bar@gmail.com', 'mailto:bar@gmail.com');
       assert.equal(
-        linkifyNormalUrls('R=foo@gmail.com, bar@gmail.com'),
+        linkifyUrlsAndApplyRewrite('R=foo@gmail.com, bar@gmail.com', {}),
         `R=${fooEmail}, ${barEmail}`
       );
     });
@@ -81,7 +242,7 @@
       const fooEmail = link('foo@gmail.com', 'mailto:foo@gmail.com');
       const barEmail = link('bar@gmail.com', 'mailto:bar@gmail.com');
       assert.equal(
-        linkifyNormalUrls('CC=foo@gmail.com, bar@gmail.com'),
+        linkifyUrlsAndApplyRewrite('CC=foo@gmail.com, bar@gmail.com', {}),
         `CC=${fooEmail}, ${barEmail}`
       );
     });
@@ -92,7 +253,7 @@
         'mailto:fooR=barCC=baz@gmail.com'
       );
       assert.equal(
-        linkifyNormalUrls('fooR=barCC=baz@gmail.com'),
+        linkifyUrlsAndApplyRewrite('fooR=barCC=baz@gmail.com', {}),
         fooBarBazEmail
       );
     });
diff --git a/polygerrit-ui/app/utils/string-util_test.ts b/polygerrit-ui/app/utils/string-util_test.ts
index 0a7ee27..c6c65b1 100644
--- a/polygerrit-ui/app/utils/string-util_test.ts
+++ b/polygerrit-ui/app/utils/string-util_test.ts
@@ -12,7 +12,7 @@
   diffFilePaths,
 } from './string-util';
 
-suite('formatter util tests', () => {
+suite('string-util tests', () => {
   test('pluralize', () => {
     const noun = 'comment';
     assert.equal(pluralize(0, noun), '');
diff --git a/tools/BUILD b/tools/BUILD
index f2d887e..a785c1b 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -46,12 +46,13 @@
         "-XepDisableWarningsInGeneratedCode",
         # The XepDisableWarningsInGeneratedCode disables only warnings, but
         # not errors. We should manually exclude all files generated by
-        # AutoValue; such files always start AutoValue_..., $AutoValue_...
-        # or $$AutoValue_...
+        # AutoValue; such files always start AutoValue_..., $AutoValue_...,
+        # $$AutoValue_... or AutoValueGson_...
         # XepExcludedPaths is a regexp. If you need more paths - use | as
         # separator.
-        "-XepExcludedPaths:.*/\\\\$$?\\\\$$?AutoValue_.*\\.java",
+        "-XepExcludedPaths:.*/\\\\$$?\\\\$$?AutoValue(Gson)?_.*\\.java",
         "-Xep:AlmostJavadoc:ERROR",
+        "-Xep:AlreadyChecked:ERROR",
         "-Xep:AlwaysThrows:ERROR",
         "-Xep:AmbiguousMethodReference:ERROR",
         "-Xep:AnnotateFormatMethod:ERROR",
@@ -351,6 +352,7 @@
         "-Xep:RestrictedApiChecker:ERROR",
         "-Xep:RethrowReflectiveOperationExceptionAsLinkageError:ERROR",
         "-Xep:ReturnFromVoid:ERROR",
+        "-Xep:ReturnMissingNullable:ERROR",
         "-Xep:ReturnValueIgnored:ERROR",
         "-Xep:RxReturnValueIgnored:ERROR",
         "-Xep:SameNameButDifferent:ERROR",