Merge "Remove property _showInlineDiff"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 2a019ca..3da69df 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -477,6 +477,11 @@
 `+refs/heads/sandbox/${username}/*+`. If you do, it's also recommended
 you grant the users the push force permission to be able to clean up
 stale branches.
+If link:config-gerrit.html#auth.userNameCaseInsensitive[auth.userNameCaseInsensitive]
+is enabled, the `${username}` is still case sensitive and will use
+the capitalization used during account creation. This is done, since
+git branches are case sensitive, so that sandbox branches containing
+`${username}` are still reachable by the users.
 
 [[category_delete]]
 === Delete Reference
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index 6808e017..02eaf83 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -14,7 +14,8 @@
   [--delete-ssh-key - | <KEY> | ALL]
   [--generate-http-password]
   [--http-password <PASSWORD>]
-  [--clear-http-password] <USER>
+  [--clear-http-password]
+  [--delete-external-id <EXTERNALID>] <USER>
 --
 
 == DESCRIPTION
@@ -106,6 +107,13 @@
 --clear-http-password::
     Clear the HTTP password for the user account.
 
+--delete-external-id::
+    Delete an external ID from a user's account if it exists.
+    If the external ID provided is 'ALL', all associated
+    external IDs are deleted from this account.
+    May be supplied more than once to remove multiple external
+    IDs from an account in a single command execution.
+
 == EXAMPLES
 Add an email and SSH key to `watcher`'s account:
 
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index 9b99960..7a7cef2 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -3,8 +3,7 @@
 
 == Overview
 
-Starting from 2.15 Gerrit accounts are fully stored in
-link:note-db.html[NoteDb].
+Gerrit accounts are stored in link:note-db.html[NoteDb].
 
 The account data consists of a sequence number (account ID), account
 properties (full name, display name, preferred email, registration
@@ -298,6 +297,13 @@
 This ensures that an external ID is used only once (e.g. an external ID can
 never be assigned to multiple accounts at a point in time).
 
+By default, the SHA-1 sum is computed preserving the case of the external ID. If
+auth.userNameCaseInsensitive` is set to `true`, the SHA-1 sum of external IDs
+in the `gerrit:` and `username:` schemes are computed from the all lowercase
+external ID. This enables case insensitive username handling. The case of the
+external ID is however preserved by using the original capitalization in the
+note content.
+
 The following commands show how to find the SHA-1 of an external ID:
 
 ----
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 32867b6..76e1f82 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -652,6 +652,32 @@
 +
 By default this is set to false.
 
+[[auth.userNameCaseInsensitive]]auth.userNameCaseInsensitive::
++
+If set the username will be handled case insensitively but case preserving,
+i.e. a user can login with `johndoe` or `JohnDoe` for the same account
+created for `JohnDoe`. The form of the username used during account creation
+will be used wherever the username is displayed. Sandbox branches created
+for a user can also only be created for this original form.
++
+Note, that this does not work for all existing accounts, if they were
+not originally created with all lowercase, since the note keys of the
+external IDs will not match the new scheme. For more details refer to
+the link:config-accounts.html#external-ids[External ID documentation].
++
+Gerrit provides the
+link:pgm-ChangeExternalIdCaseSensitivity.html[ChangeExternalIdCaseSensitivity tool]
+to migrate existing accounts to match the new scheme.
++
+Naturally, if there were two accounts only different in capitalization,
+e.g. `johndoe` and `JohnDoe`, the account `JohnDoe` will not be able
+to authenticate anymore after setting this option. If such duplicate
+accounts exist the migration tool will fail, since the newly computed
+note name would be identical and thus conflict. These duplicates thus
+have to be deleted manually by deleting the respective external ID.
++
+Default is false.
+
 [[auth.enableRunAs]]auth.enableRunAs::
 +
 If true HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
@@ -4454,8 +4480,7 @@
 [[retry.retryWithTraceOnFailure]]retry.retryWithTraceOnFailure::
 +
 Whether Gerrit should automatically retry operations on failure with tracing
-enabled. The automatically generated traces can help with debugging. Please
-note that only some of the REST endpoints support automatic retry.
+enabled. The automatically generated traces can help with debugging.
 +
 By default this is set to false.
 
@@ -5315,13 +5340,27 @@
 [[tracing.traceid.requestUriPattern]]tracing.<trace-id>.requestUriPattern::
 +
 Regular expression to match request URIs for which request tracing
-should be always enabled. Request URIs are only available for REST
-requests. Request URIs never include the '/a' prefix.
+should be enabled except if they match
+link:tracing.traceid.excludedRequestUriPattern[excludedRequestUriPattern].
+Request URIs are only available for REST requests. Request URIs never include
+the '/a' prefix.
 +
 May be specified multiple times.
 +
 By default, unset (all request URIs are matched).
 
+[[tracing.traceid.excludedRequestUriPattern]]tracing.<trace-id>.excludedRequestUriPattern::
++
+Regular expression to match request URIs for which request tracing
+should not be enabled even if they match
+link:#tracing.traceid.requestUriPattern[requestUriPattern].
+Request URIs are only available for REST requests. Request URIs never include
+the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (no request URIs are excluded).
+
 [[tracing.traceid.account]]tracing.<trace-id>.account::
 +
 Account ID of an account for which request tracing should be always
@@ -5388,7 +5427,9 @@
 
 [[deadline.id.requestUriPattern]]deadline.<id>.requestUriPattern::
 +
-Regular expression to match request URIs to which the deadline applies. Request
+Regular expression to match request URIs to which the deadline applies except if
+they match
+link:#deadline.id.excludedRequestUriPattern[excludedRequestUriPattern]. Request
 URIs are only available for REST requests. Request URIs never include the '/a'
 prefix.
 +
@@ -5396,6 +5437,17 @@
 +
 By default, unset (all request URIs are matched).
 
+[[deadline.id.excludedRequestUriPattern]]deadline.<id>.excludedRequestUriPattern::
++
+Regular expression to match request URIs to which the deadline should not be
+applied even if they match
+link:#deadline.id.requestUriPattern[requestUriPattern]. Request URIs are only
+available for REST requests. Request URIs never include the '/a' prefix.
++
+May be specified multiple times.
++
+By default, unset (no request URIs are excluded).
+
 [[deadline.id.account]]deadline.<id>.account::
 +
 Account ID of an account to which the deadline applies.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 992d459..fa2b78c 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -74,9 +74,9 @@
 manifest fields:
 
 ----
-  Implementation-Title: Example plugin showing examples
-  Implementation-Version: 1.0
-  Implementation-Vendor: Example, Inc.
+Implementation-Title: Example plugin showing examples
+Implementation-Version: 1.0
+Implementation-Vendor: Example, Inc.
 ----
 
 === ApiType
@@ -88,7 +88,7 @@
 loading a plugin that needs the plugin API.
 
 ----
-  Gerrit-ApiType: plugin
+Gerrit-ApiType: plugin
 ----
 
 === Explicit Registration
@@ -103,9 +103,9 @@
 `@Listen` and `@Export("")` annotations.
 
 ----
-  Gerrit-Module:     tld.example.project.CoreModuleClassName
-  Gerrit-SshModule:  tld.example.project.SshModuleClassName
-  Gerrit-HttpModule: tld.example.project.HttpModuleClassName
+Gerrit-Module:     tld.example.project.CoreModuleClassName
+Gerrit-SshModule:  tld.example.project.SshModuleClassName
+Gerrit-HttpModule: tld.example.project.HttpModuleClassName
 ----
 
 === Batch runtime
@@ -120,7 +120,7 @@
 offline reindexing task.
 
 ----
-  Gerrit-BatchModule: tld.example.project.CoreModuleClassName
+Gerrit-BatchModule: tld.example.project.CoreModuleClassName
 ----
 
 In this runtime, only the module designated by `Gerrit-BatchModule` is
@@ -132,7 +132,7 @@
 A plugin can optionally provide its own plugin name.
 
 ----
-  Gerrit-PluginName: replication
+Gerrit-PluginName: replication
 ----
 
 This is useful for plugins that contribute plugin-owned capabilities that
@@ -218,7 +218,7 @@
 with no down time.
 
 ----
-  Gerrit-ReloadMode: restart
+Gerrit-ReloadMode: restart
 ----
 
 In either mode ('restart' or 'reload') any plugin or extension can
@@ -261,7 +261,7 @@
 credentials and possibly verify connectivity to validate them.
 
 ----
-  Gerrit-InitStep: tld.example.project.MyInitStep
+Gerrit-InitStep: tld.example.project.MyInitStep
 ----
 
 MyInitStep needs to follow the standard Gerrit InitStep syntax
@@ -278,37 +278,37 @@
 
 [source,java]
 ----
-  public class MyInitStep implements InitStep {
-    private final String pluginName;
-    private final ConsoleUI ui;
-    private final AllProjectsConfig allProjectsConfig;
+public class MyInitStep implements InitStep {
+  private final String pluginName;
+  private final ConsoleUI ui;
+  private final AllProjectsConfig allProjectsConfig;
 
-    @Inject
-    public MyInitStep(@PluginName String pluginName, ConsoleUI ui,
-        AllProjectsConfig allProjectsConfig) {
-      this.pluginName = pluginName;
-      this.ui = ui;
-      this.allProjectsConfig = allProjectsConfig;
-    }
-
-    @Override
-    public void run() throws Exception {
-    }
-
-    @Override
-    public void postRun() throws Exception {
-      ui.message("\n");
-      ui.header(pluginName + " Integration");
-      boolean enabled = ui.yesno(true, "By default enabled for all projects");
-      Config cfg = allProjectsConfig.load().getConfig();
-      if (enabled) {
-        cfg.setBoolean("plugin", pluginName, "enabled", enabled);
-      } else {
-        cfg.unset("plugin", pluginName, "enabled");
-      }
-      allProjectsConfig.save(pluginName, "Initialize " + pluginName + " Integration");
-    }
+  @Inject
+  public MyInitStep(@PluginName String pluginName, ConsoleUI ui,
+      AllProjectsConfig allProjectsConfig) {
+    this.pluginName = pluginName;
+    this.ui = ui;
+    this.allProjectsConfig = allProjectsConfig;
   }
+
+  @Override
+  public void run() throws Exception {
+  }
+
+  @Override
+  public void postRun() throws Exception {
+    ui.message("\n");
+    ui.header(pluginName + " Integration");
+    boolean enabled = ui.yesno(true, "By default enabled for all projects");
+    Config cfg = allProjectsConfig.load().getConfig();
+    if (enabled) {
+      cfg.setBoolean("plugin", pluginName, "enabled", enabled);
+    } else {
+      cfg.unset("plugin", pluginName, "enabled");
+    }
+    allProjectsConfig.save(pluginName, "Initialize " + pluginName + " Integration");
+  }
+}
 ----
 
 Bear in mind that the Plugin's InitStep class will be loaded but
@@ -706,9 +706,12 @@
 in between, leading to the final operator name.  An example
 registration looks like this:
 
-    bind(ChangeOperatorFactory.class)
-       .annotatedWith(Exports.named("sample"))
-       .to(SampleOperator.class);
+[source,java]
+----
+bind(ChangeOperatorFactory.class)
+  .annotatedWith(Exports.named("sample"))
+  .to(SampleOperator.class);
+----
 
 If this is registered in the `myplugin` plugin, then the resulting
 operator will be named `sample_myplugin`.
@@ -736,7 +739,7 @@
 ----
 
 [[search_operands]]
-=== Search Operands ===
+== Search Operands
 
 Plugins can define new search operands to extend change searching.
 Plugin methods implementing search operands (returning a
@@ -748,31 +751,33 @@
 a module's `configure()` method in the plugin.
 
 The new operand, when used in a search would appear as:
-  operatorName:operandName_pluginName
+  `operatorName:operandName_pluginName`
 
 A sample `ChangeHasOperandFactory` class implementing, and registering, a
 new `has:sample_pluginName` operand is shown below:
 
-====
-  public class SampleHasOperand implements ChangeHasOperandFactory {
-    public static class Module extends AbstractModule {
-      @Override
-      protected void configure() {
-        bind(ChangeHasOperandFactory.class)
-            .annotatedWith(Exports.named("sample")
-            .to(SampleHasOperand.class);
-      }
-    }
-
+[source, java]
+----
+public class SampleHasOperand implements ChangeHasOperandFactory {
+  public static class Module extends AbstractModule {
     @Override
-    public Predicate<ChangeData> create(ChangeQueryBuilder builder)
-        throws QueryParseException {
-      return new HasSamplePredicate();
+    protected void configure() {
+      bind(ChangeHasOperandFactory.class)
+          .annotatedWith(Exports.named("sample")
+          .to(SampleHasOperand.class);
     }
-====
+  }
+
+  @Override
+  public Predicate<ChangeData> create(ChangeQueryBuilder builder)
+      throws QueryParseException {
+    return new HasSamplePredicate();
+  }
+}
+----
 
 [[command_options]]
-=== Command Options ===
+== Command Options
 
 Plugins can provide additional options for each of the gerrit ssh and the
 REST API commands by implementing the DynamicBean interface and registering
@@ -801,6 +806,7 @@
       logger.atSevere().log("Say Hello in the Log %s", arg);
     }
   }
+}
 ----
 
 To provide additional Guice bindings for options to a command in another classloader, bind a
@@ -815,21 +821,21 @@
 
 [source, java]
 ----
-  bind(DynamicOptions.DynamicBean.class)
-      .annotatedWith(Exports.named(
-          "com.google.gerrit.plugins.otherplugin.command"))
-      .to(MyOptionsModulesClassNamesProvider.class);
+bind(DynamicOptions.DynamicBean.class)
+    .annotatedWith(Exports.named(
+        "com.google.gerrit.plugins.otherplugin.command"))
+    .to(MyOptionsModulesClassNamesProvider.class);
 
-  static class MyOptionsModulesClassNamesProvider implements DynamicOptions.ModulesClassNamesProvider {
-    {@literal @}Override
-    public String getClassName() {
-      return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
-    }
-    {@literal @}Override
-    public Iterable<String> getModulesClassNames()() {
-      return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule";
-    }
+static class MyOptionsModulesClassNamesProvider implements DynamicOptions.ModulesClassNamesProvider {
+  @Override
+  public String getClassName() {
+    return "com.googlesource.gerrit.plugins.myplugin.CommandOptions";
   }
+  @Override
+  public Iterable<String> getModulesClassNames()() {
+    return "com.googlesource.gerrit.plugins.myplugin.MyOptionsModule";
+  }
+}
 ----
 
 === Calling Command Options ===
@@ -891,7 +897,7 @@
 ----
 
 [[query_attributes]]
-=== Change Attributes ===
+== Change Attributes
 
 ==== ChangePluginDefinedInfoFactory
 
@@ -967,13 +973,9 @@
 }
 ----
 
-Example
+Example:
 ----
-
-ssh -p 29418 localhost gerrit query --myplugin-name--all "change:1" --format json
-
-Output:
-
+$ ssh -p 29418 localhost gerrit query --myplugin-name--all "change:1" --format json
 {
    "url" : "http://localhost:8080/1",
    "plugins" : [
@@ -986,10 +988,7 @@
     ...
 }
 
-curl http://localhost:8080/changes/1?myplugin-name--all
-
-Output:
-
+$ curl http://localhost:8080/changes/1?myplugin-name--all
 {
   "_number": 1,
   ...
@@ -1133,8 +1132,8 @@
 `plugin.helloworld` subsection:
 
 ----
-  [plugin "helloworld"]
-    enabled = true
+[plugin "helloworld"]
+  enabled = true
 ----
 
 Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
@@ -1337,7 +1336,7 @@
 Here and example of ref-updated JSON event payload with `instanceId`:
 
 [source,json]
----
+----
 {
   "submitter": {
     "name": "Administrator",
@@ -1354,7 +1353,7 @@
   "eventCreatedOn": 1588849085,
   "instanceId": "instance1"
 }
----
+----
 
 [[capabilities]]
 == Plugin Owned Capabilities
@@ -1681,11 +1680,11 @@
 can be accessed from any REST client, i. e.:
 
 ----
-  curl -X POST -H "Content-Type: application/json" \
+$ curl -X POST -H "Content-Type: application/json" \
     -d '{message: "François", french: true}' \
     --user joe:secret \
     http://host:port/a/changes/1/revisions/1/cookbook~say-hello
-  "Bonjour François from change 1, patch set 1!"
+"Bonjour François from change 1, patch set 1!"
 ----
 
 A special case is to bind an endpoint without a view name.  This is
@@ -1782,7 +1781,6 @@
 [source,java]
 ----
 public class MyTopMenuExtension implements TopMenu {
-
   @Override
   public List<MenuEntry> getEntries() {
     return Lists.newArrayList(
@@ -1799,7 +1797,6 @@
 [source,java]
 ----
 public class MyTopMenuExtension implements TopMenu {
-
   @Override
   public List<MenuEntry> getEntries() {
     return Lists.newArrayList(
@@ -1817,17 +1814,17 @@
 specific requests and add an menu item for this:
 
 [source,java]
----
-  new MenuItem("My Screen", "/plugins/myplugin/project/${projectName}");
----
+----
+new MenuItem("My Screen", "/plugins/myplugin/project/${projectName}");
+----
 
 This also enables plugins to provide menu items for project aware
 screens:
 
 [source,java]
----
-  new MenuItem("My Screen", "/x/my-screen/for/${projectName}");
----
+----
+new MenuItem("My Screen", "/x/my-screen/for/${projectName}");
+----
 
 If no Guice modules are declared in the manifest, the top menu extension may use
 auto-registration by providing an `@Listen` annotation:
@@ -2041,7 +2038,6 @@
 bind(AccountExternalIdCreator.class)
   .annotatedWith(UniqueAnnotations.create())
   .to(MyExternalIdCreator.class);
-}
 ----
 
 [[download-commands]]
@@ -2108,7 +2104,6 @@
 
 @Listen
 public class MyWeblinkPlugin implements PatchSetWebLink {
-
   private String name = "MyLink";
   private String placeHolderUrlProjectCommit = "http://my.tool.com/project=%s/commit=%s";
   private String imageUrl = "http://placehold.it/16x16.gif";
@@ -2189,7 +2184,6 @@
 import com.google.inject.servlet.ServletModule;
 
 public class HttpModule extends ServletModule {
-
   @Override
   protected void configureServlets() {
     serveRegex(URL_REGEX).with(LfsApiServlet.class);
@@ -2200,7 +2194,7 @@
 import org.eclipse.jgit.lfs.server.s3.S3Repository;
 
 public class S3LargeFileRepository extends S3Repository {
-...
+  ...
 }
 ----
 
@@ -2250,8 +2244,8 @@
 file. For example:
 
 ----
-  [plugin "my-plugin"]
-    metricsPrefix = my-metrics
+[plugin "my-plugin"]
+  metricsPrefix = my-metrics
 ----
 
 will cause the metrics to be recorded under `my-metrics/${metric-name}`.
@@ -2274,6 +2268,7 @@
 implementation, e.g. one that supports cluster setup with multiple
 primary Gerrit nodes handling write operations.
 
+[source,java]
 ----
 DynamicItem.bind(binder(), AccountPatchReviewStore.class)
     .to(MultiMasterAccountPatchReviewStore.class);
@@ -2521,6 +2516,26 @@
 }
 ----
 
+
+[[account-tag]]
+== Account Tag Plugins
+
+Gerrit provides an extension point that enables Plugins to supply additional
+tags on an account.
+
+[source, java]
+----
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.AccountTagProvider;
+import java.util.List;
+
+public class MyPlugin implements AccountTagProvider {
+  public List<String> getTags(Account.Id id) {
+    // Implement your logic here
+  }
+}
+----
+
 [[ssh-command-creation-interception]]
 == SSH Command Creation Interception
 
@@ -2536,6 +2551,8 @@
   @Override
   public String intercept(String in) {
     return pluginName + " mycommand";
+  }
+}
 ----
 
 [[ssh-command-execution-interception]]
@@ -2569,7 +2586,8 @@
 And then declare it in your SSH module:
 [source, java]
 ----
-  DynamicSet.bind(binder(), SshExecuteCommandInterceptor.class).to(SshExecuteCommandInterceptorImpl.class);
+DynamicSet.bind(binder(), SshExecuteCommandInterceptor.class)
+  .to(SshExecuteCommandInterceptorImpl.class);
 ----
 
 [[pre-submit-evaluator]]
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt
index a83ad44..764e326 100644
--- a/Documentation/dev-stars.txt
+++ b/Documentation/dev-stars.txt
@@ -29,9 +29,6 @@
 There are link:rest-api-accounts.html#default-star-endpoints[
 additional REST endpoints] for the link:#default-star[default star].
 
-Only the link:#default-star[default star] is shown in the WebUI and
-can be updated from there. Other stars do not show up in the WebUI.
-
 [[default-star]]
 == Default Star
 
@@ -61,36 +58,11 @@
 
 The ignore star is represented by the special star label 'ignore'.
 
-[[reviewed-star]]
-== Reviewed Star
-
-If the "reviewed/<patchset_id>"-star is set by a user, and <patchset_id>
-matches the current patch set, the change is always reported as "reviewed"
-in the ChangeInfo.
-
-This allows users to "de-highlight" changes in a dashboard until a new
-patchset has been uploaded.
-
-[[unreviewed-star]]
-== Unreviewed Star
-
-If the "unreviewed/<patchset_id>"-star is set by a user, and <patchset_id>
-matches the current patch set, the change is always reported as "unreviewed"
-in the ChangeInfo.
-
-This allows users to "highlight" changes in a dashboard.
-
 [[query-stars]]
 == Query Stars
 
 There are several query operators to find changes with stars:
 
-* link:user-search.html#star[star:<LABEL>]:
-  Matches any change that was starred by the current user with the
-  label `<LABEL>`.
-* link:user-search.html#has-stars[has:stars]:
-  Matches any change that was starred by the current user with any
-  label.
 * link:user-search.html#is-starred[is:starred] /
   link:user-search.html#has-star[has:star]:
   Matches any change that was starred by the current user with the
diff --git a/Documentation/index.txt b/Documentation/index.txt
index dc94b14..782a6a9 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -77,6 +77,7 @@
 . link:pgm-index.html[Server Side Administrative Tools]
 . link:repository-maintenance.html[Repository Maintenance]
 . link:user-request-tracing.html[Request Tracing]
+. link:user-request-cancellation-and-deadlines.html[Request Cancellation and Deadlines]
 . link:note-db.html[NoteDb]
 . link:config-accounts.html[Accounts on NoteDb]
 . link:config-groups.html[Groups on NoteDb]
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 3953737..ab79c8f 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -252,6 +252,7 @@
 
 * @types/resemblejs
 * @types/resize-observer-browser
+* @types/trusted-types
 
 [[DefinitelyTyped_license]]
 ----
@@ -280,6 +281,48 @@
 ----
 
 
+[[Lit]]
+Lit
+
+* @lit/reactive-element
+* lit
+* lit-element
+* lit-html
+
+[[Lit_license]]
+----
+BSD 3-Clause License
+
+Copyright (c) 2017 Google LLC. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
 [[Polymer-2014]]
 Polymer-2014
 
@@ -1141,84 +1184,6 @@
 ----
 
 
-[[lit-element]]
-lit-element
-
-* lit-element
-
-[[lit-element_license]]
-----
-BSD 3-Clause License
-
-Copyright (c) 2017, The Polymer Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
-[[lit-html]]
-lit-html
-
-* lit-html
-
-[[lit-html_license]]
-----
-BSD 3-Clause License
-
-Copyright (c) 2017, The Polymer Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
 [[page]]
 page
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 476751c..86ff904 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3211,6 +3211,7 @@
 
 * @types/resemblejs
 * @types/resize-observer-browser
+* @types/trusted-types
 
 [[DefinitelyTyped_license]]
 ----
@@ -3239,6 +3240,48 @@
 ----
 
 
+[[Lit]]
+Lit
+
+* @lit/reactive-element
+* lit
+* lit-element
+* lit-html
+
+[[Lit_license]]
+----
+BSD 3-Clause License
+
+Copyright (c) 2017 Google LLC. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+
 [[Polymer-2014]]
 Polymer-2014
 
@@ -4100,84 +4143,6 @@
 ----
 
 
-[[lit-element]]
-lit-element
-
-* lit-element
-
-[[lit-element_license]]
-----
-BSD 3-Clause License
-
-Copyright (c) 2017, The Polymer Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
-[[lit-html]]
-lit-html
-
-* lit-html
-
-[[lit-html_license]]
-----
-BSD 3-Clause License
-
-Copyright (c) 2017, The Polymer Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* Neither the name of the copyright holder nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
 [[page]]
 page
 
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 03be71e..36b3473 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -12,32 +12,121 @@
 
 * `build/label`: Version of Gerrit server software.
 * `events`: Triggered events.
+** `type`:
+   The type of the event.
 
 === Actions
 
 * `action/retry_attempt_count`: Number of retry attempts made
-by RetryHelper to execute an action (0 == single attempt, no retry)
+  by RetryHelper to execute an action (0 == single attempt, no retry)
+** `action_type`:
+   The type of the action that was retried.
+** `operation_name`:
+   The name of the operation that was retried.
+** `cause`:
+   The original cause that triggered the retry.
 * `action/retry_timeout_count`: Number of action executions of RetryHelper
-that ultimately timed out
+  that ultimately timed out
+** `action_type`:
+   The type of the action that was retried.
+** `operation_name`:
+   The name of the operation that was retried.
+** `cause`:
+   The original cause that triggered the retry.
 * `action/auto_retry_count`: Number of automatic retries with tracing
+** `action_type`:
+   The type of the action that was retried.
+** `operation_name`:
+   The name of the operation that was retried.
+** `cause`:
+   The cause for the retry.
 * `action/failures_on_auto_retry_count`: Number of failures on auto retry
+** `action_type`:
+   The type of the action that was retried.
+** `operation_name`:
+   The name of the operation that was retried.
+** `cause`:
+   The cause for the retry.
 
+[[cancellations]]
 === Cancellations
 
 * `cancellation/advisory_deadline_count`: Exceeded advisory deadlines by request
+** `request_type`:
+   The type of the request to which the advisory deadline applied.
+** `request_uri`:
+   The redacted URI of the request to which the advisory deadline applied (only
+   set for request_type = REST).
+** `deadline_id`:
+   The ID of the advisory deadline.
 * `cancellation/cancelled_requests_count`: Number of request cancellations by
   request
+** `request_type`:
+   The type of the request that was cancelled.
+** `request_uri`:
+   The redacted URI of the request that was cancelled (only set for
+   request_type = REST).
+** `cancellation_reason`:
+   The reason why the request was cancelled.
+* `cancellation/receive_timeout_count`: Number of requests that are cancelled
+  because link:config.html#receive.timeout[receive.timout] is exceeded
+** `cancellation_type`:
+   The cancellation type (graceful or forceful).
+
+[[performance]]
+=== Performance
+
+* `performance/operations`: Latency of performing operations
+** `operation_name`:
+   The operation that was performed.
+** `change_identifier`:
+   The ID of the change for which the operation was performed (format =
+   '<project>~<numeric-change-id>').
+** `trace_id`:
+   The ID of the trace if tracing was done.
+* `performance/operations_count`: Number of performed operations
+** `operation_name`:
+   The operation that was performed.
+** `trace_id`:
+   The ID of the trace if tracing was done.
+** `request`:
+   The request for which the operation was performed (format = '<request-type>
+   <redacted-request-uri>').
+* `performance/plugin_operations_count`: Number of performed operations by
+  plugin
+** `operation_name`:
+   The operation that was performed.
+** `plugin`:
+   The name of the plugin that performed the operation.
+** `trace_id`:
+   The ID of the trace if tracing was done.
+
 
 === Pushes
 
-* `receivecommits/changes`: histogram of number of changes processed
-in a single upload, split up by update type (change created/updated,
-change autoclosed).
-* `receivecommits/latency`: latency per change for processing a push,
-split up by update type (create+replace, and autoclose)
-* `receivecommits/push_latency`: total latency for processing a push,
-split up by update type (create+replace, autoclose, normal)
-* `receivecommits/timeout`: number of timeouts during push processing.
+* `receivecommits/changes`: histogram of number of changes processed in a single
+   upload
+** `type`:
+   type of push (create/replace, autoclose)
+* `receivecommits/latency_per_push`: processing delay for a processing single
+  push
+** `type`:
+   type of push (create/replace, autoclose, normal)
+* `receivecommits/latency_per_push_per_change`: Processing delay per push
+  divided by the number of changes in said push. (Only includes pushes which
+  contain changes.)
+** `type`:
+   type of push (create/replace, autoclose, normal)
+* `receivecommits/timeout`: rate of push timeouts
+* `receivecommits/ps_revision_missing`: errors due to patch set revision missing
+* `receivecommits/push_count`: number of pushes
+** `kind`:
+   The push kind (direct vs. magic).
+** `project`:
+   The name of the project for which the push is done.
+** `type`:
+   The type of the update (CREATE, UPDATE, CREATE/UPDATE, UPDATE_NONFASTFORWARD,
+   DELETE).
 
 === Process
 
@@ -55,25 +144,58 @@
 * `proc/jvm/memory/object_pending_finalization_count`: Approximate number of
 objects needing finalization.
 * `proc/jvm/gc/count`: Number of GCs.
+** `gc_name`:
+   The name of the garbage collector.
 * `proc/jvm/gc/time`: Approximate accumulated GC elapsed time.
-* `proc/jvm/memory/pool/committed/<pool name>`: Committed amount of memory for pool.
-* `proc/jvm/memory/pool/max/<pool name>`: Maximum amount of memory for pool.
-* `proc/jvm/memory/pool/used/<pool name>`: Used amount of memory for pool.
+** `gc_name`:
+   The name of the garbage collector.
+* `proc/jvm/memory/pool/committed`: Committed amount of memory for pool.
+** `pool_name`:
+   The name of the memory pool.
+* `proc/jvm/memory/pool/max`: Maximum amount of memory for pool.
+** `pool_name`:
+   The name of the memory pool.
+* `proc/jvm/memory/pool/used`: Used amount of memory for pool.
+** `pool_name`:
+   The name of the memory pool.
 * `proc/jvm/thread/num_live`: Current live thread count.
 * `proc/jvm/thread/num_daemon_live`: Current live daemon threads count.
-* `proc/jvm/thread/num_peak_live`: Peak live thread count since the Java virtual machine started or peak was reset.
-* `proc/jvm/thread/num_total_started`: Total number of threads created and also started since the Java virtual machine started.
-* `proc/jvm/thread/num_deadlocked_threads`: Number of threads that are deadlocked waiting for object monitors or ownable synchronizers.
-   If deadlocks waiting for ownable synchronizers can be monitored depends on the capabilities of the used JVM.
+* `proc/jvm/thread/num_peak_live`: Peak live thread count since the Java virtual
+  machine started or peak was reset.
+* `proc/jvm/thread/num_total_started`: Total number of threads created and also
+  started since the Java virtual machine started.
+* `proc/jvm/thread/num_deadlocked_threads`: Number of threads that are
+  deadlocked waiting for object monitors or ownable synchronizers.
+  If deadlocks waiting for ownable synchronizers can be monitored depends on the
+  capabilities of the used JVM.
 
 === Caches
 
 * `caches/memory_cached`: Memory entries.
+** `cache_name`:
+   The name of the cache.
 * `caches/memory_hit_ratio`: Memory hit ratio.
+** `cache_name`:
+   The name of the cache.
 * `caches/memory_eviction_count`: Memory eviction count.
+** `cache_name`:
+   The name of the cache.
 * `caches/disk_cached`: Disk entries used by persistent cache.
+** `cache_name`:
+   The name of the cache.
 * `caches/disk_hit_ratio`: Disk hit ratio for persistent cache.
-* `caches/refresh_count`: The number of refreshes per cache with an indicator if a reload was necessary.
+** `cache_name`:
+   The name of the cache.
+* `caches/refresh_count`: The number of refreshes per cache with an indicator if
+  a reload was necessary.
+** `cache`:
+   The name of the cache.
+** `outdated`:
+   Whether the cache entry was outdated on reload.
+* `caches/diff/timeouts`: The number of git file diff computations that resulted
+  in timeouts.
+* `caches/diff/legacy/timeouts`: The number of git file diff computations (using
+  the legacy cache) that resulted in timeouts.
 
 Cache disk metrics are expensive to compute on larger installations and are not
 computed by default. They can be enabled via the
@@ -82,65 +204,110 @@
 
 === Change
 
-* `change/submit_rule_evaluation`: Latency for evaluating submit rules on a change.
-* `change/submit_type_evaluation`: Latency for evaluating the submit type on a change.
+* `change/submit_rule_evaluation`: Latency for evaluating submit rules on a
+  change.
+* `change/submit_type_evaluation`: Latency for evaluating the submit type on a
+  change.
+* `change/post_review/draft_handling`: Total number of draft handling option
+  (KEEP, PUBLISH, PUBLISH_ALL_REVISIONS) selected by users while posting a
+  review.
+** `type`:
+  The type of the draft handling option (KEEP, PUBLISH, PUBLISH_ALL_REVISIONS).
 
 === Comments
 
-* `ported_comments/as_patchset_level`: Total number of comments ported as patchset-level comments.
-* `ported_comments/as_file_level`: Total number of comments ported as file-level comments.
-* `ported_comments/as_range_comments`: Total number of comments having line/range values in the ported patchset.
+* `ported_comments/as_patchset_level`: Total number of comments ported as
+  patchset-level comments.
+* `ported_comments/as_file_level`: Total number of comments ported as file-level
+  comments.
+* `ported_comments/as_range_comments`: Total number of comments having
+  line/range values in the ported patchset.
 
 === HTTP
 
 ==== Jetty
 
-* `http/server/jetty/connections/connections`: The current number of open connections
-* `http/server/jetty/connections/connections_total`: The total number of connections opened
-* `http/server/jetty/connections/connections_duration_max`: The max duration of a connection in ms
-* `http/server/jetty/connections/connections_duration_mean`: The mean duration of a connection in ms
-* `http/server/jetty/connections/connections_duration_stdev`: The standard deviation of the duration of a connection in ms
-* `http/server/jetty/connections/received_messages`: The total number of messages received
-* `http/server/jetty/connections/sent_messages`: The total number of messages sent
-* `http/server/jetty/connections/received_bytes`: Total number of bytes received by tracked connections
-* `http/server/jetty/connections/sent_bytes`: Total number of bytes sent by tracked connections"
+* `http/server/jetty/connections/connections`: The current number of open
+  connections
+* `http/server/jetty/connections/connections_total`: The total number of
+  connections opened
+* `http/server/jetty/connections/connections_duration_max`: The max duration of
+  a connection in ms
+* `http/server/jetty/connections/connections_duration_mean`: The mean duration
+  of a connection in ms
+* `http/server/jetty/connections/connections_duration_stdev`: The standard
+  deviation of the duration of a connection in ms
+* `http/server/jetty/connections/received_messages`: The total number of
+  messages received
+* `http/server/jetty/connections/sent_messages`: The total number of messages
+  sent
+* `http/server/jetty/connections/received_bytes`: Total number of bytes received
+  by tracked connections
+* `http/server/jetty/connections/sent_bytes`: Total number of bytes sent by
+  tracked connections
 * `http/server/jetty/threadpool/active_threads`: Active threads
 * `http/server/jetty/threadpool/idle_threads`: Idle threads
 * `http/server/jetty/threadpool/reserved_threads`: Reserved threads
 * `http/server/jetty/threadpool/max_pool_size`: Maximum thread pool size
 * `http/server/jetty/threadpool/min_pool_size`: Minimum thread pool size
 * `http/server/jetty/threadpool/pool_size`: Current thread pool size
-* `http/server/jetty/threadpool/queue_size`: Queued requests waiting for a thread
+* `http/server/jetty/threadpool/queue_size`: Queued requests waiting for a
+  thread
+* `http/server/jetty/threadpool/is_low_on_threads`: Whether thread pool is low
+  on threads
 
 ==== LDAP
 
 * `ldap/login_latency`: Latency of logins.
 * `ldap/user_search_latency`: Latency for searching the user account.
-* `ldap/group_search_latency`: Latency for querying the group memberships of an account.
+* `ldap/group_search_latency`: Latency for querying the group memberships of an
+  account.
 * `ldap/group_expansion_latency`: Latency for expanding nested groups.
 
 ==== REST API
 
 * `http/server/error_count`: Rate of REST API error responses.
+** `status`:
+   HTTP status code
 * `http/server/success_count`: Rate of REST API success responses.
+** `status`:
+   HTTP status code
 * `http/server/rest_api/count`: Rate of REST API calls by view.
+** `view`:
+   view implementation class
 * `http/server/rest_api/change_id_type`: Rate of REST API calls by change ID type.
+** `change_id_type`:
+   The type of the change identifier.
 * `http/server/rest_api/error_count`: Rate of REST API calls by view.
+** `view`:
+   view implementation class
+** `error_code`:
+   HTTP status code
+** `cause`:
+   The cause of the error.
 * `http/server/rest_api/server_latency`: REST API call latency by view.
+** `view`:
+   view implementation class
 * `http/server/rest_api/response_bytes`: Size of REST API response on network
-(may be gzip compressed) by view.
+  (may be gzip compressed) by view.
+** `view`:
+   view implementation class
 * `http/server/rest_api/change_json/to_change_info_latency`: Latency for
-toChangeInfo invocations in ChangeJson.
+  toChangeInfo invocations in ChangeJson.
 * `http/server/rest_api/change_json/to_change_infos_latency`: Latency for
-toChangeInfos invocations in ChangeJson.
+  toChangeInfos invocations in ChangeJson.
 * `http/server/rest_api/change_json/format_query_results_latency`: Latency for
-formatQueryResults invocations in ChangeJson.
-* `http/server/rest_api/ui_actions/latency`: Latency for RestView#getDescription calls.
+  formatQueryResults invocations in ChangeJson.
+* `http/server/rest_api/ui_actions/latency`: Latency for RestView#getDescription
+  calls.
+** `view`:
+   view implementation class
 
 === Query
 
 * `query/query_latency`: Successful query latency, accumulated over the life
-of the process.
+  of the process.
+** `index`: index name
 
 === Core Queues
 
@@ -159,11 +326,15 @@
 Each queue provides the following metrics:
 
 * `queue/<queue_name>/pool_size`: Current number of threads in the pool
-* `queue/<queue_name>/max_pool_size`: Maximum allowed number of threads in the pool
-* `queue/<queue_name>/active_threads`: Number of threads that are actively executing tasks
+* `queue/<queue_name>/max_pool_size`: Maximum allowed number of threads in the
+  pool
+* `queue/<queue_name>/active_threads`: Number of threads that are actively
+  executing tasks
 * `queue/<queue_name>/scheduled_tasks`: Number of scheduled tasks in the queue
-* `queue/<queue_name>/total_scheduled_tasks_count`: Total number of tasks that have been scheduled
-* `queue/<queue_name>/total_completed_tasks_count`: Total number of tasks that have completed execution
+* `queue/<queue_name>/total_scheduled_tasks_count`: Total number of tasks that
+  have been scheduled
+* `queue/<queue_name>/total_completed_tasks_count`: Total number of tasks that
+  have completed execution
 
 === SSH sessions
 
@@ -175,7 +346,7 @@
 
 * `topic/cross_project_submit`: number of cross-project topic submissions.
 * `topic/cross_project_submit_completed`: number of cross-project
-topic submissions that concluded successfully.
+  topic submissions that concluded successfully.
 
 === JGit
 
@@ -192,23 +363,34 @@
 * `load_success_count` : Successful cache loads for JGit block cache.
 * `miss_count` : Cache misses for JGit block cache.
 * `miss_ratio` : Cache miss ratio for JGit block cache.
-* `cache_used_per_repository` : Bytes of memory retained per repository for the top N repositories
-having most data in the cache. The number N of reported repositories is limited to 1000.
+* `cache_used_per_repository` : Bytes of memory retained per repository for the
+  top N repositories having most data in the cache. The number N of reported
+  repositories is limited to 1000.
+** `repository_name`: The name of the repository.
 
 === Git
 
 * `git/upload-pack/request_count`: Total number of git-upload-pack requests.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/upload-pack/phase_counting`: Time spent in the 'Counting...' phase.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/upload-pack/phase_compressing`: Time spent in the 'Compressing...' phase.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/upload-pack/phase_writing`: Time spent transferring bytes to client.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/upload-pack/pack_bytes`: Distribution of sizes of packs sent to clients.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/auto-merge/num_operations`: Number of auto merge operations and context.
+** `operation`:
+   The type of the operation (CACHE_LOAD, IN_MEMORY_WRITE, ON_DISK_WRITE).
 * `git/auto-merge/latency`: Latency of auto merge operations and context.
-
-=== BatchUpdate
-
-* `batch_update/execute_change_ops`: BatchUpdate change update latency,
-excluding reindexing
+** `operation`:
+   The type of the operation (CACHE_LOAD, IN_MEMORY_WRITE, ON_DISK_WRITE).
 
 === NoteDb
 
@@ -218,43 +400,63 @@
 * `notedb/parse_latency`: NoteDb parse latency for changes.
 * `notedb/external_id_cache_load_count`: Total number of times the external ID
   cache loader was called.
-* `notedb/external_id_partial_read_latency`: Latency for generating a new external ID
-  cache state from a prior state.
+** `partial`:
+   Whether the reload was partial.
+* `notedb/external_id_partial_read_latency`: Latency for generating a new
+  external ID cache state from a prior state.
 * `notedb/external_id_update_count`: Total number of external ID updates.
 * `notedb/read_all_external_ids_latency`: Latency for reading all
-external ID's from NoteDb.
+  external ID's from NoteDb.
 * `notedb/read_single_account_config_latency`: Latency for reading a single
-account config from NoteDb.
+  account config from NoteDb.
 * `notedb/read_single_external_id_latency`: Latency for reading a single
-external ID from NoteDb.
+  external ID from NoteDb.
 
 === Permissions
 
-* `permissions/permission_collection/filter_latency`: Latency to filter access sections
-by user and ref.
+* `permissions/permission_collection/filter_latency`: Latency for access filter
+  computations in PermissionCollection
 * `permissions/ref_filter/full_filter_count`: Rate of full ref filter operations
-* `permissions/ref_filter/skip_filter_count`: Rate of ref filter operations where
-we skip full evaluation because the user can read all refs
+* `permissions/ref_filter/skip_filter_count`: Rate of ref filter operations
+  where we skip full evaluation because the user can read all refs
 
 === Reviewer Suggestion
 
 * `reviewer_suggestion/query_accounts`: Latency for querying accounts for
-reviewer suggestion.
+  reviewer suggestion.
 * `reviewer_suggestion/recommend_accounts`: Latency for recommending accounts
-for reviewer suggestion.
+  for reviewer suggestion.
 * `reviewer_suggestion/load_accounts`: Latency for loading accounts for
-reviewer suggestion.
+  reviewer suggestion.
 * `reviewer_suggestion/query_groups`: Latency for querying groups for reviewer
-suggestion.
+  suggestion.
+* `reviewer_suggestion/filter_visibility`: Latency for removing users that can't
+  see the change
 
 === Repo Sequences
 
 * `sequence/next_id_latency`: Latency of requesting IDs from repo sequences.
+** `sequence`:
+   The sequence from which IDs were retrieved.
+** `multiple`:
+   Whether more than one ID was retrieved.
 
 === Plugin
 
 * `plugin/latency`: Latency for plugin invocation.
+** `plugin_name`"
+   The name of the plugin.
+** `class`:
+   The class of the plugin that was invoked.
+** `export_value`:
+   The export name under which the invoked class is registered.
 * `plugin/error_count`: Number of plugin errors.
+** `plugin_name`"
+   The name of the plugin.
+** `class`:
+   The class of the plugin that was invoked.
+** `export_value`:
+   The export name under which the invoked class is registered.
 
 === Group
 
@@ -263,11 +465,19 @@
 === Replication Plugin
 
 * `plugins/replication/replication_latency`: Time spent pushing to remote
-destination.
+  destination.
+** `destination`: The destination of the replication.
 * `plugins/replication/replication_delay`: Time spent waiting before pushing to
-remote destination.
+  remote destination.
+** `destination`: The destination of the replication.
 * `plugins/replication/replication_retries`: Number of retries when pushing to
-remote destination.
+  remote destination.
+** `destination`: The destination of the replication.
+* `plugins/replication/latency_slower_than_threshold`: latency for project to
+  destination, where latency was slower than threshold
+** `slow_threshold`: The threshold.
+** `project`: The name of the project.
+** `destination`: The destination of the replication.
 
 === License
 
diff --git a/Documentation/pgm-ChangeExternalIdCaseSensitivity.txt b/Documentation/pgm-ChangeExternalIdCaseSensitivity.txt
new file mode 100644
index 0000000..1fb4b97
--- /dev/null
+++ b/Documentation/pgm-ChangeExternalIdCaseSensitivity.txt
@@ -0,0 +1,71 @@
+= ChangeExternalIdCaseSensitivity
+
+== NAME
+ChangeExternalIdCaseSensitivity - Convert `username` and `gerrit`
+external IDs to be handled case insensitively
+
+== SYNOPSIS
+[verse]
+--
+_java_ -jar gerrit.war _ChangeExternalIdCaseSensitivity_
+  -d <SITE_PATH>
+  [--batch]
+  [--dryrun]
+--
+
+== DESCRIPTION
+Convert `username` and `gerrit` external IDs to be handled case
+insensitively or case sensitively. This is done by recomputing
+the name of the note from the sha1 sum of the all lowercase
+external ID key or of the key with its original capitalization
+respectively.
+
+The tool uses the `auth.userNameCaseInsensitive` option to determine,
+whether the migration should be performed to case insensitive or case sensitive
+usernames, i.e. if the option is set to `false`, migration will be performed to
+make external IDs case insensitive and if set to `true` to case sensitive.
+
+== OPTIONS
+
+-d::
+--site-path::
+	Path of the Gerrit site
+
+--batch::
+    No user interaction is required. The tool won't ask for confirmation before migrating.
+
+--dryrun::
+    Whether to perform the conversion without persisting it.
+
+== CONTEXT
+This command can only be run offline with direct access to the server's
+site.
+
+== EXAMPLES
+To convert the external IDs to be case insensitive:
+
+----
+    $ git config -f $SITE/etc/gerrit.config --get auth.userNameCaseInsensitive
+    > false
+    $ java -jar gerrit.war ChangeExternalIdCaseSensitivity -d site_path
+----
+
+To convert the external IDs to be case sensitive again:
+
+----
+    $ git config -f $SITE/etc/gerrit.config --get auth.userNameCaseInsensitive
+    > true
+    $ java -jar gerrit.war ChangeExternalIdCaseSensitivity -d site_path
+----
+
+
+== SEE ALSO
+
+* Configuration parameter link:config-gerrit.html#auth.userNameCaseInsensitive[auth.userNameCaseInsensitive]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index dde0231..8f4cbda 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -38,6 +38,9 @@
 link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase]::
 	Convert the local username of every account to lower case.
 
+link:pgm-ChangeExternalIdCaseSensitivity.html[ChangeExternalIdCaseSensitivity]::
+    Convert external IDs to be case insensitive.
+
 link:pgm-MigrateAccountPatchReviewDb.html[MigrateAccountPatchReviewDb]::
 	Migrates AccountPatchReviewDb from one database backend to another.
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 0caebfc..ae0c0a6 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1343,6 +1343,7 @@
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "disable_keyboard_shortcuts": true,
+    "disable_token_highlighting": true,
     "diff_view": "SIDE_BY_SIDE",
     "mute_common_path_prefixes": true,
     "my": [
@@ -1394,6 +1395,7 @@
     "diff_view": "SIDE_BY_SIDE",
     "publish_comments_on_push": true,
     "disable_keyboard_shortcuts": true,
+    "disable_token_highlighting": true,
     "work_in_progress_by_default": true,
     "mute_common_path_prefixes": true,
     "my": [
@@ -1779,7 +1781,16 @@
 
 Only external ids belonging to the caller may be deleted. Users that have
 link:access-control.html#capability_modifyAccount[Modify Account] can delete
-external ids that belong to other accounts.
+external ids that belong to other accounts. External ids in the 'username:'
+scheme can only be deleted by users that have
+link:access-control.html#capability_administrateServer[Administrate Server]
+or both
+link:access-control.html#capability_maintainServer[Maintain Server] and
+link:access-control.html#capability__modifyAccount[Modify Account]
+since the user may not be able to login anymore, after the removal of the
+external id with scheme 'username:'. Users cannot delete their own external id
+with scheme 'username:' in order to prevent they can lock themselves out
+since they may not be able to login anymore.
 
 .Request
 ----
@@ -2001,7 +2012,7 @@
 --
 
 Star a change with the default label. Changes starred with the default
-label are returned for the search query `is:starred` or `starredby:USER`
+label are returned for the search query `is:starred` or `has:star`
 and automatically notify the user whenever updates are made to the
 change.
 
@@ -2033,131 +2044,6 @@
   HTTP/1.1 204 No Content
 ----
 
-[[star-endpoints]]
-== Star Endpoints
-
-[[get-starred-changes]]
-=== Get Starred Changes
---
-'GET /accounts/link:#account-id[\{account-id\}]/stars.changes'
---
-
-Gets the changes that were starred with any label by the identified
-user account. This URL endpoint is functionally identical to the
-changes query `GET /changes/?q=has:stars`. The result is a list of
-link:rest-api-changes.html#change-info[ChangeInfo] entities.
-
-.Request
-----
-  GET /a/accounts/self/stars.changes
-----
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    {
-      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
-      "project": "myProject",
-      "branch": "master",
-      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
-      "subject": "Implementing Feature X",
-      "status": "NEW",
-      "created": "2013-02-01 09:59:32.126000000",
-      "updated": "2013-02-21 11:16:36.775000000",
-      "stars": [
-        "ignore",
-        "risky"
-      ],
-      "mergeable": true,
-      "submittable": false,
-      "insertions": 145,
-      "deletions": 12,
-      "_number": 3965,
-      "owner": {
-        "name": "John Doe"
-      }
-    }
-  ]
-----
-
-[[get-stars]]
-=== Get Star Labels From Change
---
-'GET /accounts/link:#account-id[\{account-id\}]/stars.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
---
-
-Get star labels from a change.
-
-.Request
-----
-  GET /a/accounts/self/stars.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
-----
-
-As response the star labels that the user applied on the change are
-returned. The labels are lexicographically sorted.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    "blue",
-    "green",
-    "red"
-  ]
-----
-
-[[set-stars]]
-=== Update Star Labels On Change
---
-'POST /accounts/link:#account-id[\{account-id\}]/stars.changes/link:rest-api-changes.html#change-id[\{change-id\}]'
---
-
-Update star labels on a change. The star labels to be added/removed
-must be specified in the request body as link:#stars-input[StarsInput]
-entity. Starred changes are returned for the search query `has:stars`.
-
-.Request
-----
-  POST /a/accounts/self/stars.changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "add": [
-      "blue",
-      "red"
-    ],
-    "remove": [
-      "yellow"
-    ]
-  }
-----
-
-As response the star labels that the user applied on the change are
-returned. The labels are lexicographically sorted.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  [
-    "blue",
-    "green",
-    "red"
-  ]
-----
-
 [[ids]]
 == IDs
 
@@ -2820,6 +2706,8 @@
 Allowed values are `AUTO_MERGE` and `FIRST_PARENT`.
 |`disable_keyboard_shortcuts`     |not set if `false`|
 Whether to disable all keyboard shortcuts.
+|`disable_token_highlighting`     [not set if `false`]
+Whether to disable token highlighting on hover.
 |`publish_comments_on_push`     |not set if `false`|
 Whether to link:user-upload.html#publish-comments[publish draft comments] on
 push by default.
@@ -2917,18 +2805,6 @@
 |`valid`         ||Whether the SSH key is valid.
 |=============================
 
-[[stars-input]]
-=== StarsInput
-The `StarsInput` entity contains star labels that should be added to
-or removed from a change.
-
-[options="header",cols="1,^1,5"]
-|========================
-|Field Name ||Description
-|`add`      |optional|List of labels to add to the change.
-|`remove`   |optional|List of labels to remove from the change.
-|========================
-
 [[username-input]]
 === UsernameInput
 The `UsernameInput` entity contains information for setting the
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index b376a74..d83ef0e 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -600,8 +600,9 @@
 ----
 
 As a response, two link:#change-info[ChangeInfo] entities are returned
-that describe information added and removed from the `old` change state.
-Only fields that differ between the change's two states are returned.
+that describe information added and removed from the `old` change state, and
+the two link:#change-info[ChangeInfo] entities that generated the diff are
+returned. Only fields that differ between the change's two states are returned.
 
 .Response
 ----
@@ -625,9 +626,55 @@
       "topic": "new-topic"
     },
     "removed": {
-      "updated": "2013-02-20 12:05:34.111000000",
+      "updated": "2013-02-01 09:59:32.126000000",
       "topic": "old-topic"
-    }
+    },
+    "old_change_info": {
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "attention_set": [],
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "topic": "old-topic",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-01 09:59:32.126000000",
+      "mergeable": true,
+      "insertions": 34,
+      "deletions": 101,
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    },
+    "new_change_info": {
+      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "project": "myProject",
+      "branch": "master",
+      "attention_set": [
+        {
+          "account": {
+            "name": "John Doe"
+          },
+         "last_update": "2013-02-21 11:16:36.775000000",
+         "reason": "reviewer or cc replied"
+        }
+      ],
+      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+      "subject": "Implementing Feature X",
+      "status": "NEW",
+      "topic": "new-topic",
+      "created": "2013-02-01 09:59:32.126000000",
+      "updated": "2013-02-21 11:16:36.775000000",
+      "mergeable": true,
+      "insertions": 34,
+      "deletions": 101,
+      "_number": 3965,
+      "owner": {
+        "name": "John Doe"
+      }
+    },
   }
 ----
 
@@ -2561,42 +2608,6 @@
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0
 ----
 
-[[mark-as-reviewed]]
-=== Mark as Reviewed
---
-'PUT /changes/link:#change-id[\{change-id\}]/reviewed'
---
-
-Marks a change as reviewed.
-
-This allows users to "de-highlight" changes in their dashboard until a new
-patch set is uploaded.
-
-This differs from the link:#ignore[ignore] endpoint, which will mute
-emails and hide the change from dashboard completely until it is
-link:#unignore[unignored] again.
-
-
-.Request
-----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewed HTTP/1.0
-----
-
-[[mark-as-unreviewed]]
-=== Mark as Unreviewed
---
-'PUT /changes/link:#change-id[\{change-id\}]/unreviewed'
---
-
-Marks a change as unreviewed.
-
-This allows users to "highlight" changes in their dashboard
-
-.Request
-----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unreviewed HTTP/1.0
-----
-
 [[get-hashtags]]
 === Get Hashtags
 --
@@ -3264,6 +3275,8 @@
 * are visible to the calling user
 * are not already reviewer on the change
 * don't own the change
+* are not service users (unless
+  link:config.html#suggest.skipServiceUsers[skipServiceUsers] is set to `false`)
 
 Groups can be excluded from the results by specifying the 'exclude-groups'
 request parameter:
@@ -6512,7 +6525,9 @@
 |`unresolved_comment_count`  |optional|
 Number of unresolved inline comment threads across all patch sets. Not set if
 the current change index doesn't have the data.
-|`_number`            ||The legacy numeric ID of the change.
+|`_number`            ||
+The numeric ID of the change. (The underscore is just a relict of a prior
+attempt to deprecate the numeric ID.)
 |`owner`              ||
 The owner of the change as an link:rest-api-accounts.html#account-info[
 AccountInfo] entity.
@@ -6520,6 +6535,9 @@
 Actions the caller might be able to perform on this revision. The
 information is a map of view name to link:#action-info[ActionInfo]
 entities.
+|`submit_records`             ||
+List of the link:rest-api-changes.html#submit-record-info[SubmitRecordInfo]
+containing the submit records for the change at the latest patchset.
 |`requirements`             |optional|
 List of the link:rest-api-changes.html#requirement[requirements] to be met before this change
 can be submitted. This field is deprecated in favour of `submit_requirements`.
@@ -8161,6 +8179,37 @@
 the failure of the rule predicate.
 |===========================
 
+[[submit-record-info]]
+=== SubmitRecordInfo
+The `SubmitRecordInfo` entity describes results from a submit_rule.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name      ||Description
+|`rule_name`||
+The name of the submit rule that created this submit record. The submit rule is
+specified in the form of "$plugin~$rule" where `$plugin` is the plugin name
+and `$rule` is the name of the class that implemented the submit rule.
+|`status`||
+`OK`, the change can be submitted. +
+`NOT_READY`, additional labels are required before submit. +
+`CLOSED`, closed changes cannot be submitted. +
+`FORCED`, the change was submitted bypassing the submit rule. +
+`RULE_ERROR`, rule code failed with an error.
+|`labels`|optional|
+A list of labels, each containing the following fields. +
+  * `label`: the label name. +
+  * `status`: the label status: {`OK`, `REJECT`, `MAY`, `NEED`, `IMPOSSIBLE`}. +
+  * `appliedBy`: the link:rest-api-accounts.html#account-info[AccountInfo]
+  that applied the vote to the label.
+|`requirements`|optional|
+List of the link:rest-api-changes.html#requirement[requirements] to be met
+before this change can be submitted.
+|`error_message`|optional|
+When status is RULE_ERROR this message provides some text describing
+the failure of the rule predicate.
+|===========================
+
 [[submit-requirement-expression-info]]
 === SubmitRequirementExpressionInfo
 The `SubmitRequirementExpressionInfo` describes the result of evaluating a
@@ -8198,6 +8247,10 @@
 |`status`||
 Status describing the result of evaluating the submit requirement. The status
 is one of (`SATISFIED`, `UNSATISFIED`, `OVERRIDDEN`, `NOT_APPLICABLE`).
+|`is_legacy`||
+If true, this submit requirement result was created from a legacy
+link:#submit-record[SubmitRecord]. Otherwise, it was created by evaluating a
+submit requirement.
 |`applicability_expression_result`|optional|
 A link:#submit-requirement-expression-info[SubmitRequirementExpressionInfo]
 containing the result of evaluating the applicability expression. Not set if the
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index eb38434..92c6dbf 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2561,6 +2561,9 @@
 Retrieves the branches and tags in which a change is included. As result
 an link:rest-api-changes.html#included-in-info[IncludedInInfo] entity is returned.
 
+Branches that are not visible to the calling user according to the project's
+read permissions are filtered out from the result.
+
 .Request
 ----
   GET /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/in HTTP/1.0
diff --git a/Documentation/user-request-cancellation-and-deadlines.txt b/Documentation/user-request-cancellation-and-deadlines.txt
new file mode 100644
index 0000000..b368d6a
--- /dev/null
+++ b/Documentation/user-request-cancellation-and-deadlines.txt
@@ -0,0 +1,184 @@
+:linkattrs:
+= Request Cancellation and Deadlines
+
+[[motivation]]
+== Motivation
+
+Protect the Gerrit service by aborting requests that were cancelled or for which
+the deadline has exceeded. If these requests are not aborted, it can happen that
+too many of these requests are accumulated so that the server runs out of
+resources (e.g. threads).
+
+[[request-cancellation]]
+== Request Cancellation
+
+If a user cancels a request by disconnecting, ideally Gerrit should detect this
+and abort the request execution to avoid doing unnecessary work. If nobody is
+waiting for the response, Gerrit shouldn't spend resources to compute it.
+
+Detecting cancelled requests is not easily possible with all protocols that a
+client may use. At the moment Gerrit only detects request cancellations for git
+pushes, but not for other request types (in particular cancelled requests are
+not detected for REST calls over HTTP, SSH commands and git clone/fetch).
+
+[[server-side-deadlines]]
+== Server-side deadlines
+
+To limit the maximal execution time for requests, administrators can [configure
+server-side deadlines](config-gerrit.html#deadline.id). If a server-side
+deadline is exceeded by a matching request, the request is automatically
+aborted. In this case the client gets a proper error message informing the user
+about the exceeded deadline.
+
+Clients may override server-side deadlines by setting a
+[deadline](#client-provided-deadline) on the request. This means, if a request
+fails due to an exceeded server-side deadline, the client may repeat the request
+with a higher deadline or no deadline (deadline = 0) to get unblocked.
+
+Server-side deadlines are meant to protect the Gerrit service against resource
+exhaustion due to performence issues with a particular request. E.g. imagine a
+situation where requests for a certain REST endpoint are very slow. If more and
+more of such requests get stuck and are not being aborted, the Gerrit service
+may run out of threads, causing an outage for the entire Gerrit service.
+Server-side deadlines may prevent this because the slow requests get aborted
+after the deadline is exceeded, and hence the server resources are freed up.
+
+In some cases server-side deadlines may also lead to a better user experience,
+as it's better to tell the user that there is a performance issue, that prevents
+the execution of the request, than letting them wait indefinitely.
+
+Finally server-side deadlines can help ops engineers to detect performance
+issues more reliably and more quicky. For this alerts may be setup that are
+based on the [cancellation metrics](metrics.html#cancellations).
+
+[[receive-timeout]]
+=== Receive Timeout
+
+For git pushes it is possible to configure a [hard
+timeout](config-gerrit.html#receive.timeout). In contrast to server-side
+deadlines, this timeout is not overridable by [client-provided
+deadlines](#client-provided-deadlines).
+
+[[client-provided-deadlines]]
+== Client-provided deadlines
+
+Clients can set a deadline on requests to limit the maximal execution time that
+they are willing to wait for a response. If the request doesn't finish within
+this deadline the request is aborted and the client receives an error, with a
+message telling them that the deadline has been exceeded.
+
+How to set a deadline on a request depends on the request type:
+
+[options="header",cols="1,6"]
+|=======================
+|Request Type   |How to set a deadline?
+|REST over HTTP |Set the [X-Gerrit-Deadline header](rest-api.html#deadline).
+|SSH command    |Set the [deadline option](cmd-index.html#deadline).
+|git push       |Set the [deadline push option](user-upload.html#deadline).
+|git clone/fetch|Not supported.
+|=======================
+
+[[override-server-side-deadline]]
+=== Override server-side deadline
+
+By setting a deadline on a request it is possible to override any [server-side
+deadline](#server-side-deadline), e.g. in order to increase it. Setting the
+deadline to `0` disables any server-side deadline. This allows clients to get
+unblocked if a request has previously failed due to an exceeded deadline.
+
+[NOTE]
+It is stronly discouraged for clients to permanently override [server-side
+deadlines](#server-side-deadlines] with a higher deadline or to permanently
+disable them by always setting the deadline to `0`. If this becomes necessary
+the caller should get in touch with the Gerrit administrators to increase the
+server-side deadlines or resolve the performance issue in another way.
+
+[NOTE]
+It's not possible for clients to override the [receive
+timeout](#receive-timeout) that is enforced on git push.
+
+[[faqs]]
+== FAQs
+
+[[deadline-exceeded-what-to-do]]
+=== My request failed due to an execeeded deadline, what can I do?
+
+To get unblocked, you may repeat the request with deadlines disabled. To do this
+set the deadline to `0` on the request as explained
+[above](#override-server-side-deadline).
+
+If doing this becomes required frequently, please get in touch with the Gerrit
+administrators in order to investigate the performance issue and increase the
+server-side deadline if necessary.
+
+[NOTE]
+Setting deadlines for requests that are done from the Gerrit web UI is not
+possible. If exceeded deadlines occur frequently here, please get in touch with
+the Gerrit administrators in order to investigate the performance issue.
+
+[[push-fails-due-to-exceeded-deadline-but-cannot-be-overridden]]
+=== My git push fails due to an exceeded deadline and I cannot override the deadline, what can I do?
+
+As explained [above](#receive-timeout) a configured receive timeout cannot be
+overridden by clients. If pushes fail due to this timeout, get in touch with the
+Gerrit administrators in order to investigate the performance issue and increase
+the receive timeout if necessary.
+
+[[when-are-requests-aborted]]
+=== How quickly does a request get aborted when it is cancelled or a deadline is exceeded?
+
+In order to know if a request should be aborted, Gerrit needs to explicitly
+check whether the request is cancelled or whether a deadline is exceeded.
+Gerrit does this check at the beginning and end of all performance critical
+steps and sub-steps. This means, the request is only aborted the next time such
+a step starts or finishes, which can also be never (e.g. if the request is stuck
+inside of a step).
+
+[NOTE]
+Technically the check whether a request should be aborted is done whenever the
+execution time of an operation or sub-step is captured, either by a timer
+metric or a `TraceTimer` ('TraceTimer` is the class that logs the execution time
+when the request is being [traced](user-request-tracing.html)).
+
+[[how-are-requests-aborted]]
+=== How does Gerrit abort requests?
+
+The exact response that is returned to the client depends on the request type
+and the cancellation reason:
+
+[options="header",cols="1,3,3"]
+|=======================
+|Request Type   |Cancellation Reason|Response
+|REST over HTTP |Client Disconnected|The response is '499 Client Closed Request'.
+|               |Server-side deadline exceeded|The response is '408 Server Deadline Exceeded'.
+|               |Client-provided deadline exceeded|The response is '408 Client Provided Deadline Exceeded'.
+|SSH command    |Client Disconnected|The error message is 'Client Closed Request'.
+|               |Server-side deadline exceeded|The error message is 'Server Deadline Exceeded'.
+|               |Client-provided deadline exceeded|The error message is 'Client Provided Deadline Exceeded'.
+|git push       |Client Disconnected|The error status is 'Client Closed Request'.
+|               |Server-side deadline exceeded|The error status is 'Server Deadline Exceeded'.
+|               |Client-provided deadline exceeded|The error status is 'Client Provided Deadline Exceeded'.
+|git clone/fetch|Not supported.
+|=======================
+
+This means clients always get a proper error message telling the user why the
+request has been aborted.
+
+Errors due to aborted requests are usually not counted as internal server errors,
+but the [cancellation metrics](metrics.html#cancellations) may be used to setup
+alerting for performance issues.
+
+[NOTE]
+During a request, cancellations can occur at any time. This means for non-atomic
+operations, it can happen that the operation is cancelled after some steps have
+already been successfully performed and before all steps have been executed,
+potentially leaving behind an inconsistent state (same as when a request fails
+due to an error). However for important steps, such a NoteDb updates that span
+multiple repositories, Gerrit ensures that they are not torn by cancellations.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index e684b85..303242df 100644
--- a/Documentation/user-request-tracing.txt
+++ b/Documentation/user-request-tracing.txt
@@ -148,3 +148,10 @@
 * permission checks (e.g. which rule is responsible for a deny)
 * timer metrics
 * all other logs
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index a9779b1..3977278 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -321,7 +321,7 @@
 +
 Matches any change touching file at 'PATH'. By default exact path
 matching is used, but regular expressions can be enabled by starting
-with `^`.  For example, to match all XML files use `file:^.*\.xml$`.
+with `^`.  For example, to match all XML files use `file:"^.*\.xml$"`.
 The link:http://www.brics.dk/automaton/[dk.brics.automaton library,role=external,window=_blank]
 is used for the evaluation of such patterns.
 +
@@ -331,15 +331,15 @@
 files, use `file:^.*\.java`.
 +
 The entire regular expression pattern, including the `^` character,
-should be double quoted when using more complex construction (like
-ones using a bracket expression). For example, to match all XML
+should be double quoted. For example, to match all XML
 files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
 `file:"^name[1-3].xml"`.
 +
 Slash ('/') is used path separator.
 +
-More examples:
-* `-file:^path/.*` - changes that do not modify files from `path/`,
+*More examples:*
+
+* `-file:^path/.*` - changes that do not modify files from `path/`.
 * `file:{^~(path/.*)}` - changes that modify files not from `path/` (but may
 contain files from `path/`).
 
@@ -406,6 +406,8 @@
 +
 'star:star' is the same as 'has:star' and 'is:starred'.
 
+Only "ignore" and "star" are supported labels.
+
 [[has]]
 has:draft::
 +
@@ -417,11 +419,6 @@
 Same as 'is:starred' and 'star:star', true if the change has been
 starred by the current user with the default label.
 
-[[has-stars]]
-has:stars::
-+
-True if the change has been starred by the current user with any label.
-
 has:edit::
 +
 True if the change has inline edit created by the current user.
@@ -430,6 +427,11 @@
 +
 True if the change has unresolved comments.
 
+has:attention::
++
+True if the change has attention by the current user.
+
+
 [[is]]
 is:assigned::
 +
@@ -445,6 +447,10 @@
 +
 True if the change does not have an assignee.
 
+is:attention::
++
+True if the change has attention by the current user.
+
 is:watched::
 +
 True if this change matches one of the current user's watch filters,
@@ -530,6 +536,16 @@
 +
 True if the change is a merge commit.
 
+[[cherrypick]]
+is:cherrypick::
++
+True if the change is a cherrypick of an another change.
+
+This is limited to changes which are cherrypicked using REST API
+or WebUI only. It is not able to identify changes which are
+cherry-picked locally using the git cherry-pick command and then
+pushed to Gerrit.
+
 [[status]]
 status:open, status:pending, status:new::
 +
@@ -588,14 +604,17 @@
 author:'AUTHOR'::
 +
 Changes where 'AUTHOR' is the author of the current patch set. 'AUTHOR' may be
-the author's exact email address, or part of the name or email address.
+the author's exact email address, or part of the name or email address. The
+special case of `author:self` will find changes authored by the caller.
 
 [[committer]]
 committer:'COMMITTER'::
 +
 Changes where 'COMMITTER' is the committer of the current patch set.
 'COMMITTER' may be the committer's exact email address, or part of the name or
-email address.
+email address. The special case of `committer:self` will find changes committed
+by the caller.
+
 
 [[submittable]]
 submittable:'SUBMIT_STATUS'::
@@ -606,6 +625,22 @@
 only applies to the top-level status; individual label statuses can be
 searched link:#labels[by label].
 
+[[rule]]
+rule:'SUBMIT_RULE_NAME'::
++
+Changes where 'SUBMIT_RULE_NAME' returns a submit record with status in {OK,
+FORCED}. This means that the submit rule has passed and is not blocking the
+change submission. 'SUBMIT_RULE_NAME' should be in the form of
+'$plugin_name~$rule_name'. For gerrit core rules, 'SUBMIT_RULE_NAME' should
+be in the form of '$rule_name' (example: `DefaultSubmitRule`), or
+'gerrit~$rule_name' (example: `gerrit~DefaultSubmitRule`).
+
+rule:'SUBMIT_RULE_NAME'='STATUS'::
++
+Changes where 'SUBMIT_RULE_NAME' returns a submit record with status equals to
+'STATUS'. The status can be any of the statuses that are documented for the
+`status` field of link:rest-api-changes.html#submit-record[SubmitRecord].
+
 [[unresolved]]
 unresolved:'RELATION''NUMBER'::
 +
@@ -702,6 +737,10 @@
 to one of the fields in the
 link:rest-api-changes.html#submit-record[SubmitRecord] REST API entity.
 
+`label:Code-Review\<=-1`::
++
+Matches changes with either a -1, -2, or any lower score.
+
 `label:Code-Review=MAX`::
 +
 Matches changes with label voted with the highest possible score.
@@ -738,15 +777,20 @@
 The special "owner" parameter corresponds to the change owner.  Matches
 all changes that have a +2 vote from the change owner.
 
+`label:Code-Review=+2,user=non_uploader`::
+`label:Code-Review=ok,user=non_uploader`::
+`label:Code-Review=+2,non_uploader`::
+`label:Code-Review=ok,non_uploader`::
++
+The special "non_uploader" parameter corresponds to any user who's not the
+uploader of the latest patchset. Matches all changes that have a +2 vote from
+a non upoader.
+
 `label:Code-Review=+1,group=ldap/linux.workflow`::
 +
 Matches changes with a +1 code review where the reviewer is in the
 ldap/linux.workflow group.
 
-`label:Code-Review\<=-1`::
-+
-Matches changes with either a -1, -2, or any lower score.
-
 `is:open label:Code-Review+2 label:Verified+1 NOT label:Verified-1 NOT label:Code-Review-2`::
 `is:open label:Code-Review=ok label:Verified=ok`::
 +
@@ -785,24 +829,6 @@
 Magical internal flag to prove the current user has access to read
 the change.  This flag is always added to any query.
 
-starredby:'USER'::
-+
-Matches changes that have been starred by 'USER' with the default label.
-The special case `starredby:self` applies to the caller.
-
-watchedby:'USER'::
-+
-Matches changes that 'USER' has configured watch filters for.
-The special case `watchedby:self` applies to the caller.
-
-draftby:'USER'::
-+
-Matches changes that 'USER' has left unpublished draft comments on.
-Since the drafts are unpublished, it is not possible to see the
-draft text, or even how many drafts there are. The special case
-of `draftby:self` will find changes where the caller has created
-a draft comment.
-
 [[limit]]
 limit:'CNT'::
 +
diff --git a/WORKSPACE b/WORKSPACE
index 5c38224..93cae7d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -922,7 +922,6 @@
 
 yarn_install(
     name = "npm",
-    data = ["//:twinkie.patch"],
     frozen_lockfile = False,
     package_json = "//:package.json",
     package_path = "",
diff --git a/contrib/ui-api-proxy.go b/contrib/ui-api-proxy.go
deleted file mode 100644
index 1ae1c1a..0000000
--- a/contrib/ui-api-proxy.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// ui-api-proxy is a reverse http proxy that allows the UI to be served from
-// a different host than the API. This allows testing new UI features served
-// from localhost but using live production data.
-//
-// Run the binary, download & install the Go tools available at
-// http://golang.org/doc/install . To run, execute `go run ui-api-proxy.go`.
-// For a description of the available flags, execute
-// `go run ui-api-proxy.go --help`.
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"net"
-	"net/http"
-	"net/http/httputil"
-	"net/url"
-	"strings"
-	"time"
-)
-
-var (
-	ui   = flag.String("ui", "http://localhost:8080", "host to which ui requests will be forwarded")
-	api  = flag.String("api", "https://gerrit-review.googlesource.com", "host to which api requests will be forwarded")
-	port = flag.Int("port", 0, "port on which to run this server")
-)
-
-func main() {
-	flag.Parse()
-
-	uiURL, err := url.Parse(*ui)
-	if err != nil {
-		log.Fatalf("proxy: parsing ui addr %q failed: %v\n", *ui, err)
-	}
-	apiURL, err := url.Parse(*api)
-	if err != nil {
-		log.Fatalf("proxy: parsing api addr %q failed: %v\n", *api, err)
-	}
-
-	l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%v", *port))
-	if err != nil {
-		log.Fatalln("proxy: listen failed: ", err)
-	}
-	defer l.Close()
-	fmt.Printf("OK\nListening on http://%v/\n", l.Addr())
-
-	err = http.Serve(l, &httputil.ReverseProxy{
-		FlushInterval: 500 * time.Millisecond,
-		Director: func(r *http.Request) {
-			if strings.HasPrefix(r.URL.Path, "/changes/") || strings.HasPrefix(r.URL.Path, "/projects/") {
-				r.URL.Scheme, r.URL.Host = apiURL.Scheme, apiURL.Host
-			} else {
-				r.URL.Scheme, r.URL.Host = uiURL.Scheme, uiURL.Host
-			}
-			if r.URL.Scheme == "" {
-				r.URL.Scheme = "http"
-			}
-			r.Host, r.URL.Opaque, r.URL.RawQuery = r.URL.Host, r.RequestURI, ""
-		},
-	})
-	if err != nil {
-		log.Fatalln("proxy: serve failed: ", err)
-	}
-}
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index aa13339..c67991d 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.ServiceUserClassifier;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.group.db.GroupDelta;
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.notedb.Sequences;
@@ -52,18 +53,21 @@
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final GroupCache groupCache;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   AccountCreator(
       Sequences sequences,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       GroupCache groupCache,
-      @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
+      @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider,
+      ExternalIdFactory externalIdFactory) {
     accounts = new HashMap<>();
     this.sequences = sequences;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.groupCache = groupCache;
     this.groupsUpdateProvider = groupsUpdateProvider;
+    this.externalIdFactory = externalIdFactory;
   }
 
   public synchronized TestAccount create(
@@ -84,11 +88,11 @@
     String httpPass = null;
     if (username != null) {
       httpPass = "http-pass";
-      extIds.add(ExternalId.createUsername(username, id, httpPass));
+      extIds.add(externalIdFactory.createUsername(username, id, httpPass));
     }
 
     if (email != null) {
-      extIds.add(ExternalId.createEmail(id, email));
+      extIds.add(externalIdFactory.createEmail(id, email));
     }
 
     accountsUpdateProvider
diff --git a/java/com/google/gerrit/acceptance/FakeSubmitRule.java b/java/com/google/gerrit/acceptance/FakeSubmitRule.java
new file mode 100644
index 0000000..d53456f
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/FakeSubmitRule.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRecord.Status;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
+import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
+import java.util.Optional;
+
+/** Fake submit rule that returns OK if the change contains one or more hashtags. */
+@Singleton
+public class FakeSubmitRule implements SubmitRule {
+  public static class Module extends AbstractModule {
+    @Override
+    public void configure() {
+      bind(SubmitRule.class).annotatedWith(Exports.named("Fake")).to(FakeSubmitRule.class);
+    }
+  }
+
+  @Override
+  public Optional<SubmitRecord> evaluate(ChangeData cd) {
+    SubmitRecord record = new SubmitRecord();
+    record.status = cd.hashtags().isEmpty() ? Status.NOT_READY : Status.OK;
+    record.ruleName = FakeSubmitRule.class.getSimpleName();
+    return Optional.of(record);
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 9732d2c..4d87f4d 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -293,7 +293,6 @@
    * @param baseConfig default config values; merged with config from {@code desc} and then written
    *     into {@code site/etc/gerrit.config}.
    * @param site temp directory where site will live.
-   * @throws Exception
    */
   public static void init(Description desc, Config baseConfig, Path site) throws Exception {
     checkArgument(!desc.memory(), "can't initialize site path for in-memory test: %s", desc);
@@ -347,7 +346,6 @@
    * @param testSysModule additional Guice module to use.
    * @param testSshModule additional Guice module to use.
    * @return started server.
-   * @throws Exception
    */
   public static GerritServer initAndStart(
       TemporaryFolder temporaryFolder,
@@ -384,7 +382,6 @@
    * @param additionalArgs additional command-line arguments for the daemon program; only allowed if
    *     the test is not in-memory.
    * @return started server.
-   * @throws Exception
    */
   public static GerritServer start(
       Description desc,
diff --git a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index e4b0eea..373246a 100644
--- a/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.config.AllProjectsConfigProvider;
+import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.FileBasedGlobalPluginConfigProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GlobalPluginConfigProvider;
@@ -57,6 +59,7 @@
   protected void configure() {
     bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
     bind(GlobalPluginConfigProvider.class).to(FileBasedGlobalPluginConfigProvider.class);
+    bind(AllProjectsConfigProvider.class).to(FileBasedAllProjectsConfigProvider.class);
     bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
 
     if (repoManager != null) {
diff --git a/java/com/google/gerrit/acceptance/SshSessionMina.java b/java/com/google/gerrit/acceptance/SshSessionMina.java
index 4d8691b..3b0ba3b 100644
--- a/java/com/google/gerrit/acceptance/SshSessionMina.java
+++ b/java/com/google/gerrit/acceptance/SshSessionMina.java
@@ -107,13 +107,11 @@
   @Override
   public int execAndReturnStatus(String command) throws Exception {
     Process process = getMinaSession().exec(command, 0);
-    InputStream in = process.getInputStream();
     InputStream err = process.getErrorStream();
 
     Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
     error = s.hasNext() ? s.next() : null;
 
-    s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
     try {
       return process.exitValue();
     } catch (IllegalThreadStateException e) {
diff --git a/java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java b/java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java
index ae88e37..87063c9 100644
--- a/java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java
+++ b/java/com/google/gerrit/acceptance/config/GlobalPluginConfig.java
@@ -28,12 +28,12 @@
   /** Name of the plugin, corresponding to {@code $site/etc/@pluginName.config}. */
   String pluginName();
 
-  /** @see GerritConfig#name() */
+  /** See {@link GerritConfig#name()} */
   String name();
 
-  /** @see GerritConfig#value() */
+  /** See {@link GerritConfig#value()} */
   String value() default "";
 
-  /** @see GerritConfig#values() */
+  /** See {@link GerritConfig#values()} */
   String[] values() default "";
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
index 3763f9a..c6457a4 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AccountsUpdate.ConfigureDeltaFromState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -44,13 +45,18 @@
   private final Accounts accounts;
   private final AccountsUpdate accountsUpdate;
   private final Sequences seq;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   public AccountOperationsImpl(
-      Accounts accounts, @ServerInitiated AccountsUpdate accountsUpdate, Sequences seq) {
+      Accounts accounts,
+      @ServerInitiated AccountsUpdate accountsUpdate,
+      Sequences seq,
+      ExternalIdFactory externalIdFactory) {
     this.accounts = accounts;
     this.accountsUpdate = accountsUpdate;
     this.seq = seq;
+    this.externalIdFactory = externalIdFactory;
   }
 
   @Override
@@ -72,7 +78,7 @@
     return createdAccount.account().id();
   }
 
-  private static void initAccountDelta(
+  private void initAccountDelta(
       AccountDelta.Builder builder, TestAccountCreation accountCreation, Account.Id accountId) {
     accountCreation.fullname().ifPresent(builder::setFullName);
     accountCreation.preferredEmail().ifPresent(e -> setPreferredEmail(builder, accountId, e));
@@ -84,19 +90,19 @@
         .secondaryEmails()
         .forEach(
             secondaryEmail ->
-                builder.addExternalId(ExternalId.createEmail(accountId, secondaryEmail)));
+                builder.addExternalId(externalIdFactory.createEmail(accountId, secondaryEmail)));
   }
 
-  private static void setPreferredEmail(
+  private void setPreferredEmail(
       AccountDelta.Builder builder, Account.Id accountId, String preferredEmail) {
     builder
         .setPreferredEmail(preferredEmail)
-        .addExternalId(ExternalId.createEmail(accountId, preferredEmail));
+        .addExternalId(externalIdFactory.createEmail(accountId, preferredEmail));
   }
 
-  private static void setUsername(
+  private void setUsername(
       AccountDelta.Builder builder, Account.Id accountId, String username, String httpPassword) {
-    builder.addExternalId(ExternalId.createUsername(username, accountId, httpPassword));
+    builder.addExternalId(externalIdFactory.createUsername(username, accountId, httpPassword));
   }
 
   private class PerAccountOperationsImpl implements PerAccountOperations {
@@ -202,14 +208,14 @@
               .collect(toImmutableSet()));
       builder.addExternalIds(
           newSecondaryEmails.stream()
-              .map(secondaryEmail -> ExternalId.createEmail(accountId, secondaryEmail))
+              .map(secondaryEmail -> externalIdFactory.createEmail(accountId, secondaryEmail))
               .collect(toImmutableSet()));
       if (accountUpdate.preferredEmail().isPresent()) {
         builder.addExternalId(
-            ExternalId.createEmail(accountId, accountUpdate.preferredEmail().get()));
+            externalIdFactory.createEmail(accountId, accountUpdate.preferredEmail().get()));
       } else if (accountState.account().preferredEmail() != null) {
         builder.addExternalId(
-            ExternalId.createEmail(accountId, accountState.account().preferredEmail()));
+            externalIdFactory.createEmail(accountId, accountState.account().preferredEmail()));
       }
     }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
index 738be4d..2dd3f91 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperations.java
@@ -81,11 +81,11 @@
      *
      * <p>Example:
      *
-     * <pre>
+     * <pre>{@code
      * projectOperations.forInvalidation()
      *     .addProjectConfigUpdater(cfg -> cfg.setString("invalidSection", null, "foo", "bar"))
      *     .invalidate();
-     * </pre>
+     * }</pre>
      *
      * <p><strong>Note:</strong> The invalidation will fail with an exception if the project to
      * invalidate doesn't exist.
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index 9305914..9a9f309 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.auth.NoSuchUserException;
@@ -346,10 +347,12 @@
 
   static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
     private final ExternalIds externalIds;
+    private final ExternalIdKeyFactory externalIdKeyFactory;
 
     @Inject
-    UserLoader(ExternalIds externalIds) {
+    UserLoader(ExternalIds externalIds, ExternalIdKeyFactory externalIdKeyFactory) {
       this.externalIds = externalIds;
+      this.externalIdKeyFactory = externalIdKeyFactory;
     }
 
     @Override
@@ -358,7 +361,7 @@
           TraceContext.newTimer(
               "Loading account for username", Metadata.builder().username(username).build())) {
         return externalIds
-            .get(ExternalId.Key.create(SCHEME_GERRIT, username))
+            .get(externalIdKeyFactory.create(SCHEME_GERRIT, username))
             .map(ExternalId::accountId);
       }
     }
diff --git a/java/com/google/gerrit/common/data/GitwebType.java b/java/com/google/gerrit/common/data/GitwebType.java
index 9cc408b..e69eacf 100644
--- a/java/com/google/gerrit/common/data/GitwebType.java
+++ b/java/com/google/gerrit/common/data/GitwebType.java
@@ -29,7 +29,7 @@
   private char pathSeparator = '/';
   private boolean urlEncode = true;
 
-  /** @return name displayed in links. */
+  /** Returns name displayed in links. */
   public String getLinkName() {
     return name;
   }
@@ -43,7 +43,7 @@
     this.name = name;
   }
 
-  /** @return parameterized string for the branch URL. */
+  /** Returns parameterized string for the branch URL. */
   public String getBranch() {
     return branch;
   }
@@ -57,7 +57,7 @@
     branch = str;
   }
 
-  /** @return parameterized string for the tag URL. */
+  /** Returns parameterized string for the tag URL. */
   public String getTag() {
     return tag;
   }
@@ -71,7 +71,7 @@
     tag = str;
   }
 
-  /** @return parameterized string for the file URL. */
+  /** Returns parameterized string for the file URL. */
   public String getFile() {
     return file;
   }
@@ -85,7 +85,7 @@
     file = str;
   }
 
-  /** @return parameterized string for the file history URL. */
+  /** Returns parameterized string for the file history URL. */
   public String getFileHistory() {
     return fileHistory;
   }
@@ -99,7 +99,7 @@
     fileHistory = str;
   }
 
-  /** @return parameterized string for the project URL. */
+  /** Returns parameterized string for the project URL. */
   public String getProject() {
     return project;
   }
@@ -113,7 +113,7 @@
     project = str;
   }
 
-  /** @return parameterized string for the revision URL. */
+  /** Returns parameterized string for the revision URL. */
   public String getRevision() {
     return revision;
   }
@@ -127,7 +127,7 @@
     revision = str;
   }
 
-  /** @return parameterized string for the root tree URL. */
+  /** Returns parameterized string for the root tree URL. */
   public String getRootTree() {
     return rootTree;
   }
@@ -141,7 +141,7 @@
     rootTree = str;
   }
 
-  /** @return path separator used for branch and project names. */
+  /** Returns path separator used for branch and project names. */
   public char getPathSeparator() {
     return pathSeparator;
   }
@@ -155,7 +155,7 @@
     this.pathSeparator = separator;
   }
 
-  /** @return whether to URL encode path segments. */
+  /** Returns whether to URL encode path segments. */
   public boolean getUrlEncode() {
     return urlEncode;
   }
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index 8bfd960..253266d 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -165,17 +165,17 @@
     }
   }
 
-  /** @return all valid capability names. */
+  /** Returns all valid capability names. */
   public static Collection<String> getAllNames() {
     return Collections.unmodifiableList(NAMES_ALL);
   }
 
-  /** @return true if the name is recognized as a capability name. */
+  /** Returns true if the name is recognized as a capability name. */
   public static boolean isGlobalCapability(String varName) {
     return NAMES_LC.contains(varName.toLowerCase());
   }
 
-  /** @return true if the capability should have a range attached. */
+  /** Returns true if the capability should have a range attached. */
   public static boolean hasRange(String varName) {
     for (String n : RANGE_NAMES) {
       if (n.equalsIgnoreCase(varName)) {
@@ -189,7 +189,7 @@
     return Collections.unmodifiableList(Arrays.asList(RANGE_NAMES));
   }
 
-  /** @return the valid range for the capability if it has one, otherwise null. */
+  /** Returns the valid range for the capability if it has one, otherwise null. */
   public static PermissionRange.WithDefaults getRange(String varName) {
     if (QUERY_LIMIT.equalsIgnoreCase(varName)) {
       return new PermissionRange.WithDefaults(
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index c6400df..b5bf44b 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -45,7 +45,6 @@
    *
    * @param version for which to return an ElasticVersion
    * @return the corresponding ElasticVersion if supported
-   * @throws UnsupportedVersion
    */
   public static ElasticVersion forVersion(String version) {
     for (ElasticVersion value : ElasticVersion.values()) {
diff --git a/java/com/google/gerrit/entities/AccessSection.java b/java/com/google/gerrit/entities/AccessSection.java
index d97bca8..69a234a 100644
--- a/java/com/google/gerrit/entities/AccessSection.java
+++ b/java/com/google/gerrit/entities/AccessSection.java
@@ -52,7 +52,7 @@
     return new AutoValue_AccessSection.Builder().setName(name).setPermissions(ImmutableList.of());
   }
 
-  /** @return true if the name is likely to be a valid reference section name. */
+  /** Returns true if the name is likely to be a valid reference section name. */
   public static boolean isValidRefSectionName(String name) {
     return name.startsWith("refs/") || name.startsWith("^refs/");
   }
diff --git a/java/com/google/gerrit/entities/AccountGroup.java b/java/com/google/gerrit/entities/AccountGroup.java
index 0b2a346..001a544 100644
--- a/java/com/google/gerrit/entities/AccountGroup.java
+++ b/java/com/google/gerrit/entities/AccountGroup.java
@@ -54,7 +54,7 @@
       return uuid();
     }
 
-    /** @return true if the UUID is for a group managed within Gerrit. */
+    /** Returns true if the UUID is for a group managed within Gerrit. */
     public boolean isInternalGroup() {
       return get().matches("^[0-9a-f]{40}$");
     }
diff --git a/java/com/google/gerrit/entities/Address.java b/java/com/google/gerrit/entities/Address.java
index 2324330..5d63476 100644
--- a/java/com/google/gerrit/entities/Address.java
+++ b/java/com/google/gerrit/entities/Address.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 import com.google.gerrit.common.Nullable;
 
 /** Represents an address (name + email) in an email message. */
@@ -66,8 +67,9 @@
 
   public abstract String email();
 
+  @Memoized
   @Override
-  public final int hashCode() {
+  public int hashCode() {
     return email().hashCode();
   }
 
diff --git a/java/com/google/gerrit/entities/GroupDescription.java b/java/com/google/gerrit/entities/GroupDescription.java
index e950257..7054bed 100644
--- a/java/com/google/gerrit/entities/GroupDescription.java
+++ b/java/com/google/gerrit/entities/GroupDescription.java
@@ -22,22 +22,22 @@
 public class GroupDescription {
   /** The Basic information required to be exposed by any Group. */
   public interface Basic {
-    /** @return the non-null UUID of the group. */
+    /** Returns the non-null UUID of the group. */
     AccountGroup.UUID getGroupUUID();
 
-    /** @return the non-null name of the group. */
+    /** Returns the non-null name of the group. */
     String getName();
 
     /**
-     * @return optional email address to send to the group's members. If provided, Gerrit will use
-     *     this email address to send change notifications to the group.
+     * Returns optional email address to send to the group's members. If provided, Gerrit will use
+     * this email address to send change notifications to the group.
      */
     @Nullable
     String getEmailAddress();
 
     /**
-     * @return optional URL to information about the group. Typically a URL to a web page that
-     *     permits users to apply to join the group, or manage their membership.
+     * Returns optional URL to information about the group. Typically a URL to a web page that
+     * permits users to apply to join the group, or manage their membership.
      */
     @Nullable
     String getUrl();
diff --git a/java/com/google/gerrit/entities/GroupReference.java b/java/com/google/gerrit/entities/GroupReference.java
index 208ba0f..125153e 100644
--- a/java/com/google/gerrit/entities/GroupReference.java
+++ b/java/com/google/gerrit/entities/GroupReference.java
@@ -17,6 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 import com.google.gerrit.common.Nullable;
 
 /** Describes a group within a projects {@link AccessSection}s. */
@@ -78,8 +79,9 @@
     return "?";
   }
 
+  @Memoized
   @Override
-  public final int hashCode() {
+  public int hashCode() {
     return uuid(this).hashCode();
   }
 
diff --git a/java/com/google/gerrit/entities/ImmutableConfig.java b/java/com/google/gerrit/entities/ImmutableConfig.java
index a5efc14..83a44d1 100644
--- a/java/com/google/gerrit/entities/ImmutableConfig.java
+++ b/java/com/google/gerrit/entities/ImmutableConfig.java
@@ -51,27 +51,27 @@
     return cfg;
   }
 
-  /** @see Config#getSections() */
+  /** See {@link Config#getSections()} */
   public Set<String> getSections() {
     return cfg.getSections();
   }
 
-  /** @see Config#getNames(String) */
+  /** See {@link Config#getNames(String)} */
   public Set<String> getNames(String section) {
     return cfg.getNames(section);
   }
 
-  /** @see Config#getNames(String, String) */
+  /** See {@link Config#getNames(String, String)} */
   public Set<String> getNames(String section, String subsection) {
     return cfg.getNames(section, subsection);
   }
 
-  /** @see Config#getStringList(String, String, String) */
+  /** See {@link Config#getStringList(String, String, String)} */
   public String[] getStringList(String section, String subsection, String name) {
     return cfg.getStringList(section, subsection, name);
   }
 
-  /** @see Config#getSubsections(String) */
+  /** See {@link Config#getSubsections(String)} */
   public Set<String> getSubsections(String section) {
     return cfg.getSubsections(section);
   }
diff --git a/java/com/google/gerrit/entities/NotifyConfig.java b/java/com/google/gerrit/entities/NotifyConfig.java
index 17da81f..5c0a3db 100644
--- a/java/com/google/gerrit/entities/NotifyConfig.java
+++ b/java/com/google/gerrit/entities/NotifyConfig.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
@@ -106,8 +107,9 @@
     return getName().compareTo(o.getName());
   }
 
+  @Memoized
   @Override
-  public final int hashCode() {
+  public int hashCode() {
     return getName().hashCode();
   }
 
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index 856765b..2d28046 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -78,25 +78,27 @@
     public abstract String fileName();
   }
 
-  /** Type of modification made to the file path. */
+  /**
+   * Type of modification made to the file path. Ordering of values matters (used by diff cache).
+   */
   public enum ChangeType implements CodedEnum {
     /** Path is being created/introduced by this patch. */
     ADDED('A'),
 
-    /** Path already exists, and has updated content. */
-    MODIFIED('M'),
-
-    /** Path existed, but is being removed by this patch. */
-    DELETED('D'),
-
     /** Path existed at the source but was moved. */
     RENAMED('R'),
 
+    /** Path already exists, and has updated content. */
+    MODIFIED('M'),
+
     /** Path was copied from the source. */
     COPIED('C'),
 
     /** Sufficient amount of content changed to claim the file was rewritten. */
-    REWRITE('W');
+    REWRITE('W'),
+
+    /** Path existed, but is being removed by this patch. */
+    DELETED('D');
 
     private final char code;
 
diff --git a/java/com/google/gerrit/entities/PatchSetApproval.java b/java/com/google/gerrit/entities/PatchSetApproval.java
index a4bb251..f853f77 100644
--- a/java/com/google/gerrit/entities/PatchSetApproval.java
+++ b/java/com/google/gerrit/entities/PatchSetApproval.java
@@ -41,7 +41,7 @@
   }
 
   public static Builder builder() {
-    return new AutoValue_PatchSetApproval.Builder().postSubmit(false);
+    return new AutoValue_PatchSetApproval.Builder().postSubmit(false).copied(false);
   }
 
   @AutoValue.Builder
@@ -72,6 +72,8 @@
 
     public abstract Builder postSubmit(boolean isPostSubmit);
 
+    public abstract Builder copied(boolean isCopied);
+
     abstract PatchSetApproval autoBuild();
 
     public PatchSetApproval build() {
@@ -111,10 +113,12 @@
 
   public abstract boolean postSubmit();
 
+  public abstract boolean copied();
+
   public abstract Builder toBuilder();
 
   public PatchSetApproval copyWithPatchSet(PatchSet.Id psId) {
-    return toBuilder().key(key(psId, key().accountId(), key().labelId())).build();
+    return toBuilder().key(key(psId, key().accountId(), key().labelId())).copied(true).build();
   }
 
   public PatchSet.Id patchSetId() {
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 322c79e..95164bd 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -95,7 +95,7 @@
     LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
   }
 
-  /** @return true if the name is recognized as a permission name. */
+  /** Returns true if the name is recognized as a permission name. */
   public static boolean isPermission(String varName) {
     return isLabel(varName) || isLabelAs(varName) || NAMES_LC.contains(varName.toLowerCase());
   }
@@ -104,22 +104,22 @@
     return isLabel(varName) || isLabelAs(varName);
   }
 
-  /** @return true if the permission name is actually for a review label. */
+  /** Returns true if the permission name is actually for a review label. */
   public static boolean isLabel(String varName) {
     return varName.startsWith(LABEL) && LABEL.length() < varName.length();
   }
 
-  /** @return true if the permission is for impersonated review labels. */
+  /** Returns true if the permission is for impersonated review labels. */
   public static boolean isLabelAs(String var) {
     return var.startsWith(LABEL_AS) && LABEL_AS.length() < var.length();
   }
 
-  /** @return permission name for the given review label. */
+  /** Returns permission name for the given review label. */
   public static String forLabel(String labelName) {
     return LABEL + labelName;
   }
 
-  /** @return permission name to apply a label for another user. */
+  /** Returns permission name to apply a label for another user. */
   public static String forLabelAs(String labelName) {
     return LABEL_AS + labelName;
   }
diff --git a/java/com/google/gerrit/entities/PermissionRange.java b/java/com/google/gerrit/entities/PermissionRange.java
index fa9f4c2..d283069 100644
--- a/java/com/google/gerrit/entities/PermissionRange.java
+++ b/java/com/google/gerrit/entities/PermissionRange.java
@@ -46,7 +46,7 @@
       defaultMax = max;
     }
 
-    /** @return all values between {@link #getMin()} and {@link #getMax()} */
+    /** Returns all values between {@link #getMin()} and {@link #getMax()} */
     public List<Integer> getValuesAsList() {
       ArrayList<Integer> r = new ArrayList<>(getRangeSize());
       for (int i = min; i <= max; i++) {
@@ -55,7 +55,7 @@
       return r;
     }
 
-    /** @return number of values between {@link #getMin()} and {@link #getMax()} */
+    /** Returns number of values between {@link #getMin()} and {@link #getMax()} */
     public int getRangeSize() {
       return max - min;
     }
diff --git a/java/com/google/gerrit/entities/SubmitRecord.java b/java/com/google/gerrit/entities/SubmitRecord.java
index 860997f..95ad9f8 100644
--- a/java/com/google/gerrit/entities/SubmitRecord.java
+++ b/java/com/google/gerrit/entities/SubmitRecord.java
@@ -68,6 +68,8 @@
     }
   }
 
+  // Name of the rule that created this submit record, formatted as '$pluginName~$ruleName'
+  public String ruleName;
   public Status status;
   public List<Label> labels;
   public List<LegacySubmitRequirement> requirements;
@@ -158,6 +160,7 @@
    */
   public SubmitRecord deepCopy() {
     SubmitRecord copy = new SubmitRecord();
+    copy.ruleName = ruleName;
     copy.status = status;
     copy.errorMessage = errorMessage;
     if (labels != null) {
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
index e1d5f39..4f2f2f6 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -41,6 +41,12 @@
   /** SHA-1 of the patchset commit ID for which the submit requirement was evaluated. */
   public abstract ObjectId patchSetCommitId();
 
+  /**
+   * Whether this result was created from a legacy {@link SubmitRecord}, or by evaluating a {@link
+   * SubmitRequirement}.
+   */
+  public abstract boolean legacy();
+
   @Memoized
   public Status status() {
     if (assertError(submittabilityExpressionResult())
@@ -109,6 +115,8 @@
 
     public abstract Builder patchSetCommitId(ObjectId value);
 
+    public abstract Builder legacy(boolean value);
+
     public abstract SubmitRequirementResult build();
   }
 
diff --git a/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
index 78a35ff..9e77025 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
@@ -39,7 +39,8 @@
             .setKey(patchSetApprovalKeyProtoConverter.toProto(patchSetApproval.key()))
             .setValue(patchSetApproval.value())
             .setGranted(patchSetApproval.granted().getTime())
-            .setPostSubmit(patchSetApproval.postSubmit());
+            .setPostSubmit(patchSetApproval.postSubmit())
+            .setCopied(patchSetApproval.copied());
 
     patchSetApproval.tag().ifPresent(builder::setTag);
     Account.Id realAccountId = patchSetApproval.realAccountId();
@@ -61,7 +62,8 @@
             .key(patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()))
             .value(proto.getValue())
             .granted(new Timestamp(proto.getGranted()))
-            .postSubmit(proto.getPostSubmit());
+            .postSubmit(proto.getPostSubmit())
+            .copied(proto.getCopied());
     if (proto.hasTag()) {
       builder.tag(proto.getTag());
     }
diff --git a/java/com/google/gerrit/extensions/BUILD b/java/com/google/gerrit/extensions/BUILD
index 21949f7..f36018b 100644
--- a/java/com/google/gerrit/extensions/BUILD
+++ b/java/com/google/gerrit/extensions/BUILD
@@ -34,6 +34,7 @@
         "//java/com/google/gerrit/common:annotations",
         "//lib:guava",
         "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
     ],
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 6df9889..9c9c282 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.extensions.api.accounts;
 
-import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -23,7 +22,6 @@
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AgreementInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
@@ -32,7 +30,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import java.util.List;
 import java.util.Map;
-import java.util.SortedSet;
 
 public interface AccountApi {
   AccountInfo get() throws RestApiException;
@@ -67,12 +64,6 @@
 
   void unstarChange(String changeId) throws RestApiException;
 
-  void setStars(String changeId, StarsInput input) throws RestApiException;
-
-  SortedSet<String> getStars(String changeId) throws RestApiException;
-
-  List<ChangeInfo> getStarredChanges() throws RestApiException;
-
   List<GroupInfo> getGroups() throws RestApiException;
 
   List<EmailInfo> getEmails() throws RestApiException;
@@ -221,21 +212,6 @@
     }
 
     @Override
-    public void setStars(String changeId, StarsInput input) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public SortedSet<String> getStars(String changeId) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
-    public List<ChangeInfo> getStarredChanges() throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public List<GroupInfo> getGroups() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 15fca9a..285b385 100644
--- a/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -38,7 +38,11 @@
    */
   AccountApi id(String id) throws RestApiException;
 
-  /** @see #id(String) */
+  /**
+   * Look up an account by ID. #id(String)
+   *
+   * <p>See #id(String)
+   */
   AccountApi id(int id) throws RestApiException;
 
   /**
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 04f5bd2..690ba4e 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -140,14 +140,6 @@
   boolean ignored() throws RestApiException;
 
   /**
-   * Mark this change as reviewed/unreviewed.
-   *
-   * @param reviewed flag to decide if this change should be marked as reviewed ({@code true}) or
-   *     unreviewed ({@code false})
-   */
-  void markAsReviewed(boolean reviewed) throws RestApiException;
-
-  /**
    * Create a new change that reverts this change.
    *
    * @see Changes#id(int)
@@ -332,7 +324,6 @@
    * Get hashtags on a change.
    *
    * @return hashtags
-   * @throws RestApiException
    */
   Set<String> getHashtags() throws RestApiException;
 
@@ -367,7 +358,6 @@
    *
    * @return comments in a map keyed by path; comments have the {@code revision} field set to
    *     indicate their patch set.
-   * @throws RestApiException
    * @deprecated Callers should use {@link #commentsRequest()} instead
    */
   @Deprecated
@@ -380,7 +370,6 @@
    *
    * @return comments as a list; comments have the {@code revision} field set to indicate their
    *     patch set.
-   * @throws RestApiException
    * @deprecated Callers should use {@link #commentsRequest()} instead
    */
   @Deprecated
@@ -401,7 +390,6 @@
    *
    * @return robot comments in a map keyed by path; robot comments have the {@code revision} field
    *     set to indicate their patch set.
-   * @throws RestApiException
    */
   Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException;
 
@@ -410,7 +398,6 @@
    *
    * @return drafts in a map keyed by path; comments have the {@code revision} field set to indicate
    *     their patch set.
-   * @throws RestApiException
    */
   default Map<String, List<CommentInfo>> drafts() throws RestApiException {
     return draftsRequest().get();
@@ -421,7 +408,6 @@
    *
    * @return drafts as a list; comments have the {@code revision} field set to indicate their patch
    *     set.
-   * @throws RestApiException
    */
   default List<CommentInfo> draftsAsList() throws RestApiException {
     return draftsRequest().getAsList();
@@ -451,7 +437,6 @@
    * Get all messages of a change with detailed account info.
    *
    * @return a list of messages sorted by their creation time.
-   * @throws RestApiException
    */
   List<ChangeMessageInfo> messages() throws RestApiException;
 
@@ -474,7 +459,6 @@
      *
      * @return comments in a map keyed by path; comments have the {@code revision} field set to
      *     indicate their patch set.
-     * @throws RestApiException
      */
     public abstract Map<String, List<CommentInfo>> get() throws RestApiException;
 
@@ -814,11 +798,6 @@
     }
 
     @Override
-    public void markAsReviewed(boolean reviewed) throws RestApiException {
-      throw new NotImplementedException();
-    }
-
-    @Override
     public PureRevertInfo pureRevert() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/changes/FileApi.java b/java/com/google/gerrit/extensions/api/changes/FileApi.java
index 26f9452..e20ac56 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -29,10 +29,18 @@
   /** Diff against the revision's parent version of the file. */
   DiffInfo diff() throws RestApiException;
 
-  /** @param base revision id of the revision to be used as the diff base */
+  /**
+   * Diff against the specified base
+   *
+   * @param base revision id of the revision to be used as the diff base
+   */
   DiffInfo diff(String base) throws RestApiException;
 
-  /** @param parent 1-based parent number to diff against */
+  /**
+   * Diff against the specified parent
+   *
+   * @param parent 1-based parent number to diff against
+   */
   DiffInfo diff(int parent) throws RestApiException;
 
   /**
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 229b9d4..1307516 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -160,7 +160,6 @@
    *
    * @param format the format of the archive
    * @return the archive as {@link BinaryResult}
-   * @throws RestApiException
    */
   BinaryResult getArchive(ArchiveFormat format) throws RestApiException;
 
diff --git a/java/com/google/gerrit/extensions/api/changes/StarsInput.java b/java/com/google/gerrit/extensions/api/changes/StarsInput.java
deleted file mode 100644
index 1207d27..0000000
--- a/java/com/google/gerrit/extensions/api/changes/StarsInput.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.api.changes;
-
-import java.util.Set;
-
-public class StarsInput {
-  public Set<String> add;
-  public Set<String> remove;
-
-  public StarsInput() {}
-
-  public StarsInput(Set<String> add) {
-    this.add = add;
-  }
-
-  public StarsInput(Set<String> add, Set<String> remove) {
-    this.add = add;
-    this.remove = remove;
-  }
-}
diff --git a/java/com/google/gerrit/extensions/api/config/Config.java b/java/com/google/gerrit/extensions/api/config/Config.java
index eb7288d..041e1dd 100644
--- a/java/com/google/gerrit/extensions/api/config/Config.java
+++ b/java/com/google/gerrit/extensions/api/config/Config.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 
 public interface Config {
-  /** @return An API for getting server related configurations. */
+  /** Returns an API for getting server related configurations. */
   Server server();
 
   /**
diff --git a/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java b/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
index e582f1b..9fb57ad 100644
--- a/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
+++ b/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.config;
 
+import com.google.errorprone.annotations.FormatMethod;
 import java.util.List;
 import java.util.Objects;
 
@@ -80,10 +81,12 @@
       return status.name() + ": " + message;
     }
 
+    @FormatMethod
     public static ConsistencyProblemInfo warning(String fmt, Object... args) {
       return new ConsistencyProblemInfo(Status.WARNING, String.format(fmt, args));
     }
 
+    @FormatMethod
     public static ConsistencyProblemInfo error(String fmt, Object... args) {
       return new ConsistencyProblemInfo(Status.ERROR, String.format(fmt, args));
     }
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index 70d1bff..8b69ded 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -24,7 +24,7 @@
 import java.util.List;
 
 public interface Server {
-  /** @return Version of server. */
+  /** Returns version of server. */
   String getVersion() throws RestApiException;
 
   ServerInfo getInfo() throws RestApiException;
diff --git a/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index 067f120..e1b3a9f 100644
--- a/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -24,53 +24,49 @@
 import java.util.List;
 
 public interface GroupApi {
-  /** @return group info with no {@code ListGroupsOption}s set. */
+  /** Returns group info with no {@code ListGroupsOption}s set. */
   GroupInfo get() throws RestApiException;
 
-  /** @return group info with all {@code ListGroupsOption}s set. */
+  /** Returns group info with all {@code ListGroupsOption}s set. */
   GroupInfo detail() throws RestApiException;
 
-  /** @return group name. */
+  /** Returns group name. */
   String name() throws RestApiException;
 
   /**
    * Set group name.
    *
    * @param name new name.
-   * @throws RestApiException
    */
   void name(String name) throws RestApiException;
 
-  /** @return owning group info. */
+  /** Returns owning group info. */
   GroupInfo owner() throws RestApiException;
 
   /**
    * Set group owner.
    *
    * @param owner identifier of new group owner.
-   * @throws RestApiException
    */
   void owner(String owner) throws RestApiException;
 
-  /** @return group description. */
+  /** Returns group description. */
   String description() throws RestApiException;
 
   /**
    * Set group decsription.
    *
    * @param description new description.
-   * @throws RestApiException
    */
   void description(String description) throws RestApiException;
 
-  /** @return group options. */
+  /** Returns group options. */
   GroupOptionsInfo options() throws RestApiException;
 
   /**
    * Set group options.
    *
    * @param options new options.
-   * @throws RestApiException
    */
   void options(GroupOptionsInfo options) throws RestApiException;
 
@@ -78,7 +74,6 @@
    * List group members, non-recursively.
    *
    * @return group members.
-   * @throws RestApiException
    */
   List<AccountInfo> members() throws RestApiException;
 
@@ -87,7 +82,6 @@
    *
    * @param recursive whether to recursively included groups.
    * @return group members.
-   * @throws RestApiException
    */
   List<AccountInfo> members(boolean recursive) throws RestApiException;
 
@@ -96,7 +90,6 @@
    *
    * @param members list of member identifiers, in any format accepted by {@link
    *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
-   * @throws RestApiException
    */
   void addMembers(List<String> members) throws RestApiException;
 
@@ -105,7 +98,6 @@
    *
    * @param members list of member identifiers, in any format accepted by {@link
    *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
-   * @throws RestApiException
    */
   default void addMembers(String... members) throws RestApiException {
     addMembers(Arrays.asList(members));
@@ -116,7 +108,6 @@
    *
    * @param members list of member identifiers, in any format accepted by {@link
    *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
-   * @throws RestApiException
    */
   void removeMembers(List<String> members) throws RestApiException;
 
@@ -125,7 +116,6 @@
    *
    * @param members list of member identifiers, in any format accepted by {@link
    *     com.google.gerrit.extensions.api.accounts.Accounts#id(String)}
-   * @throws RestApiException
    */
   default void removeMembers(String... members) throws RestApiException {
     removeMembers(Arrays.asList(members));
@@ -135,7 +125,6 @@
    * Lists the subgroups of this group.
    *
    * @return the found subgroups
-   * @throws RestApiException
    */
   List<GroupInfo> includedGroups() throws RestApiException;
 
@@ -143,7 +132,6 @@
    * Adds subgroups to this group.
    *
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
-   * @throws RestApiException
    */
   void addGroups(List<String> groups) throws RestApiException;
 
@@ -151,7 +139,6 @@
    * Adds subgroups to this group.
    *
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
-   * @throws RestApiException
    */
   default void addGroups(String... groups) throws RestApiException {
     addGroups(Arrays.asList(groups));
@@ -161,7 +148,6 @@
    * Removes subgroups from this group.
    *
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
-   * @throws RestApiException
    */
   void removeGroups(List<String> groups) throws RestApiException;
 
@@ -169,7 +155,6 @@
    * Removes subgroups from this group.
    *
    * @param groups list of group identifiers, in any format accepted by {@link Groups#id(String)}
-   * @throws RestApiException
    */
   default void removeGroups(String... groups) throws RestApiException {
     removeGroups(Arrays.asList(groups));
@@ -179,7 +164,6 @@
    * Returns the audit log of the group.
    *
    * @return list of audit events of the group.
-   * @throws RestApiException
    */
   List<? extends GroupAuditEventInfo> auditLog() throws RestApiException;
 
@@ -187,8 +171,6 @@
    * Reindexes the group.
    *
    * <p>Only supported for internal groups.
-   *
-   * @throws RestApiException
    */
   void index() throws RestApiException;
 
diff --git a/java/com/google/gerrit/extensions/api/groups/Groups.java b/java/com/google/gerrit/extensions/api/groups/Groups.java
index 81b5f47..1a46930 100644
--- a/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -47,7 +47,7 @@
   /** Create a new group. */
   GroupApi create(GroupInput input) throws RestApiException;
 
-  /** @return new request for listing groups. */
+  /** Returns new request for listing groups. */
   ListRequest list();
 
   /**
diff --git a/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java b/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
index 417f55a..c3d760b 100644
--- a/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
+++ b/java/com/google/gerrit/extensions/auth/oauth/OAuthServiceProvider.java
@@ -40,9 +40,7 @@
    * After establishing of secure communication channel, this method supossed to access the
    * protected resoure and retrieve the username.
    *
-   * @param token
    * @return OAuth user information
-   * @throws IOException
    */
   OAuthUserInfo getUserInfo(OAuthToken token) throws IOException;
 
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 21b319e..b26f435 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -148,6 +148,7 @@
   public DefaultBase defaultBaseForMerges;
   public Boolean publishCommentsOnPush;
   public Boolean disableKeyboardShortcuts;
+  public Boolean disableTokenHighlighting;
   public Boolean workInProgressByDefault;
   public List<MenuItem> my;
   public List<String> changeTable;
@@ -207,6 +208,7 @@
     p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
     p.publishCommentsOnPush = false;
     p.disableKeyboardShortcuts = false;
+    p.disableTokenHighlighting = false;
     p.workInProgressByDefault = false;
     return p;
   }
diff --git a/java/com/google/gerrit/extensions/common/AccountInfo.java b/java/com/google/gerrit/extensions/common/AccountInfo.java
index 60ba18d..4701e86 100644
--- a/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -27,10 +27,13 @@
  * are defined in {@link AccountDetailInfo}.
  */
 public class AccountInfo {
-  /** Tags are additional properties of an account. */
-  public enum Tag {
+  /**
+   * Tags are additional properties of an account. These are just tags known to Gerrit core. Plugins
+   * may define their own.
+   */
+  public static final class Tags {
     /** Tag indicating that this account is a service user. */
-    SERVICE_USER
+    public static final String SERVICE_USER = "SERVICE_USER";
   }
 
   /** The numeric ID of the account. */
@@ -74,7 +77,7 @@
   public Boolean inactive;
 
   /** Tags, such as whether this account is a service user. */
-  public List<Tag> tags;
+  public List<String> tags;
 
   public AccountInfo(Integer id) {
     this._accountId = id;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 6afe8ac..2bb3dd7 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -112,6 +112,7 @@
   public List<PluginDefinedInfo> plugins;
   public Collection<TrackingIdInfo> trackingIds;
   public Collection<LegacySubmitRequirementInfo> requirements;
+  public Collection<SubmitRecordInfo> submitRecords;
   public Collection<SubmitRequirementResultInfo> submitRequirements;
 
   public ChangeInfo() {}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 0447e80..ad112d3 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -63,9 +63,12 @@
    */
   public static ChangeInfoDifference getDifference(
       ChangeInfo oldChangeInfo, ChangeInfo newChangeInfo) {
-    return ChangeInfoDifference.create(
-        /* added= */ getAdded(oldChangeInfo, newChangeInfo),
-        /* removed= */ getAdded(newChangeInfo, oldChangeInfo));
+    return ChangeInfoDifference.builder()
+        .setOldChangeInfo(oldChangeInfo)
+        .setNewChangeInfo(newChangeInfo)
+        .setAdded(getAdded(oldChangeInfo, newChangeInfo))
+        .setRemoved(getAdded(newChangeInfo, oldChangeInfo))
+        .build();
   }
 
   @SuppressWarnings("unchecked") // reflection is used to construct instances of T
@@ -143,7 +146,7 @@
     }
   }
 
-  /** @return {@code null} if nothing has been added to {@code oldCollection} */
+  /** Returns {@code null} if nothing has been added to {@code oldCollection} */
   private static ImmutableList<?> getAddedForCollection(
       Collection<?> oldCollection, Collection<?> newCollection) {
     ImmutableList<?> notInOldCollection = getAdditions(oldCollection, newCollection);
@@ -165,7 +168,7 @@
     return duplicatesMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
   }
 
-  /** @return {@code null} if nothing has been added to {@code oldMap} */
+  /** Returns {@code null} if nothing has been added to {@code oldMap} */
   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/common/ChangeInfoDifference.java b/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
index 269c673..997c3ee 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDifference.java
@@ -20,11 +20,29 @@
 @AutoValue
 public abstract class ChangeInfoDifference {
 
+  public abstract ChangeInfo oldChangeInfo();
+
+  public abstract ChangeInfo newChangeInfo();
+
   public abstract ChangeInfo added();
 
   public abstract ChangeInfo removed();
 
-  public static ChangeInfoDifference create(ChangeInfo added, ChangeInfo removed) {
-    return new AutoValue_ChangeInfoDifference(added, removed);
+  public static Builder builder() {
+    return new AutoValue_ChangeInfoDifference.Builder();
+  }
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setOldChangeInfo(ChangeInfo oldChangeInfo);
+
+    public abstract Builder setNewChangeInfo(ChangeInfo newChangeInfo);
+
+    public abstract Builder setAdded(ChangeInfo added);
+
+    public abstract Builder setRemoved(ChangeInfo removed);
+
+    public abstract ChangeInfoDifference build();
   }
 }
diff --git a/java/com/google/gerrit/extensions/common/SubmitRecordInfo.java b/java/com/google/gerrit/extensions/common/SubmitRecordInfo.java
new file mode 100644
index 0000000..09c9841
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/SubmitRecordInfo.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+
+/** API response containing a {@link com.google.gerrit.entities.SubmitRecord} entity. */
+public class SubmitRecordInfo {
+  public enum Status {
+    OK,
+    NOT_READY,
+    CLOSED,
+    FORCED,
+    RULE_ERROR
+  }
+
+  public static class Label {
+    public enum Status {
+      OK,
+      REJECT,
+      NEED,
+      MAY,
+      IMPOSSIBLE
+    }
+
+    public String label;
+    public Status status;
+    public AccountInfo appliedBy;
+  }
+
+  public String ruleName;
+  public Status status;
+  public List<Label> labels;
+  public List<LegacySubmitRequirementInfo> requirements;
+  public String errorMessage;
+}
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
index 685e81a..d17da0a 100644
--- a/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
@@ -47,6 +47,9 @@
   /** Overall result (status) of evaluating this submit requirement. */
   public Status status;
 
+  /** Whether this result was created from a legacy submit record. */
+  public boolean isLegacy;
+
   /** Result of evaluating the applicability expression. */
   public SubmitRequirementExpressionInfo applicabilityExpressionResult;
 
diff --git a/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java b/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java
index deb03b0..2af9a767 100644
--- a/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java
+++ b/java/com/google/gerrit/extensions/common/TestSubmitRuleInfo.java
@@ -19,7 +19,7 @@
 import java.util.Objects;
 
 public class TestSubmitRuleInfo {
-  /** @see com.google.gerrit.entities.SubmitRecord.Status */
+  /** See {@link com.google.gerrit.entities.SubmitRecord.Status} */
   public String status;
 
   public String errorMessage;
diff --git a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
index 162dd99..9c354fb 100644
--- a/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
+++ b/java/com/google/gerrit/extensions/conditions/BooleanCondition.java
@@ -48,7 +48,7 @@
 
   BooleanCondition() {}
 
-  /** @return evaluate the condition and return its value. */
+  /** Evaluates the condition and return its value. */
   public abstract boolean value();
 
   /**
@@ -63,7 +63,9 @@
    * Reduce evaluation tree by cutting off branches that evaluate trivially and replacing them with
    * a leave note corresponding to the value the branch evaluated to.
    *
-   * <p><code>
+   * <p>
+   *
+   * <pre>{@code
    * Example 1 (T=True, F=False, C=non-trivial check):
    *      OR
    *     /  \    =>    T
@@ -76,7 +78,7 @@
    *      AND
    *     /  \    =>    F
    *    T   F
-   * </code>
+   * }</pre>
    *
    * <p>There is no guarantee that the resulting tree is minimal. The only guarantee made is that
    * branches that evaluate trivially will be cut off and replaced by primitive values.
diff --git a/java/com/google/gerrit/extensions/config/DownloadScheme.java b/java/com/google/gerrit/extensions/config/DownloadScheme.java
index d81657a..96b5878 100644
--- a/java/com/google/gerrit/extensions/config/DownloadScheme.java
+++ b/java/com/google/gerrit/extensions/config/DownloadScheme.java
@@ -26,12 +26,12 @@
    */
   public abstract String getUrl(String project);
 
-  /** @return whether this scheme requires authentication */
+  /** Returns whether this scheme requires authentication */
   public abstract boolean isAuthRequired();
 
-  /** @return whether this scheme supports authentication */
+  /** Returns whether this scheme supports authentication */
   public abstract boolean isAuthSupported();
 
-  /** @return whether the download scheme is enabled */
+  /** Returns whether the download scheme is enabled */
   public abstract boolean isEnabled();
 }
diff --git a/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java b/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
index edb3e69..45c33c9 100644
--- a/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
+++ b/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
@@ -22,8 +22,9 @@
 public interface GarbageCollectorListener {
   interface Event extends ProjectEvent {
     /**
-     * @return Properties describing the result of the garbage collection performed by JGit.
-     * @see org.eclipse.jgit.api.GarbageCollectCommand#call()
+     * Returns properties describing the result of the garbage collection performed by JGit.
+     *
+     * <p>See {@link org.eclipse.jgit.api.GarbageCollectCommand#call }
      */
     Properties getStatistics();
   }
diff --git a/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index bdddfd9..2ee376e 100644
--- a/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -63,7 +63,7 @@
   private boolean base64;
   private String attachmentName;
 
-  /** @return the MIME type of the result, for HTTP clients. */
+  /** Returns the MIME type of the result, for HTTP clients. */
   public String getContentType() {
     Charset enc = getCharacterEncoding();
     if (enc != null) {
@@ -100,7 +100,7 @@
     return this;
   }
 
-  /** @return length in bytes of the result; -1 if not known. */
+  /** Returns length in bytes of the result; -1 if not known. */
   public long getContentLength() {
     return contentLength;
   }
@@ -111,7 +111,7 @@
     return this;
   }
 
-  /** @return true if this result can be gzip compressed to clients. */
+  /** Returns true if this result can be gzip compressed to clients. */
   public boolean canGzip() {
     return gzip;
   }
@@ -122,7 +122,7 @@
     return this;
   }
 
-  /** @return true if the result must be base64 encoded. */
+  /** Returns true if the result must be base64 encoded. */
   public boolean isBase64() {
     return base64;
   }
diff --git a/java/com/google/gerrit/extensions/restapi/IdString.java b/java/com/google/gerrit/extensions/restapi/IdString.java
index 736c3ba..b2538fa 100644
--- a/java/com/google/gerrit/extensions/restapi/IdString.java
+++ b/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -36,17 +36,17 @@
     urlEncoded = s;
   }
 
-  /** @return the decoded value of the string. */
+  /** Returns the decoded value of the string. */
   public String get() {
     return Url.decode(urlEncoded);
   }
 
-  /** @return true if the string is the empty string. */
+  /** Returns true if the string is the empty string. */
   public boolean isEmpty() {
     return urlEncoded.isEmpty();
   }
 
-  /** @return the original URL encoding supplied by the client. */
+  /** Returns the original URL encoding supplied by the client. */
   public String encoded() {
     return urlEncoded;
   }
diff --git a/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java b/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java
index b8c9d38..86b821b 100644
--- a/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java
+++ b/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java
@@ -22,4 +22,12 @@
   public PreconditionFailedException(String msg) {
     super(msg);
   }
+
+  /**
+   * @param msg message to return to the client describing the error.
+   * @param cause cause of this exception.
+   */
+  public PreconditionFailedException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/restapi/RestResource.java b/java/com/google/gerrit/extensions/restapi/RestResource.java
index cc5d48d..3c8144a 100644
--- a/java/com/google/gerrit/extensions/restapi/RestResource.java
+++ b/java/com/google/gerrit/extensions/restapi/RestResource.java
@@ -26,7 +26,7 @@
 
   /** A resource with a last modification date. */
   public interface HasLastModified {
-    /** @return time for the Last-Modified header. HTTP truncates the header value to seconds. */
+    /** Returns time for the Last-Modified header. HTTP truncates the header value to seconds. */
     Timestamp getLastModified();
   }
 
diff --git a/java/com/google/gerrit/extensions/webui/WebUiPlugin.java b/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
index 2d49e1c..4f129b0 100644
--- a/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
+++ b/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
@@ -44,7 +44,7 @@
 
   private String pluginName;
 
-  /** @return installed name of the plugin that provides this UI feature. */
+  /** Returns installed name of the plugin that provides this UI feature. */
   public final String getPluginName() {
     return pluginName;
   }
@@ -54,7 +54,7 @@
     this.pluginName = pluginName;
   }
 
-  /** @return path to initialization script within the plugin's JAR. */
+  /** Returns path to initialization script within the plugin's JAR. */
   public abstract String getJavaScriptResourcePath();
 
   @Override
diff --git a/java/com/google/gerrit/git/GitUpdateFailureException.java b/java/com/google/gerrit/git/GitUpdateFailureException.java
index 76ef217..7fcb828 100644
--- a/java/com/google/gerrit/git/GitUpdateFailureException.java
+++ b/java/com/google/gerrit/git/GitUpdateFailureException.java
@@ -46,12 +46,12 @@
             .collect(toImmutableList());
   }
 
-  /** @return the names of the refs for which the update failed. */
+  /** Returns the names of the refs for which the update failed. */
   public ImmutableList<String> getFailedRefs() {
     return failures.stream().map(GitUpdateFailure::ref).collect(toImmutableList());
   }
 
-  /** @return the failures that caused this exception. */
+  /** Returns the failures that caused this exception. */
   @UsedAt(UsedAt.Project.GOOGLE)
   public ImmutableList<GitUpdateFailure> getFailures() {
     return failures;
diff --git a/java/com/google/gerrit/gpg/CheckResult.java b/java/com/google/gerrit/gpg/CheckResult.java
index 8655b2a..2743e74 100644
--- a/java/com/google/gerrit/gpg/CheckResult.java
+++ b/java/com/google/gerrit/gpg/CheckResult.java
@@ -62,22 +62,22 @@
     this.problems = problems;
   }
 
-  /** @return whether the result has status {@link Status#OK} or better. */
+  /** Returns whether the result has status {@link Status#OK} or better. */
   public boolean isOk() {
     return status.compareTo(Status.OK) >= 0;
   }
 
-  /** @return whether the result has status {@link Status#TRUSTED} or better. */
+  /** Returns whether the result has status {@link Status#TRUSTED} or better. */
   public boolean isTrusted() {
     return status.compareTo(Status.TRUSTED) >= 0;
   }
 
-  /** @return the status enum value associated with the object. */
+  /** Returns the status enum value associated with the object. */
   public Status getStatus() {
     return status;
   }
 
-  /** @return any problems encountered during checking. */
+  /** Returns any problems encountered during checking. */
   public List<String> getProblems() {
     return problems;
   }
diff --git a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 9477cb6..71dff97 100644
--- a/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.UrlFormatter;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -62,17 +63,20 @@
     private final IdentifiedUser.GenericFactory userFactory;
     private final int maxTrustDepth;
     private final ImmutableMap<Long, Fingerprint> trusted;
+    private final ExternalIdKeyFactory externalIdKeyFactory;
 
     @Inject
     Factory(
         @GerritServerConfig Config cfg,
         Provider<InternalAccountQuery> accountQueryProvider,
         IdentifiedUser.GenericFactory userFactory,
-        DynamicItem<UrlFormatter> urlFormatter) {
+        DynamicItem<UrlFormatter> urlFormatter,
+        ExternalIdKeyFactory externalIdKeyFactory) {
       this.accountQueryProvider = accountQueryProvider;
       this.urlFormatter = urlFormatter;
       this.userFactory = userFactory;
       this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0);
+      this.externalIdKeyFactory = externalIdKeyFactory;
 
       String[] strs = cfg.getStringList("receive", null, "trustedKey");
       if (strs.length != 0) {
@@ -103,6 +107,7 @@
   private final Provider<InternalAccountQuery> accountQueryProvider;
   private final DynamicItem<UrlFormatter> urlFormatter;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   private IdentifiedUser expectedUser;
 
@@ -113,6 +118,7 @@
     if (factory.trusted != null) {
       enableTrust(factory.maxTrustDepth, factory.trusted);
     }
+    this.externalIdKeyFactory = factory.externalIdKeyFactory;
   }
 
   /**
@@ -247,7 +253,8 @@
     return sb.toString();
   }
 
-  static ExternalId.Key toExtIdKey(PGPPublicKey key) {
-    return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
+  ExternalId.Key toExtIdKey(PGPPublicKey key) {
+    return externalIdKeyFactory.create(
+        SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
   }
 }
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 82b3892..36a4af7 100644
--- a/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -154,8 +154,11 @@
   protected abstract Repository getRepository() throws IOException;
 
   /**
+   * Specifies whether this repository should be closed before returning froms {@link
+   * #check(PushCertificate)}
+   *
    * @param repo a repository previously returned by {@link #getRepository()}.
-   * @return whether this repository should be closed before returning from {@link
+   * @return true if this repository should be closed before returning from {@link
    *     #check(PushCertificate)}.
    */
   protected abstract boolean shouldClose(Repository repo);
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 1be37f5..e0c921d 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.UserInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.mail.send.DeleteKeySender;
 import com.google.inject.Inject;
@@ -53,6 +54,7 @@
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final ExternalIds externalIds;
   private final DeleteKeySender.Factory deleteKeySenderFactory;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
   DeleteGpgKey(
@@ -60,12 +62,14 @@
       Provider<PublicKeyStore> storeProvider,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       ExternalIds externalIds,
-      DeleteKeySender.Factory deleteKeySenderFactory) {
+      DeleteKeySender.Factory deleteKeySenderFactory,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.serverIdent = serverIdent;
     this.storeProvider = storeProvider;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.externalIds = externalIds;
     this.deleteKeySenderFactory = deleteKeySenderFactory;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   @Override
@@ -73,7 +77,8 @@
       throws RestApiException, PGPException, IOException, ConfigInvalidException {
     PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
     String fingerprint = BaseEncoding.base16().encode(key.getFingerprint());
-    Optional<ExternalId> extId = externalIds.get(ExternalId.Key.create(SCHEME_GPGKEY, fingerprint));
+    Optional<ExternalId> extId =
+        externalIds.get(externalIdKeyFactory.create(SCHEME_GPGKEY, fingerprint));
     if (!extId.isPresent()) {
       throw new ResourceNotFoundException(fingerprint);
     }
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 1b5e06a..d46b344 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -53,6 +53,8 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.mail.send.AddKeySender;
 import com.google.gerrit.server.mail.send.DeleteKeySender;
@@ -93,6 +95,8 @@
   private final ExternalIds externalIds;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final RetryHelper retryHelper;
+  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
   PostGpgKeys(
@@ -105,7 +109,9 @@
       Provider<InternalAccountQuery> accountQueryProvider,
       ExternalIds externalIds,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
-      RetryHelper retryHelper) {
+      RetryHelper retryHelper,
+      ExternalIdFactory externalIdFactory,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.serverIdent = serverIdent;
     this.self = self;
     this.storeProvider = storeProvider;
@@ -116,6 +122,8 @@
     this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.retryHelper = retryHelper;
+    this.externalIdFactory = externalIdFactory;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   @Override
@@ -140,7 +148,7 @@
             throw new ResourceConflictException("GPG key already associated with another account");
           }
         } else {
-          newExtIds.add(ExternalId.create(extIdKey, rsrc.getUser().getAccountId()));
+          newExtIds.add(externalIdFactory.create(extIdKey, rsrc.getUser().getAccountId()));
         }
       }
 
@@ -287,7 +295,7 @@
   }
 
   private ExternalId.Key toExtIdKey(byte[] fp) {
-    return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
+    return externalIdKeyFactory.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
   }
 
   private Account getAccountByExternalId(ExternalId.Key extIdKey) {
diff --git a/java/com/google/gerrit/gpg/testing/TestKeys.java b/java/com/google/gerrit/gpg/testing/TestKeys.java
index de66889..0423474 100644
--- a/java/com/google/gerrit/gpg/testing/TestKeys.java
+++ b/java/com/google/gerrit/gpg/testing/TestKeys.java
@@ -436,13 +436,13 @@
   /**
    * A key with an additional user ID.
    *
-   * <pre>
+   * <pre>{@code
    * pub   2048R/98C51DBF 2015-07-30
    *       Key fingerprint = 42B3 294D 1924 D7EB AF4A  A99F 5024 BB44 98C5 1DBF
    * uid                  foo:myId
    * uid                  Testuser Five <test5@example.com>
    * sub   2048R/C781A9E3 2015-07-30
-   * </pre>
+   * }</pre>
    */
   public static TestKey validKeyWithSecondUserId() {
     return new TestKey(
@@ -1033,13 +1033,13 @@
   /**
    * Master Key without expiration with subkey with expiration.
    *
-   * <pre>
+   * <pre>{@code
    * pub   rsa1024 2018-11-17 [C]
    *       5734 2C37 982A 843B 19C0  622B 6AAF 2D26 B481 02DB
    * uid            [ultimate] Testuser 10 <testuser10@example.com>
    * sub   rsa1024 2018-11-17 [S] [expires: 2065-11-05]
    *       0A4A 9660 1B96 2DFC E898  E686 4305 C92E 626E B485
-   * </pre>
+   * }</pre>
    */
   public static TestKey validKeyWithoutExpirationWithSubkeyWithExpiration() throws Exception {
     return new TestKey(
diff --git a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index de989ac..a421139 100644
--- a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -76,17 +76,23 @@
   private final AccountCache accountCache;
   private final AccountManager accountManager;
   private final AuthConfig authConfig;
+  private final AuthRequest.Factory authRequestFactory;
+  private final PasswordVerifier passwordVerifier;
 
   @Inject
   ProjectBasicAuthFilter(
       DynamicItem<WebSession> session,
       AccountCache accountCache,
       AccountManager accountManager,
-      AuthConfig authConfig) {
+      AuthConfig authConfig,
+      AuthRequest.Factory authRequestFactory,
+      PasswordVerifier passwordVerifier) {
     this.session = session;
     this.accountCache = accountCache;
     this.accountManager = accountManager;
     this.authConfig = authConfig;
+    this.authRequestFactory = authRequestFactory;
+    this.passwordVerifier = passwordVerifier;
   }
 
   @Override
@@ -155,7 +161,7 @@
     GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
         || gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
-      if (PasswordVerifier.checkPassword(who.externalIds(), username, password)) {
+      if (passwordVerifier.checkPassword(who.externalIds(), username, password)) {
         logger.atFine().log(
             "HTTP:%s %s username/password authentication succeeded",
             req.getMethod(), req.getRequestURI());
@@ -167,7 +173,7 @@
       return failAuthentication(rsp, username, req);
     }
 
-    AuthRequest whoAuth = AuthRequest.forUser(username);
+    AuthRequest whoAuth = authRequestFactory.createForUser(username);
     whoAuth.setPassword(password);
 
     try {
@@ -177,7 +183,7 @@
           "HTTP:%s %s Realm authentication succeeded", req.getMethod(), req.getRequestURI());
       return true;
     } catch (NoSuchUserException e) {
-      if (PasswordVerifier.checkPassword(who.externalIds(), username, password)) {
+      if (passwordVerifier.checkPassword(who.externalIds(), username, password)) {
         return succeedAuthentication(who, null);
       }
       logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index dab36c4..fa53053 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -76,6 +76,7 @@
   private final AccountManager accountManager;
   private final String gitOAuthProvider;
   private final boolean userNameToLowerCase;
+  private final AuthRequest.Factory authRequestFactory;
 
   private String defaultAuthPlugin;
   private String defaultAuthProvider;
@@ -86,13 +87,15 @@
       DynamicMap<OAuthLoginProvider> pluginsProvider,
       AccountCache accountCache,
       AccountManager accountManager,
-      @GerritServerConfig Config gerritConfig) {
+      @GerritServerConfig Config gerritConfig,
+      AuthRequest.Factory authRequestFactory) {
     this.session = session;
     this.loginProviders = pluginsProvider;
     this.accountCache = accountCache;
     this.accountManager = accountManager;
     this.gitOAuthProvider = gerritConfig.getString("auth", null, "gitOAuthProvider");
     this.userNameToLowerCase = gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -162,7 +165,7 @@
     }
 
     Account account = who.get().account();
-    AuthRequest authRequest = AuthRequest.forExternalUser(authInfo.username);
+    AuthRequest authRequest = authRequestFactory.createForExternalUser(authInfo.username);
     authRequest.setEmailAddress(account.preferredEmail());
     authRequest.setDisplayName(account.fullName());
     authRequest.setPassword(authInfo.tokenOrSecret);
diff --git a/java/com/google/gerrit/httpd/RequestMetricsFilter.java b/java/com/google/gerrit/httpd/RequestMetricsFilter.java
index c97b9ad..0ff1a79 100644
--- a/java/com/google/gerrit/httpd/RequestMetricsFilter.java
+++ b/java/com/google/gerrit/httpd/RequestMetricsFilter.java
@@ -55,17 +55,17 @@
       startedMemory = threadMxBean.getCurrentThreadAllocatedBytes();
     }
 
-    /** @return total CPU time in milliseconds for executing request */
+    /** Returns total CPU time in milliseconds for executing request */
     public long getTotalCpuTime() {
       return (threadMxBean.getCurrentThreadCpuTime() - startedTotalCpu) / 1_000_000;
     }
 
-    /** @return CPU time in user mode in milliseconds for executing request */
+    /** Returns CPU time in user mode in milliseconds for executing request */
     public long getUserCpuTime() {
       return (threadMxBean.getCurrentThreadUserTime() - startedUserCpu) / 1_000_000;
     }
 
-    /** @return memory allocated in bytes for executing request */
+    /** Returns memory allocated in bytes for executing request */
     public long getAllocatedMemory() {
       return startedMemory == -1
           ? -1
diff --git a/java/com/google/gerrit/httpd/WebModule.java b/java/com/google/gerrit/httpd/WebModule.java
index e416075..0645aac 100644
--- a/java/com/google/gerrit/httpd/WebModule.java
+++ b/java/com/google/gerrit/httpd/WebModule.java
@@ -75,6 +75,10 @@
     listener().toInstance(registerInParentInjectors());
 
     install(UniversalWebLoginFilter.module());
+
+    // Static injection was unfortunately the best solution in this place. However, it is to be
+    // avoided if possible.
+    requestStaticInjection(WebSessionManager.Val.class);
   }
 
   private void installAuthModule() {
diff --git a/java/com/google/gerrit/httpd/WebSessionManager.java b/java/com/google/gerrit/httpd/WebSessionManager.java
index c0900ec..87bf3a6 100644
--- a/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -32,6 +32,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -186,6 +187,8 @@
   public static final class Val implements Serializable {
     static final long serialVersionUID = 2L;
 
+    @Inject private static transient ExternalIdKeyFactory externalIdKeyFactory;
+
     private transient Account.Id accountId;
     private transient long refreshCookieAt;
     private transient boolean persistentCookie;
@@ -295,7 +298,7 @@
             persistentCookie = readVarInt32(in) != 0;
             continue;
           case 4:
-            externalId = ExternalId.Key.parse(readString(in));
+            externalId = externalIdKeyFactory.parse(readString(in));
             continue;
           case 5:
             sessionId = readString(in);
diff --git a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 97bb44b..2f760f0 100644
--- a/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -31,7 +31,7 @@
 import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
 import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
@@ -62,6 +62,8 @@
   private final AccountManager accountManager;
   private final SiteHeaderFooter headers;
   private final Provider<InternalAccountQuery> queryProvider;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   BecomeAnyAccountLoginServlet(
@@ -70,13 +72,17 @@
       AccountCache ac,
       AccountManager am,
       SiteHeaderFooter shf,
-      Provider<InternalAccountQuery> qp) {
+      Provider<InternalAccountQuery> qp,
+      ExternalIdKeyFactory eikf,
+      AuthRequest.Factory arf) {
     webSession = ws;
     accounts = a;
     accountCache = ac;
     accountManager = am;
     headers = shf;
     queryProvider = qp;
+    externalIdKeyFactory = eikf;
+    authRequestFactory = arf;
   }
 
   @Override
@@ -220,7 +226,8 @@
   private AuthResult create() throws IOException {
     try {
       return accountManager.authenticate(
-          new AuthRequest(ExternalId.Key.create(SCHEME_UUID, UUID.randomUUID().toString())));
+          authRequestFactory.create(
+              externalIdKeyFactory.create(SCHEME_UUID, UUID.randomUUID().toString())));
     } catch (AccountException e) {
       getServletContext().log("cannot create new account", e);
       return null;
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index e20c9b9..acb3282 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.httpd.RemoteUserUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.CacheHeaders;
 import com.google.gerrit.util.http.RequestUtil;
@@ -64,10 +65,16 @@
   private final String emailHeader;
   private final String externalIdHeader;
   private final boolean userNameToLowerCase;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
-  HttpAuthFilter(DynamicItem<WebSession> webSession, AuthConfig authConfig) throws IOException {
+  HttpAuthFilter(
+      DynamicItem<WebSession> webSession,
+      AuthConfig authConfig,
+      ExternalIdKeyFactory externalIdKeyFactory)
+      throws IOException {
     this.sessionProvider = webSession;
+    this.externalIdKeyFactory = externalIdKeyFactory;
 
     final String pageName = "LoginRedirect.html";
     final String doc = HtmlDomUtil.readFile(getClass(), pageName);
@@ -124,9 +131,9 @@
     return false;
   }
 
-  private static boolean correctUser(String user, WebSession session) {
+  private boolean correctUser(String user, WebSession session) {
     Optional<ExternalId.Key> id = session.getUser().getLastLoginExternalIdKey();
-    return id.map(i -> i.equals(ExternalId.Key.create(SCHEME_GERRIT, user))).orElse(false);
+    return id.map(i -> i.equals(externalIdKeyFactory.create(SCHEME_GERRIT, user))).orElse(false);
   }
 
   String getRemoteUser(HttpServletRequest req) {
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 1b7e477..53f33b5 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
@@ -61,6 +61,8 @@
   private final AccountManager accountManager;
   private final HttpAuthFilter authFilter;
   private final AuthConfig authConfig;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   HttpLoginServlet(
@@ -68,12 +70,16 @@
       final CanonicalWebUrl urlProvider,
       final AccountManager accountManager,
       final HttpAuthFilter authFilter,
-      final AuthConfig authConfig) {
+      final AuthConfig authConfig,
+      final ExternalIdKeyFactory externalIdKeyFactory,
+      final AuthRequest.Factory authRequestFactory) {
     this.webSession = webSession;
     this.urlProvider = urlProvider;
     this.accountManager = accountManager;
     this.authFilter = authFilter;
     this.authConfig = authConfig;
+    this.externalIdKeyFactory = externalIdKeyFactory;
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -109,7 +115,7 @@
       return;
     }
 
-    final AuthRequest areq = AuthRequest.forUser(user);
+    final AuthRequest areq = authRequestFactory.createForUser(user);
     areq.setDisplayName(authFilter.getRemoteDisplayname(req));
     areq.setEmailAddress(authFilter.getRemoteEmail(req));
     final AuthResult arsp;
@@ -154,7 +160,7 @@
       throws AccountException, IOException, ConfigInvalidException {
     accountManager.updateLink(
         arsp.getAccountId(),
-        new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, remoteAuthToken)));
+        authRequestFactory.create(externalIdKeyFactory.create(SCHEME_EXTERNAL, remoteAuthToken)));
   }
 
   private void replace(Document doc, String name, String value) {
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index 40807c0..820c7a2 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -42,12 +42,16 @@
 
   private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   HttpsClientSslCertAuthFilter(
-      final DynamicItem<WebSession> webSession, AccountManager accountManager) {
+      final DynamicItem<WebSession> webSession,
+      AccountManager accountManager,
+      final AuthRequest.Factory authRequestFactory) {
     this.webSession = webSession;
     this.accountManager = accountManager;
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -70,7 +74,7 @@
     } else {
       throw new ServletException("Couldn't extract username from your certificate");
     }
-    final AuthRequest areq = AuthRequest.forUser(userName);
+    final AuthRequest areq = authRequestFactory.createForUser(userName);
     final AuthResult arsp;
     try {
       arsp = accountManager.authenticate(areq);
diff --git a/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index a09866e..6caa760 100644
--- a/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -56,17 +56,20 @@
   private final DynamicItem<WebSession> webSession;
   private final CanonicalWebUrl urlProvider;
   private final SiteHeaderFooter headers;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   LdapLoginServlet(
       AccountManager accountManager,
       DynamicItem<WebSession> webSession,
       CanonicalWebUrl urlProvider,
-      SiteHeaderFooter headers) {
+      SiteHeaderFooter headers,
+      AuthRequest.Factory authRequestFactory) {
     this.accountManager = accountManager;
     this.webSession = webSession;
     this.urlProvider = urlProvider;
     this.headers = headers;
+    this.authRequestFactory = authRequestFactory;
   }
 
   private void sendForm(
@@ -115,7 +118,7 @@
       return;
     }
 
-    AuthRequest areq = AuthRequest.forUser(username);
+    AuthRequest areq = authRequestFactory.createForUser(username);
     areq.setPassword(password);
 
     AuthResult ares;
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index 70ed79b..a3f8fbda 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -35,7 +35,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
@@ -65,6 +65,8 @@
   private Account.Id accountId;
   private String redirectToken;
   private boolean linkMode;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   OAuthSession(
@@ -72,13 +74,17 @@
       Provider<IdentifiedUser> identifiedUser,
       AccountManager accountManager,
       CanonicalWebUrl urlProvider,
-      OAuthTokenCache tokenCache) {
+      OAuthTokenCache tokenCache,
+      ExternalIdKeyFactory externalIdKeyFactory,
+      AuthRequest.Factory authRequestFactory) {
     this.state = generateRandomState();
     this.identifiedUser = identifiedUser;
     this.webSession = webSession;
     this.accountManager = accountManager;
     this.urlProvider = urlProvider;
     this.tokenCache = tokenCache;
+    this.externalIdKeyFactory = externalIdKeyFactory;
+    this.authRequestFactory = authRequestFactory;
   }
 
   boolean isLoggedIn() {
@@ -126,7 +132,7 @@
 
   private void authenticateAndRedirect(
       HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException {
-    AuthRequest areq = new AuthRequest(ExternalId.Key.parse(user.getExternalId()));
+    AuthRequest areq = authRequestFactory.create(externalIdKeyFactory.parse(user.getExternalId()));
     AuthResult arsp;
     try {
       String claimedIdentifier = user.getClaimedIdentity();
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index b987c68..df0062c 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -32,8 +32,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
@@ -63,18 +64,24 @@
   private OAuthUserInfo user;
   private String redirectToken;
   private boolean linkMode;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   OAuthSessionOverOpenID(
       DynamicItem<WebSession> webSession,
       Provider<IdentifiedUser> identifiedUser,
       AccountManager accountManager,
-      CanonicalWebUrl urlProvider) {
+      CanonicalWebUrl urlProvider,
+      ExternalIdKeyFactory externalIdKeyFactory,
+      AuthRequest.Factory authRequestFactory) {
     this.state = generateRandomState();
     this.webSession = webSession;
     this.identifiedUser = identifiedUser;
     this.accountManager = accountManager;
     this.urlProvider = urlProvider;
+    this.externalIdKeyFactory = externalIdKeyFactory;
+    this.authRequestFactory = authRequestFactory;
   }
 
   boolean isLoggedIn() {
@@ -117,8 +124,7 @@
   private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     com.google.gerrit.server.account.AuthRequest areq =
-        new com.google.gerrit.server.account.AuthRequest(
-            ExternalId.Key.parse(user.getExternalId()));
+        authRequestFactory.create(externalIdKeyFactory.parse(user.getExternalId()));
     AuthResult arsp;
     try {
       String claimedIdentifier = user.getClaimedIdentity();
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index b685011..cf3562f 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -92,6 +92,8 @@
   private final ConsumerManager manager;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
   private final List<String> openIdDomains;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+  private final com.google.gerrit.server.account.AuthRequest.Factory authRequestFactory;
 
   /** Maximum age, in seconds, before forcing re-authentication of account. */
   private final int papeMaxAuthAge;
@@ -104,7 +106,9 @@
       @GerritServerConfig Config config,
       AuthConfig ac,
       AccountManager am,
-      ProxyProperties proxyProperties) {
+      ProxyProperties proxyProperties,
+      ExternalIdKeyFactory externalIdKeyFactory,
+      com.google.gerrit.server.account.AuthRequest.Factory authRequestFactory) {
 
     if (proxyProperties.getProxyUrl() != null) {
       final org.openid4java.util.ProxyProperties proxy = new org.openid4java.util.ProxyProperties();
@@ -132,6 +136,8 @@
                 "maxOpenIdSessionAge",
                 -1,
                 TimeUnit.SECONDS);
+    this.externalIdKeyFactory = externalIdKeyFactory;
+    this.authRequestFactory = authRequestFactory;
   }
 
   @SuppressWarnings("unchecked")
@@ -310,7 +316,7 @@
     }
 
     final com.google.gerrit.server.account.AuthRequest areq =
-        new com.google.gerrit.server.account.AuthRequest(ExternalId.Key.parse(openidIdentifier));
+        authRequestFactory.create(externalIdKeyFactory.parse(openidIdentifier));
 
     if (sregRsp != null) {
       areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
@@ -388,8 +394,7 @@
         // was missing due to a bug in Gerrit. Link the claimed.
         //
         final com.google.gerrit.server.account.AuthRequest linkReq =
-            new com.google.gerrit.server.account.AuthRequest(
-                ExternalId.Key.parse(claimedIdentifier));
+            authRequestFactory.create(externalIdKeyFactory.parse(claimedIdentifier));
         linkReq.setDisplayName(areq.getDisplayName());
         linkReq.setEmailAddress(areq.getEmailAddress());
         accountManager.link(actualId.get(), linkReq);
@@ -425,8 +430,7 @@
           webSession.get().login(arsp, remember);
           if (arsp.isNew() && claimedIdentifier != null) {
             final com.google.gerrit.server.account.AuthRequest linkReq =
-                new com.google.gerrit.server.account.AuthRequest(
-                    ExternalId.Key.parse(claimedIdentifier));
+                authRequestFactory.create(externalIdKeyFactory.parse(claimedIdentifier));
             linkReq.setDisplayName(areq.getDisplayName());
             linkReq.setEmailAddress(areq.getEmailAddress());
             accountManager.link(arsp.getAccountId(), linkReq);
diff --git a/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 3ab409e..315c9c8 100644
--- a/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -56,9 +56,18 @@
 
 public class ParameterParser {
   public static final String TRACE_PARAMETER = "trace";
+  public static final String EXPERIMENT_PARAMETER = "experiment";
 
   private static final ImmutableSet<String> RESERVED_KEYS =
-      ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields", TRACE_PARAMETER);
+      ImmutableSet.of(
+          "pp",
+          "prettyPrint",
+          "strict",
+          "callback",
+          "alt",
+          "fields",
+          TRACE_PARAMETER,
+          EXPERIMENT_PARAMETER);
 
   @AutoValue
   public abstract static class QueryParams {
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index c65d0b5..369ea29 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -359,15 +359,11 @@
     ViewData viewData = null;
 
     try (TraceContext traceContext = enableTracing(req, res)) {
-      List<IdString> path = splitPath(req);
-      RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
+      String requestUri = requestUri(req);
 
-      try (RequestStateContext requestStateContext =
-              RequestStateContext.open()
-                  .addRequestStateProvider(
-                      globals.deadlineCheckerFactory.create(
-                          requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
-          PerThreadCache ignored = PerThreadCache.create()) {
+      try (PerThreadCache ignored = PerThreadCache.create()) {
+        List<IdString> path = splitPath(req);
+        RequestInfo requestInfo = createRequestInfo(traceContext, requestUri, path);
         globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
 
         // It's important that the PerformanceLogContext is closed before the response is sent to
@@ -375,8 +371,13 @@
         // plugins happens before the client sees the response. This is needed for being able to
         // test performance logging from an acceptance test (see
         // TraceIT#performanceLoggingForRestCall()).
-        try (PerformanceLogContext performanceLogContext =
-            new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
+        try (RequestStateContext requestStateContext =
+                RequestStateContext.open()
+                    .addRequestStateProvider(
+                        globals.deadlineCheckerFactory.create(
+                            requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
+            PerformanceLogContext performanceLogContext =
+                new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
           traceRequestData(req);
 
           if (isCorsPreflight(req)) {
@@ -741,7 +742,8 @@
         if (requestCancelledException.isPresent()) {
           RequestStateProvider.Reason cancellationReason =
               requestCancelledException.get().getCancellationReason();
-          globals.cancellationMetrics.countCancelledRequest(requestInfo, cancellationReason);
+          globals.cancellationMetrics.countCancelledRequest(
+              RequestInfo.RequestType.REST, requestUri, cancellationReason);
           statusCode = getCancellationStatusCode(cancellationReason);
           responseBytes =
               replyError(
@@ -766,13 +768,12 @@
             }
           } else {
             res.reset();
-            traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+            TraceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
 
             if (status.isPresent()) {
-              responseBytes = reply(req, res, e, status.get(), getUserMessages(traceContext, e));
+              responseBytes = reply(req, res, e, status.get(), getUserMessages(e));
             } else {
-              responseBytes =
-                  replyInternalServerError(req, res, e, getUserMessages(traceContext, e));
+              responseBytes = replyInternalServerError(req, res, e, getUserMessages(e));
             }
           }
         }
@@ -982,7 +983,7 @@
       throws Exception {
     RetryableAction<T> retryableAction = globals.retryHelper.action(actionType, caller, action);
     AtomicReference<Optional<String>> traceId = new AtomicReference<>(Optional.empty());
-    if (!traceContext.isTracing()) {
+    if (!TraceContext.isTracing()) {
       // enable automatic retry with tracing in case of non-recoverable failure
       retryableAction
           .retryWithTrace(t -> !(t instanceof RestApiException))
@@ -1425,7 +1426,6 @@
    * @param config config parameters for the JSON formatting
    * @param result the object that should be formatted as JSON
    * @return the length of the response
-   * @throws IOException
    */
   public static long replyJson(
       @Nullable HttpServletRequest req,
@@ -1815,6 +1815,10 @@
     logger.atFinest().log(
         "Received REST request: %s %s (parameters: %s)",
         req.getMethod(), req.getRequestURI(), getParameterNames(req));
+    Optional.ofNullable(req.getHeader(X_GERRIT_DEADLINE))
+        .ifPresent(
+            clientProvidedDeadline ->
+                logger.atFine().log("%s = %s", X_GERRIT_DEADLINE, clientProvidedDeadline));
     logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
     logger.atFinest().log(
         "Groups: %s", lazy(() -> globals.currentUser.get().getEffectiveGroups().getKnownGroups()));
@@ -1870,9 +1874,9 @@
         .findFirst();
   }
 
-  private ImmutableList<String> getUserMessages(TraceContext traceContext, Throwable err) {
+  private ImmutableList<String> getUserMessages(Throwable err) {
     return globals.exceptionHooks.stream()
-        .flatMap(h -> h.getUserMessages(err, traceContext.getTraceId().orElse(null)).stream())
+        .flatMap(h -> h.getUserMessages(err, TraceContext.getTraceId().orElse(null)).stream())
         .collect(toImmutableList());
   }
 
@@ -1965,7 +1969,6 @@
    *     set to {@code true} if the reply may contain sensitive data
    * @param text the text reply
    * @return the length of the response
-   * @throws IOException
    */
   static long replyText(
       @Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
diff --git a/java/com/google/gerrit/index/FieldDef.java b/java/com/google/gerrit/index/FieldDef.java
index eb64c1d..76aa7cc 100644
--- a/java/com/google/gerrit/index/FieldDef.java
+++ b/java/com/google/gerrit/index/FieldDef.java
@@ -138,17 +138,17 @@
     return name;
   }
 
-  /** @return name of the field. */
+  /** Returns name of the field. */
   public String getName() {
     return name;
   }
 
-  /** @return type of the field; for repeatable fields, the inner type, not the iterable type. */
+  /** Returns type of the field; for repeatable fields, the inner type, not the iterable type. */
   public FieldType<?> getType() {
     return type;
   }
 
-  /** @return whether the field should be stored in the index. */
+  /** Returns whether the field should be stored in the index. */
   public boolean isStored() {
     return stored;
   }
@@ -203,7 +203,7 @@
     return false;
   }
 
-  /** @return whether the field is repeatable. */
+  /** Returns whether the field is repeatable. */
   public boolean isRepeatable() {
     return repeatable;
   }
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index 529cd78..ead302d 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -33,7 +33,7 @@
  * <p>Implementations must be thread-safe and should batch inserts/updates where appropriate.
  */
 public interface Index<K, V> {
-  /** @return the schema version used by this index. */
+  /** Returns the schema version used by this index. */
   Schema<V> getSchema();
 
   /** Close this index. */
diff --git a/java/com/google/gerrit/index/IndexConfig.java b/java/com/google/gerrit/index/IndexConfig.java
index 29b8ea6..8676fb2 100644
--- a/java/com/google/gerrit/index/IndexConfig.java
+++ b/java/com/google/gerrit/index/IndexConfig.java
@@ -101,27 +101,27 @@
   }
 
   /**
-   * @return maximum limit supported by the underlying index, or limited for performance reasons.
+   * Returns maximum limit supported by the underlying index, or limited for performance reasons.
    */
   public abstract int maxLimit();
 
   /**
-   * @return maximum number of pages (limit / start) supported by the underlying index, or limited
-   *     for performance reasons.
+   * Returns maximum number of pages (limit / start) supported by the underlying index, or limited
+   * for performance reasons.
    */
   public abstract int maxPages();
 
   /**
-   * @return maximum number of total index query terms supported by the underlying index, or limited
-   *     for performance reasons.
+   * Returns maximum number of total index query terms supported by the underlying index, or limited
+   * for performance reasons.
    */
   public abstract int maxTerms();
 
-  /** @return index type. */
+  /** Returns index type. */
   public abstract String type();
 
   /**
-   * @return whether different subsets of changes may be stored in different physical sub-indexes.
+   * Returns whether different subsets of changes may be stored in different physical sub-indexes.
    */
   public abstract boolean separateChangeSubIndexes();
 }
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 3aa9de0..91c3f70 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -134,7 +134,7 @@
     return fields;
   }
 
-  /** @return all fields in this schema where {@link FieldDef#isStored()} is true. */
+  /** Returns all fields in this schema where {@link FieldDef#isStored()} is true. */
   public final ImmutableMap<String, FieldDef<T, ?>> getStoredFields() {
     return storedFields;
   }
diff --git a/java/com/google/gerrit/index/query/DataSource.java b/java/com/google/gerrit/index/query/DataSource.java
index 2c2ba53..518d153 100644
--- a/java/com/google/gerrit/index/query/DataSource.java
+++ b/java/com/google/gerrit/index/query/DataSource.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.index.query;
 
 public interface DataSource<T> {
-  /** @return an estimate of the number of results from {@link #read()}. */
+  /** Returns an estimate of the number of results from {@link #read()}. */
   int getCardinality();
 
-  /** @return read from the database and return the results. */
+  /** Returns read from the database and return the results. */
   ResultSet<T> read();
 
-  /** @return read from the database and return the raw results. */
+  /** Returns read from the database and return the raw results. */
   ResultSet<FieldBundle> readRaw();
 }
diff --git a/java/com/google/gerrit/index/query/Matchable.java b/java/com/google/gerrit/index/query/Matchable.java
index 7a16ae8..f416149 100644
--- a/java/com/google/gerrit/index/query/Matchable.java
+++ b/java/com/google/gerrit/index/query/Matchable.java
@@ -18,6 +18,6 @@
   /** Does this predicate match this object? */
   boolean match(T object);
 
-  /** @return a cost estimate to run this predicate, higher figures cost more. */
+  /** Returns a cost estimate to run this predicate, higher figures cost more. */
   int getCost();
 }
diff --git a/java/com/google/gerrit/index/query/Predicate.java b/java/com/google/gerrit/index/query/Predicate.java
index 2382d30..2791f2c 100644
--- a/java/com/google/gerrit/index/query/Predicate.java
+++ b/java/com/google/gerrit/index/query/Predicate.java
@@ -159,7 +159,7 @@
     return (Matchable<T>) this;
   }
 
-  /** @return a cost estimate to run this predicate, higher figures cost more. */
+  /** Returns a cost estimate to run this predicate, higher figures cost more. */
   public int estimateCost() {
     if (!isMatchable()) {
       return 1;
diff --git a/java/com/google/gerrit/index/query/QueryResult.java b/java/com/google/gerrit/index/query/QueryResult.java
index 33fcef0..d03a68b 100644
--- a/java/com/google/gerrit/index/query/QueryResult.java
+++ b/java/com/google/gerrit/index/query/QueryResult.java
@@ -34,19 +34,19 @@
     return new AutoValue_QueryResult<>(query, predicate, ImmutableList.copyOf(entities), more);
   }
 
-  /** @return the original query string, or null if the query was created programmatically. */
+  /** Returns the original query string, or null if the query was created programmatically. */
   @Nullable
   public abstract String query();
 
-  /** @return the predicate after all rewriting and other modification by the query subsystem. */
+  /** Returns the predicate after all rewriting and other modification by the query subsystem. */
   public abstract Predicate<T> predicate();
 
-  /** @return the query results. */
+  /** Returns the query results. */
   public abstract ImmutableList<T> entities();
 
   /**
-   * @return whether the query could be retried with a higher start/limit to produce more results.
-   *     Never true if {@link #entities()} is empty.
+   * Returns whether the query could be retried with a higher start/limit to produce more results.
+   * Never true if {@link #entities()} is empty.
    */
   public abstract boolean more();
 }
diff --git a/java/com/google/gerrit/json/OutputFormat.java b/java/com/google/gerrit/json/OutputFormat.java
index 3e7c319..c5504bb 100644
--- a/java/com/google/gerrit/json/OutputFormat.java
+++ b/java/com/google/gerrit/json/OutputFormat.java
@@ -42,12 +42,12 @@
    */
   JSON_COMPACT;
 
-  /** @return true when the format is either JSON or JSON_COMPACT. */
+  /** Returns true when the format is either JSON or JSON_COMPACT. */
   public boolean isJson() {
     return this == JSON_COMPACT || this == JSON;
   }
 
-  /** @return a new Gson instance configured according to the format. */
+  /** Returns a new Gson instance configured according to the format. */
   public GsonBuilder newGsonBuilder() {
     if (!isJson()) {
       throw new IllegalStateException(String.format("%s is not JSON", this));
@@ -63,7 +63,7 @@
     return gb;
   }
 
-  /** @return a new Gson instance configured according to the format. */
+  /** Returns a new Gson instance configured according to the format. */
   public Gson newGson() {
     return newGsonBuilder().create();
   }
diff --git a/java/com/google/gerrit/lifecycle/LifecycleModule.java b/java/com/google/gerrit/lifecycle/LifecycleModule.java
index 0fb4653..efe1518 100644
--- a/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -24,13 +24,16 @@
 /** Module to support registering a unique LifecyleListener. */
 public abstract class LifecycleModule extends FactoryModule {
   /**
-   * @return a unique listener binding.
-   *     <p>To create a listener binding use:
-   *     <pre>
+   * Returns a unique listener binding.
+   *
+   * <p>To create a listener binding use:
+   *
+   * <pre>
    * listener().to(MyListener.class);
    * </pre>
-   *     where {@code MyListener} is a {@link Singleton} implementing the {@link LifecycleListener}
-   *     interface.
+   *
+   * where {@code MyListener} is a {@link Singleton} implementing the {@link LifecycleListener}
+   * interface.
    */
   protected LinkedBindingBuilder<LifecycleListener> listener() {
     final Annotation id = UniqueAnnotations.create();
diff --git a/java/com/google/gerrit/mail/HtmlParser.java b/java/com/google/gerrit/mail/HtmlParser.java
index ba73bdd..97fe06e 100644
--- a/java/com/google/gerrit/mail/HtmlParser.java
+++ b/java/com/google/gerrit/mail/HtmlParser.java
@@ -143,7 +143,7 @@
           if (!Strings.isNullOrEmpty(content)) {
             ParserUtil.appendOrAddNewComment(
                 new MailComment(
-                    content, null, null, MailComment.CommentType.CHANGE_MESSAGE, isLink),
+                    content, null, null, MailComment.CommentType.PATCHSET_LEVEL, isLink),
                 parsedComments);
           }
         } else if (lastEncounteredComment == null) {
diff --git a/java/com/google/gerrit/mail/MailComment.java b/java/com/google/gerrit/mail/MailComment.java
index 3e7da10..cea856c 100644
--- a/java/com/google/gerrit/mail/MailComment.java
+++ b/java/com/google/gerrit/mail/MailComment.java
@@ -20,7 +20,7 @@
 /** A comment parsed from inbound email */
 public class MailComment {
   public enum CommentType {
-    CHANGE_MESSAGE,
+    PATCHSET_LEVEL,
     FILE_COMMENT,
     INLINE_COMMENT
   }
diff --git a/java/com/google/gerrit/mail/TextParser.java b/java/com/google/gerrit/mail/TextParser.java
index a33c66f..c43d200 100644
--- a/java/com/google/gerrit/mail/TextParser.java
+++ b/java/com/google/gerrit/mail/TextParser.java
@@ -30,7 +30,7 @@
   /**
    * Parses comments from plaintext email.
    *
-   * @param email @param email the message as received from the email service
+   * @param email the message as received from the email service
    * @param comments list of {@link HumanComment}s previously persisted on the change that caused
    *     the original notification email to be sent out. Ordering must be the same as in the
    *     outbound email
@@ -77,7 +77,7 @@
         // This is not a comment, try to advance the file/comment pointers and
         // add previous comment to list if applicable
         if (currentComment != null) {
-          if (currentComment.type == MailComment.CommentType.CHANGE_MESSAGE) {
+          if (currentComment.type == MailComment.CommentType.PATCHSET_LEVEL) {
             currentComment.message = ParserUtil.trimQuotation(currentComment.message);
           }
           if (!Strings.isNullOrEmpty(currentComment.message)) {
@@ -115,7 +115,7 @@
           if (lastEncounteredComment == null) {
             if (lastEncounteredFileName == null) {
               // Change message
-              currentComment.type = MailComment.CommentType.CHANGE_MESSAGE;
+              currentComment.type = MailComment.CommentType.PATCHSET_LEVEL;
             } else {
               // File comment not sent in reply to another comment
               currentComment.type = MailComment.CommentType.FILE_COMMENT;
diff --git a/java/com/google/gerrit/metrics/BUILD b/java/com/google/gerrit/metrics/BUILD
index 3cc056b..0cb0275 100644
--- a/java/com/google/gerrit/metrics/BUILD
+++ b/java/com/google/gerrit/metrics/BUILD
@@ -8,6 +8,7 @@
         "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/lifecycle",
+        "//java/com/google/gerrit/server/cancellation",
         "//java/com/google/gerrit/server/logging",
         "//lib:guava",
         "//lib:jgit",
diff --git a/java/com/google/gerrit/metrics/Description.java b/java/com/google/gerrit/metrics/Description.java
index 10568bc..f5963af 100644
--- a/java/com/google/gerrit/metrics/Description.java
+++ b/java/com/google/gerrit/metrics/Description.java
@@ -133,27 +133,27 @@
     return this;
   }
 
-  /** @return true if the metric value never changes after startup. */
+  /** Returns true if the metric value never changes after startup. */
   public boolean isConstant() {
     return TRUE_VALUE.equals(annotations.get(CONSTANT));
   }
 
-  /** @return true if the metric may be interpreted as a rate over time. */
+  /** Returns true if the metric may be interpreted as a rate over time. */
   public boolean isRate() {
     return TRUE_VALUE.equals(annotations.get(RATE));
   }
 
-  /** @return true if the metric is an instantaneous sample. */
+  /** Returns true if the metric is an instantaneous sample. */
   public boolean isGauge() {
     return TRUE_VALUE.equals(annotations.get(GAUGE));
   }
 
-  /** @return true if the metric accumulates over the lifespan of the process. */
+  /** Returns true if the metric accumulates over the lifespan of the process. */
   public boolean isCumulative() {
     return TRUE_VALUE.equals(annotations.get(CUMULATIVE));
   }
 
-  /** @return the suggested field ordering. */
+  /** Returns the suggested field ordering. */
   public FieldOrdering getFieldOrdering() {
     String o = annotations.get(FIELD_ORDERING);
     return o != null ? FieldOrdering.valueOf(o) : FieldOrdering.AT_END;
@@ -187,7 +187,7 @@
     return u;
   }
 
-  /** @return immutable copy of all annotations (configurable properties). */
+  /** Returns an immutable copy of all annotations (configurable properties). */
   public ImmutableMap<String, String> getAnnotations() {
     return ImmutableMap.copyOf(annotations);
   }
diff --git a/java/com/google/gerrit/metrics/Field.java b/java/com/google/gerrit/metrics/Field.java
index bdae854..5508819 100644
--- a/java/com/google/gerrit/metrics/Field.java
+++ b/java/com/google/gerrit/metrics/Field.java
@@ -102,19 +102,19 @@
         .metadataMapper(metadataMapper);
   }
 
-  /** @return name of this field within the metric. */
+  /** Returns name of this field within the metric. */
   public abstract String name();
 
-  /** @return type of value used within the field. */
+  /** Returns type of value used within the field. */
   public abstract Class<T> valueType();
 
-  /** @return mapper that maps a field value to a field in the {@link Metadata} class. */
+  /** Returns mapper that maps a field value to a field in the {@link Metadata} class. */
   public abstract BiConsumer<Metadata.Builder, T> metadataMapper();
 
-  /** @return description text for the field explaining its range of values. */
+  /** Returns description text for the field explaining its range of values. */
   public abstract Optional<String> description();
 
-  /** @return formatter to format field values. */
+  /** Returns formatter to format field values. */
   public abstract Function<T, String> formatter();
 
   @AutoValue.Builder
diff --git a/java/com/google/gerrit/metrics/Timer0.java b/java/com/google/gerrit/metrics/Timer0.java
index d0033a4..2617431 100644
--- a/java/com/google/gerrit/metrics/Timer0.java
+++ b/java/com/google/gerrit/metrics/Timer0.java
@@ -18,6 +18,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.cancellation.RequestStateContext;
 import com.google.gerrit.server.logging.LoggingContext;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
 import java.util.concurrent.TimeUnit;
@@ -60,6 +61,7 @@
    * @return timer context
    */
   public Context start() {
+    RequestStateContext.abortIfCancelled();
     return new Context(this);
   }
 
@@ -75,6 +77,7 @@
         .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationMs));
     logger.atFinest().log("%s took %dms", name, durationMs);
     doRecord(value, unit);
+    RequestStateContext.abortIfCancelled();
   }
 
   /**
diff --git a/java/com/google/gerrit/metrics/Timer1.java b/java/com/google/gerrit/metrics/Timer1.java
index a8fb1a2..319f3e0 100644
--- a/java/com/google/gerrit/metrics/Timer1.java
+++ b/java/com/google/gerrit/metrics/Timer1.java
@@ -18,6 +18,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.cancellation.RequestStateContext;
 import com.google.gerrit.server.logging.LoggingContext;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
@@ -68,6 +69,7 @@
    * @return timer context
    */
   public Context<F1> start(F1 fieldValue) {
+    RequestStateContext.abortIfCancelled();
     return new Context<>(this, fieldValue);
   }
 
@@ -90,6 +92,7 @@
 
     logger.atFinest().log("%s (%s = %s) took %dms", name, field.name(), fieldValue, durationMs);
     doRecord(fieldValue, value, unit);
+    RequestStateContext.abortIfCancelled();
   }
 
   /**
diff --git a/java/com/google/gerrit/metrics/Timer2.java b/java/com/google/gerrit/metrics/Timer2.java
index 8a4a793..8ae76a2 100644
--- a/java/com/google/gerrit/metrics/Timer2.java
+++ b/java/com/google/gerrit/metrics/Timer2.java
@@ -18,6 +18,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.cancellation.RequestStateContext;
 import com.google.gerrit.server.logging.LoggingContext;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
@@ -74,6 +75,7 @@
    * @return timer context
    */
   public Context<F1, F2> start(F1 fieldValue1, F2 fieldValue2) {
+    RequestStateContext.abortIfCancelled();
     return new Context<>(this, fieldValue1, fieldValue2);
   }
 
@@ -100,6 +102,7 @@
         "%s (%s = %s, %s = %s) took %dms",
         name, field1.name(), fieldValue1, field2.name(), fieldValue2, durationMs);
     doRecord(fieldValue1, fieldValue2, value, unit);
+    RequestStateContext.abortIfCancelled();
   }
 
   /**
diff --git a/java/com/google/gerrit/metrics/Timer3.java b/java/com/google/gerrit/metrics/Timer3.java
index 2044da6..9df963a 100644
--- a/java/com/google/gerrit/metrics/Timer3.java
+++ b/java/com/google/gerrit/metrics/Timer3.java
@@ -18,6 +18,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.cancellation.RequestStateContext;
 import com.google.gerrit.server.logging.LoggingContext;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.PerformanceLogRecord;
@@ -80,6 +81,7 @@
    * @return timer context
    */
   public Context<F1, F2, F3> start(F1 fieldValue1, F2 fieldValue2, F3 fieldValue3) {
+    RequestStateContext.abortIfCancelled();
     return new Context<>(this, fieldValue1, fieldValue2, fieldValue3);
   }
 
@@ -116,6 +118,7 @@
         fieldValue3,
         durationMs);
     doRecord(fieldValue1, fieldValue2, fieldValue3, value, unit);
+    RequestStateContext.abortIfCancelled();
   }
 
   /**
diff --git a/java/com/google/gerrit/metrics/TimerContext.java b/java/com/google/gerrit/metrics/TimerContext.java
index 62eb030..a3754c5 100644
--- a/java/com/google/gerrit/metrics/TimerContext.java
+++ b/java/com/google/gerrit/metrics/TimerContext.java
@@ -29,7 +29,7 @@
    */
   public abstract void record(long elapsed);
 
-  /** @return the start time in system time nanoseconds. */
+  /** Returns the start time in system time nanoseconds. */
   public long getStartTime() {
     return startNanos;
   }
diff --git a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
index e8611b3..d64bd19 100644
--- a/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
+++ b/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -184,7 +184,9 @@
                         + "having most data in the cache.")
                 .setGauge()
                 .setUnit("byte"),
-            Field.ofString("repository_name", Metadata.Builder::projectName).build());
+            Field.ofString("repository_name", Metadata.Builder::projectName)
+                .description("The name of the repository.")
+                .build());
     metrics.newTrigger(
         repoEnt,
         () -> {
diff --git a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
new file mode 100644
index 0000000..01c76c1
--- /dev/null
+++ b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
@@ -0,0 +1,202 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.Collection;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.kohsuke.args4j.Option;
+
+/**
+ * Changes the case sensitivity of `username:` and `gerrit:` external IDs by recomputing the SHA-1
+ * sums used as note names.
+ */
+public class ChangeExternalIdCaseSensitivity extends SiteProgram {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Option(name = "--batch", usage = "Don't ask for confirmation before migrating.")
+  private boolean batch;
+
+  @Option(name = "--dryrun", usage = "Do a dryrun of the migration.")
+  private boolean dryrun;
+
+  private final LifecycleManager manager = new LifecycleManager();
+  private final TextProgressMonitor monitor = new TextProgressMonitor();
+
+  private Config globalConfig;
+  private boolean isUserNameCaseInsensitive;
+  private ConsoleUI ui;
+
+  @Inject private GitRepositoryManager repoManager;
+  @Inject private AllUsersName allUsersName;
+  @Inject private Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
+  @Inject private ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
+  @Inject private ExternalIds externalIds;
+  @Inject private ExternalIdFactory externalIdFactory;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+    ui = ConsoleUI.getInstance(batch);
+
+    Injector dbInjector = createDbInjector();
+    manager.add(dbInjector, dbInjector.createChildInjector(NoteDbSchemaVersionCheck.module()));
+    dbInjector
+        .createChildInjector(
+            new FactoryModule() {
+              @Override
+              protected void configure() {
+                bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
+                factory(MetaDataUpdate.InternalFactory.class);
+                DynamicMap.mapOf(binder(), ExternalIdUpsertPreprocessor.class);
+
+                // The ChangeExternalIdCaseSensitivity program needs to access all external IDs only
+                // once to update them. After the update they are not accessed again. Hence the
+                // LocalUsernamesToLowerCase program doesn't benefit from caching external IDs and
+                // the external ID cache can be disabled.
+                install(DisabledExternalIdCache.module());
+              }
+            })
+        .injectMembers(this);
+    globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+
+    this.isUserNameCaseInsensitive =
+        globalConfig.getBoolean("auth", "userNameCaseInsensitive", false);
+
+    String message =
+        "auth.userNameCaseInsensitive is set to %b. "
+            + "External IDs will be migrated to be case %ssensitive. Continue?";
+    if (!ui.yesno(
+        true, message, isUserNameCaseInsensitive, isUserNameCaseInsensitive ? "" : "in")) {
+      return 0;
+    }
+
+    Collection<ExternalId> todo = externalIds.all();
+    monitor.beginTask("Converting external ID note names", todo.size());
+
+    manager.start();
+    try {
+      try (Repository repo = repoManager.openRepository(allUsersName)) {
+        ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
+        for (ExternalId extId : todo) {
+          recomputeExternalIdNoteId(extIdNotes, extId);
+          monitor.update(1);
+        }
+        if (!dryrun) {
+          try (MetaDataUpdate metaDataUpdate =
+              metaDataUpdateServerFactory.get().create(allUsersName)) {
+            metaDataUpdate.setMessage(
+                String.format(
+                    "Migration to case %ssensitive usernames",
+                    isUserNameCaseInsensitive ? "" : "in"));
+            extIdNotes.commit(metaDataUpdate);
+          }
+        }
+      }
+    } finally {
+      manager.stop();
+      monitor.endTask();
+    }
+
+    int exitCode;
+    if (!dryrun) {
+      updateGerritConfig();
+
+      exitCode = reindexAccounts();
+    } else {
+      exitCode = 0;
+    }
+    return exitCode;
+  }
+
+  private void recomputeExternalIdNoteId(ExternalIdNotes extIdNotes, ExternalId extId)
+      throws DuplicateKeyException, IOException {
+    if (extId.isScheme(SCHEME_GERRIT) || extId.isScheme(SCHEME_USERNAME)) {
+      ExternalIdKeyFactory keyFactory =
+          new ExternalIdKeyFactory(
+              new ExternalIdKeyFactory.Config() {
+                @Override
+                public boolean isUserNameCaseInsensitive() {
+                  return !isUserNameCaseInsensitive;
+                }
+              });
+      ExternalId.Key updatedKey = keyFactory.create(extId.key().scheme(), extId.key().id());
+      if (!extId.key().sha1().getName().equals(updatedKey.sha1().getName())) {
+        logger.atInfo().log("Converting note name of external ID: %s", extId.key());
+        ExternalId updatedExtId =
+            externalIdFactory.create(
+                updatedKey, extId.accountId(), extId.email(), extId.password(), extId.blobId());
+        extIdNotes.replace(extId, updatedExtId);
+      }
+    }
+  }
+
+  private void updateGerritConfig() throws IOException, ConfigInvalidException {
+    logger.atInfo().log("Setting auth.userNameCaseInsensitive to true in gerrit.config.");
+    FileBasedConfig config =
+        new FileBasedConfig(
+            globalConfig, getSitePath().resolve("etc/gerrit.config").toFile(), FS.DETECTED);
+    config.load();
+    config.setBoolean("auth", null, "userNameCaseInsensitive", !isUserNameCaseInsensitive);
+    config.save();
+  }
+
+  private int reindexAccounts() throws Exception {
+    monitor.beginTask("Reindex accounts", ProgressMonitor.UNKNOWN);
+    String[] reindexArgs = {
+      "--site-path", getSitePath().toString(), "--index", AccountSchemaDefinitions.NAME
+    };
+    logger.atInfo().log(
+        "Migration complete, reindexing accounts with: reindex %s", String.join(" ", reindexArgs));
+    Reindex reindexPgm = new Reindex();
+    int exitCode = reindexPgm.main(reindexArgs);
+    monitor.endTask();
+    return exitCode;
+  }
+}
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index a3605f7..0a9b4d8 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -181,7 +181,7 @@
   private String devCdn = "";
 
   @Option(name = "--dev-cdn", usage = "Use specified cdn for serving static content.")
-  private void setDevCdn(String cdn) {
+  void setDevCdn(String cdn) {
     if (cdn == null) {
       cdn = "";
     }
diff --git a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 8e2f70f..f651994 100644
--- a/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
@@ -52,6 +53,7 @@
   @Inject private AllUsersName allUsersName;
   @Inject private Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
   @Inject private ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
+  @Inject private ExternalIdFactory externalIdFactory;
   @Inject private ExternalIds externalIds;
 
   @Override
@@ -105,7 +107,7 @@
       String localUserLowerCase = localUser.toLowerCase(Locale.US);
       if (!localUser.equals(localUserLowerCase)) {
         ExternalId extIdLowerCase =
-            ExternalId.create(
+            externalIdFactory.create(
                 SCHEME_GERRIT,
                 localUserLowerCase,
                 extId.accountId(),
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index c4b0040..c083296 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -164,7 +164,6 @@
    * Invoked before site init is called.
    *
    * @param init initializer instance.
-   * @throws Exception
    */
   protected boolean beforeInit(SiteInit init) throws Exception {
     return false;
@@ -174,7 +173,6 @@
    * Invoked after site init is called.
    *
    * @param run completed run instance.
-   * @throws Exception
    */
   protected void afterInit(SiteRun run) throws Exception {}
 
diff --git a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index 9519653..e2a1f04 100644
--- a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.SitePaths;
@@ -39,12 +40,18 @@
   private final InitFlags flags;
   private final SitePaths site;
   private final AllUsersName allUsers;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
-  public ExternalIdsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
+  public ExternalIdsOnInit(
+      InitFlags flags,
+      SitePaths site,
+      AllUsersNameOnInitProvider allUsers,
+      ExternalIdFactory externalIdFactory) {
     this.flags = flags;
     this.site = site;
     this.allUsers = new AllUsersName(allUsers.get());
+    this.externalIdFactory = externalIdFactory;
   }
 
   public synchronized void insert(String commitMessage, Collection<ExternalId> extIds)
@@ -52,7 +59,8 @@
     File path = getPath();
     if (path != null) {
       try (Repository allUsersRepo = new FileRepository(path)) {
-        ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, allUsersRepo);
+        ExternalIdNotes extIdNotes =
+            ExternalIdNotes.loadNoCacheUpdate(allUsers, allUsersRepo, externalIdFactory);
         extIdNotes.insert(extIds);
         try (MetaDataUpdate metaDataUpdate =
             new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsers, allUsersRepo)) {
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 2e32066..d6a0133 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.index.account.AccountIndex;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.group.GroupIndex;
@@ -52,6 +53,7 @@
   private final ExternalIdsOnInit externalIds;
   private final SequencesOnInit sequencesOnInit;
   private final GroupsOnInit groupsOnInit;
+  private final ExternalIdFactory externalIdFactory;
   private AccountIndexCollection accountIndexCollection;
   private GroupIndexCollection groupIndexCollection;
 
@@ -63,7 +65,8 @@
       VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
       ExternalIdsOnInit externalIds,
       SequencesOnInit sequencesOnInit,
-      GroupsOnInit groupsOnInit) {
+      GroupsOnInit groupsOnInit,
+      ExternalIdFactory externalIdFactory) {
     this.flags = flags;
     this.ui = ui;
     this.accounts = accounts;
@@ -71,6 +74,7 @@
     this.externalIds = externalIds;
     this.sequencesOnInit = sequencesOnInit;
     this.groupsOnInit = groupsOnInit;
+    this.externalIdFactory = externalIdFactory;
   }
 
   @Override
@@ -107,10 +111,10 @@
         String email = readEmail(sshKey);
 
         List<ExternalId> extIds = new ArrayList<>(2);
-        extIds.add(ExternalId.createUsername(username, id, httpPassword));
+        extIds.add(externalIdFactory.createUsername(username, id, httpPassword));
 
         if (email != null) {
-          extIds.add(ExternalId.createEmail(id, email));
+          extIds.add(externalIdFactory.createEmail(id, email));
         }
         externalIds.insert("Add external IDs for initial admin user", extIds);
 
diff --git a/java/com/google/gerrit/pgm/init/InitAuth.java b/java/com/google/gerrit/pgm/init/InitAuth.java
index c15cff3..948ec49 100644
--- a/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.mail.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -42,11 +43,13 @@
   private final Section ldap;
   private final Section receive;
   private final InitFlags flags;
+  private final SitePaths site;
 
   @Inject
-  InitAuth(InitFlags flags, ConsoleUI ui, Section.Factory sections) {
+  InitAuth(InitFlags flags, ConsoleUI ui, final SitePaths site, Section.Factory sections) {
     this.flags = flags;
     this.ui = ui;
+    this.site = site;
     this.auth = sections.get("auth", null);
     this.ldap = sections.get("ldap", null);
     this.receive = sections.get(RECEIVE, null);
@@ -62,6 +65,10 @@
     }
 
     initSignedPush();
+
+    if (site.isNew) {
+      initUserNameCaseSensitivity();
+    }
   }
 
   private void initAuthType() {
@@ -156,4 +163,9 @@
     boolean enable = ui.yesno(def, "Enable signed push support");
     receive.set("enableSignedPush", Boolean.toString(enable));
   }
+
+  private void initUserNameCaseSensitivity() {
+    boolean enableCaseInsensitivity = ui.yesno(true, "Use case insensitive usernames");
+    auth.set("userNameCaseInsensitive", Boolean.toString(enableCaseInsensitivity));
+  }
 }
diff --git a/java/com/google/gerrit/pgm/init/InitJGitConfig.java b/java/com/google/gerrit/pgm/init/InitJGitConfig.java
index bad55b4..b68e9f7 100644
--- a/java/com/google/gerrit/pgm/init/InitJGitConfig.java
+++ b/java/com/google/gerrit/pgm/init/InitJGitConfig.java
@@ -53,7 +53,8 @@
             ConfigConstants.CONFIG_RECEIVE_SECTION, null, ConfigConstants.CONFIG_KEY_AUTOGC, false);
         jgitConfig.save();
         ui.error(
-            "Auto-configured \"receive.autogc = false\" to disable auto-gc after git-receive-pack.");
+            "Auto-configured \"receive.autogc = false\" to disable auto-gc after"
+                + " git-receive-pack.");
       } else if (jgitConfig.getBoolean(
           ConfigConstants.CONFIG_RECEIVE_SECTION, ConfigConstants.CONFIG_KEY_AUTOGC, true)) {
         ui.error(
@@ -72,12 +73,9 @@
                 ConfigConstants.CONFIG_PROTOCOL_SECTION, null, ConfigConstants.CONFIG_KEY_VERSION);
         if (!TransferConfig.ProtocolVersion.V2.version().equals(version)) {
           ui.error(
-              String.format(
-                  "HINT: JGit option \"%s.%s = %s\". It's recommended to activate git\n"
-                      + "wire protocol version 2 to improve git fetch performance.",
-                  ConfigConstants.CONFIG_PROTOCOL_SECTION,
-                  ConfigConstants.CONFIG_KEY_VERSION,
-                  version));
+              "HINT: JGit option \"%s.%s = %s\". It's recommended to activate git\n"
+                  + "wire protocol version 2 to improve git fetch performance.",
+              ConfigConstants.CONFIG_PROTOCOL_SECTION, ConfigConstants.CONFIG_KEY_VERSION, version);
         }
       }
     } catch (IOException e) {
diff --git a/java/com/google/gerrit/pgm/init/PluginsDistribution.java b/java/com/google/gerrit/pgm/init/PluginsDistribution.java
index 73720c4..65c96ec 100644
--- a/java/com/google/gerrit/pgm/init/PluginsDistribution.java
+++ b/java/com/google/gerrit/pgm/init/PluginsDistribution.java
@@ -24,6 +24,8 @@
 
   public interface Processor {
     /**
+     * Processes the plugin
+     *
      * @param pluginName the name of the plugin (without the .jar extension)
      * @param in the content of the plugin .jar file. Implementors don't have to close this stream.
      * @throws IOException implementations will typically propagate any IOException caused by
diff --git a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index 5ca239e..abd7d43 100644
--- a/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
+++ b/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -15,15 +15,16 @@
 package com.google.gerrit.pgm.init.api;
 
 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.server.config.AllProjectsConfigProvider;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.project.GroupList;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
 import java.io.IOException;
+import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
@@ -34,16 +35,18 @@
 public class AllProjectsConfig extends VersionedMetaDataOnInit {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
-  @Nullable private final StoredConfig baseConfig;
+  private final Optional<StoredConfig> baseConfig;
   private Config cfg;
   private GroupList groupList;
 
   @Inject
-  AllProjectsConfig(AllProjectsNameOnInitProvider allProjects, SitePaths site, InitFlags flags) {
+  AllProjectsConfig(
+      AllProjectsNameOnInitProvider allProjects,
+      AllProjectsConfigProvider allProjectsConfigProvider,
+      SitePaths site,
+      InitFlags flags) {
     super(flags, site, allProjects.get(), RefNames.REFS_CONFIG);
-    this.baseConfig =
-        ProjectConfig.Factory.getBaseConfig(
-            site, new AllProjectsName(allProjects.get()), Project.nameKey(allProjects.get()));
+    this.baseConfig = allProjectsConfigProvider.get(new AllProjectsName(allProjects.get()));
   }
 
   public Config getConfig() {
@@ -62,8 +65,8 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    if (baseConfig != null) {
-      baseConfig.load();
+    if (baseConfig.isPresent()) {
+      baseConfig.get().load();
     }
     groupList = readGroupList();
     cfg = readConfig(ProjectConfig.PROJECT_CONFIG, baseConfig);
diff --git a/java/com/google/gerrit/pgm/init/api/BUILD b/java/com/google/gerrit/pgm/init/api/BUILD
index 693d319..733b9e3 100644
--- a/java/com/google/gerrit/pgm/init/api/BUILD
+++ b/java/com/google/gerrit/pgm/init/api/BUILD
@@ -11,6 +11,7 @@
         "//java/com/google/gerrit/server",
         "//lib:guava",
         "//lib:jgit",
+        "//lib/errorprone:annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index ea39a44..dffdde7 100644
--- a/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init.api;
 
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
 import com.google.gerrit.common.Die;
 import java.io.Console;
 import java.util.EnumSet;
@@ -37,7 +39,7 @@
     return new Die("aborted by user");
   }
 
-  /** @return true if this is a batch UI that has no user interaction. */
+  /** Returns true if this is a batch UI that has no user interaction. */
   public abstract boolean isBatch();
 
   /** Display a header message before a series of prompts. */
@@ -75,6 +77,7 @@
   public abstract String password(String fmt, Object... args);
 
   /** Display an error message on the system stderr. */
+  @FormatMethod
   public void error(String format, Object... args) {
     System.err.println(String.format(format, args));
     System.err.flush();
@@ -97,6 +100,7 @@
     }
 
     @Override
+    @FormatMethod
     public boolean yesno(Boolean def, String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (; ; ) {
@@ -135,7 +139,8 @@
     }
 
     @Override
-    public String readString(String def, String fmt, Object... args) {
+    @FormatMethod
+    public String readString(String def, @FormatString String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       String r;
       if (def != null) {
@@ -154,7 +159,9 @@
     }
 
     @Override
-    public String readString(String def, Set<String> allowedValues, String fmt, Object... args) {
+    @FormatMethod
+    public String readString(
+        String def, Set<String> allowedValues, @FormatString String fmt, Object... args) {
       for (; ; ) {
         String r = readString(def, fmt, args);
         if (allowedValues.contains(r.toLowerCase())) {
@@ -171,6 +178,7 @@
     }
 
     @Override
+    @FormatMethod
     public String password(String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (; ; ) {
@@ -195,6 +203,7 @@
     }
 
     @Override
+    @FormatMethod
     public <T extends Enum<?>, A extends EnumSet<? extends T>> T readEnum(
         T def, A options, String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
diff --git a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
index a937c4b..8e69eb9 100644
--- a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.init.api;
 
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -41,6 +42,18 @@
   }
 
   @Override
+  public Status getRepositoryStatus(NameKey name) {
+    try {
+      openRepository(name);
+    } catch (RepositoryNotFoundException e) {
+      return Status.NON_EXISTENT;
+    } catch (IOException e) {
+      return Status.UNAVAILABLE;
+    }
+    return Status.ACTIVE;
+  }
+
+  @Override
   public Repository openRepository(Project.NameKey name)
       throws RepositoryNotFoundException, IOException {
     return new FileRepository(getPath(name));
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 4db657d..188f30b 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -39,7 +39,7 @@
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.ServiceUserClassifierImpl;
-import com.google.gerrit.server.account.externalids.ExternalIdModule;
+import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
 import com.google.gerrit.server.approval.ApprovalCacheImpl;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.cache.h2.H2CacheModule;
@@ -79,6 +79,7 @@
 import com.google.gerrit.server.project.CommitResource;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.approval.ApprovalModule;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -171,7 +172,7 @@
     modules.add(new DefaultPermissionBackendModule());
     modules.add(new DefaultMemoryCacheModule());
     modules.add(new H2CacheModule());
-    modules.add(new ExternalIdModule());
+    modules.add(new ExternalIdCacheModule());
     modules.add(new GroupModule());
     modules.add(new NoteDbModule());
     modules.add(AccountCacheImpl.module());
@@ -188,6 +189,7 @@
     modules.add(TagCache.module());
     modules.add(PureRevertCache.module());
     modules.add(new ApprovalModule());
+    modules.add(SubmitRequirementsEvaluatorImpl.module());
     factory(CapabilityCollection.Factory.class);
     factory(ChangeData.AssistedFactory.class);
     factory(ChangeIsVisibleToPredicate.Factory.class);
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index c3be0a4..c1ba896 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -52,7 +52,7 @@
       name = "--site-path",
       aliases = {"-d"},
       usage = "Local directory containing site data")
-  private void setSitePath(String path) {
+  void setSitePath(String path) {
     sitePath = Paths.get(path).normalize();
   }
 
@@ -64,7 +64,7 @@
     this.sitePath = sitePath.normalize();
   }
 
-  /** @return the site path specified on the command line. */
+  /** Returns the site path specified on the command line. */
   protected Path getSitePath() {
     return sitePath;
   }
@@ -76,12 +76,12 @@
     }
   }
 
-  /** @return provides database connectivity and site path. */
+  /** Provides database connectivity and site path. */
   protected Injector createDbInjector() {
     return createDbInjector(false);
   }
 
-  /** @return provides database connectivity and site path. */
+  /** Provides database connectivity and site path. */
   protected Injector createDbInjector(boolean enableMetrics) {
     List<Module> modules = new ArrayList<>();
 
diff --git a/java/com/google/gerrit/server/CancellationMetrics.java b/java/com/google/gerrit/server/CancellationMetrics.java
index 9a1ac9c..f534ccb 100644
--- a/java/com/google/gerrit/server/CancellationMetrics.java
+++ b/java/com/google/gerrit/server/CancellationMetrics.java
@@ -14,12 +14,8 @@
 
 package com.google.gerrit.server;
 
-import static com.google.common.base.Preconditions.checkState;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Splitter;
 import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.metrics.Counter1;
 import com.google.gerrit.metrics.Counter3;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Field;
@@ -34,6 +30,7 @@
 public class CancellationMetrics {
   private final Counter3<String, String, String> advisoryDeadlineCount;
   private final Counter3<String, String, RequestStateProvider.Reason> cancelledRequestsCount;
+  private final Counter1<String> receiveTimeoutCount;
 
   @Inject
   CancellationMetrics(MetricMaker metrics) {
@@ -46,7 +43,7 @@
                 .build(),
             Field.ofString("request_uri", Metadata.Builder::restViewName)
                 .description(
-                    "The URI of the request to which the advisory deadline applied"
+                    "The redacted URI of the request to which the advisory deadline applied"
                         + " (only set for request_type = REST).")
                 .build(),
             Field.ofString("deadline_id", (metadataBuilder, resolveAllUsers) -> {})
@@ -62,7 +59,7 @@
                 .build(),
             Field.ofString("request_uri", Metadata.Builder::restViewName)
                 .description(
-                    "The URI of the request that was cancelled"
+                    "The redacted URI of the request that was cancelled"
                         + " (only set for request_type = REST).")
                 .build(),
             Field.ofEnum(
@@ -71,21 +68,35 @@
                     Metadata.Builder::cancellationReason)
                 .description("The reason why the request was cancelled.")
                 .build());
+
+    this.receiveTimeoutCount =
+        metrics.newCounter(
+            "cancellation/receive_timeout_count",
+            new Description(
+                    "Number of requests that are cancelled because receive.timout is exceeded")
+                .setRate(),
+            Field.ofString("cancellation_type", (metadataBuilder, resolveAllUsers) -> {})
+                .description("The cancellation type (graceful or forceful).")
+                .build());
   }
 
   public void countAdvisoryDeadline(RequestInfo requestInfo, String deadlineId) {
     advisoryDeadlineCount.increment(
-        requestInfo.requestType(),
-        requestInfo.requestUri().map(CancellationMetrics::redactRequestUri).orElse(""),
-        deadlineId);
+        requestInfo.requestType(), requestInfo.redactedRequestUri().orElse(""), deadlineId);
   }
 
   public void countCancelledRequest(
       RequestInfo requestInfo, RequestStateProvider.Reason cancellationReason) {
     cancelledRequestsCount.increment(
-        requestInfo.requestType(),
-        requestInfo.requestUri().map(CancellationMetrics::redactRequestUri).orElse(""),
-        cancellationReason);
+        requestInfo.requestType(), requestInfo.redactedRequestUri().orElse(""), cancellationReason);
+  }
+
+  public void countCancelledRequest(
+      RequestInfo.RequestType requestType,
+      String requestUri,
+      RequestStateProvider.Reason cancellationReason) {
+    cancelledRequestsCount.increment(
+        requestType.name(), RequestInfo.redactRequestUri(requestUri), cancellationReason);
   }
 
   @UsedAt(UsedAt.Project.GOOGLE)
@@ -96,56 +107,11 @@
     cancelledRequestsCount.increment(requestType, redactedRequestUri, cancellationReason);
   }
 
-  /**
-   * Redacts resource IDs from the given request URI.
-   *
-   * <p>resource IDs in the request URI are replaced with '*'.
-   *
-   * @param requestUri a REST URI that has path segments that alternate between view name and
-   *     resource IDs (e.g. "/<view>", "/<view>/<id>", "/<view>/<id>/<view>",
-   *     "/<view>/<id>/<view>/<id>", "/<view>/<id>/<view>/<id>/<view>" etc.), must be given without
-   *     the '/a' prefix
-   * @return the redacted request URI
-   */
-  @VisibleForTesting
-  static String redactRequestUri(String requestUri) {
-    requireNonNull(requestUri, "requestUri");
-    checkState(!requestUri.startsWith("/a"), "request URI must not start with '/a'");
+  public void countGracefulReceiveTimeout() {
+    receiveTimeoutCount.increment("graceful");
+  }
 
-    StringBuilder redactedRequestUri = new StringBuilder();
-
-    boolean hasLeadingSlash = false;
-    boolean hasTrailingSlash = false;
-    if (requestUri.startsWith("/")) {
-      hasLeadingSlash = true;
-      requestUri = requestUri.substring(1);
-    }
-    if (requestUri.endsWith("/")) {
-      hasTrailingSlash = true;
-      requestUri = requestUri.substring(0, requestUri.length() - 1);
-    }
-
-    boolean idPathSegment = false;
-    for (String pathSegment : Splitter.on('/').split(requestUri)) {
-      if (!idPathSegment) {
-        redactedRequestUri.append("/" + pathSegment);
-        idPathSegment = true;
-      } else {
-        redactedRequestUri.append("/");
-        if (!pathSegment.isEmpty()) {
-          redactedRequestUri.append("*");
-        }
-        idPathSegment = false;
-      }
-    }
-
-    if (!hasLeadingSlash) {
-      redactedRequestUri.deleteCharAt(0);
-    }
-    if (hasTrailingSlash) {
-      redactedRequestUri.append('/');
-    }
-
-    return redactedRequestUri.toString();
+  public void countForcefulReceiveTimeout() {
+    receiveTimeoutCount.increment("forceful");
   }
 }
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index d8b5d87..8366b09 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -127,8 +127,9 @@
   }
 
   /**
+   * Determines whether the tag starts with the autogenerated prefix
+   *
    * @param tag value of a tag, or null.
-   * @return whether the tag starts with the autogenerated prefix.
    */
   public static boolean isAutogenerated(@Nullable String tag) {
     return tag != null && tag.startsWith(AUTOGENERATED_TAG_PREFIX);
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index 46e8d33..d9edf42 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -53,7 +53,7 @@
   public static final Ordering<PatchSet> PS_ID_ORDER =
       Ordering.from(comparingInt(PatchSet::number));
 
-  /** @return a new unique identifier for change message entities. */
+  /** Returns a new unique identifier for change message entities. */
   public static String messageUuid() {
     byte[] buf = new byte[8];
     UUID_RANDOM.nextBytes(buf);
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index 7012944..0b5600d 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -103,7 +103,7 @@
     return Optional.empty();
   }
 
-  /** @return unique name of the user for logging, never {@code null} */
+  /** Returns unique name of the user for logging, never {@code null} */
   public String getLoggableName() {
     return getUserName().orElseGet(() -> getClass().getSimpleName());
   }
diff --git a/java/com/google/gerrit/server/DeadlineChecker.java b/java/com/google/gerrit/server/DeadlineChecker.java
index 7b231f9..f41b1e3 100644
--- a/java/com/google/gerrit/server/DeadlineChecker.java
+++ b/java/com/google/gerrit/server/DeadlineChecker.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
 import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.stream.Collectors.toMap;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
@@ -30,33 +30,46 @@
 import com.google.gerrit.server.cancellation.RequestStateProvider;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 import org.eclipse.jgit.lib.Config;
 
-/** {@link RequestStateProvider} that checks whether a client provided deadline is exceeded. */
+/**
+ * {@link RequestStateProvider} that checks whether a client provided deadline is exceeded.
+ *
+ * <p>Should be registered at most once per request.
+ */
 public class DeadlineChecker implements RequestStateProvider {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private static String SECTION_DEADLINE = "deadline";
 
   /**
-   * Formatter to format a timeout as {@code timeout=<TIMEOUT><TIME_UNIT>}.
+   * Creates a formatter that formats a timeout as {@code <TIMEOUT_NAME>=<TIMEOUT><TIME_UNIT>}.
    *
    * <p>If the timeout is 1 minute or greater, minutes is used as a time unit. Otherwise
    * milliseconds is just as a time unit.
+   *
+   * @param timeoutName the name of the timeout
    */
-  public static Function<Long, String> TIMEOUT_FORMATTER =
-      timeout -> {
-        String formattedTimeout = MILLISECONDS.convert(timeout, NANOSECONDS) + "ms";
-        long timeoutInMinutes = MINUTES.convert(timeout, NANOSECONDS);
-        if (timeoutInMinutes > 0) {
-          formattedTimeout = timeoutInMinutes + "m";
-        }
-        return String.format("timeout=%s", formattedTimeout);
-      };
+  public static Function<Long, String> getTimeoutFormatter(String timeoutName) {
+    requireNonNull(timeoutName, "timeoutName");
+    return timeout -> {
+      String formattedTimeout = MILLISECONDS.convert(timeout, NANOSECONDS) + "ms";
+      long timeoutInMinutes = MINUTES.convert(timeout, NANOSECONDS);
+      if (timeoutInMinutes > 0) {
+        formattedTimeout = timeoutInMinutes + "m";
+      }
+      return String.format("%s=%s", timeoutName, formattedTimeout);
+    };
+  }
 
   public interface Factory {
     DeadlineChecker create(RequestInfo requestInfo, @Nullable String clientProvidedTimeoutValue)
@@ -68,8 +81,13 @@
   }
 
   private final CancellationMetrics cancellationsMetrics;
+
+  /** The start time of the request in nanoseconds. */
+  private final long start;
+
   private final RequestInfo requestInfo;
   private final RequestStateProvider.Reason cancellationReason;
+  private final String timeoutName;
 
   /**
    * Timeout in nanoseconds after which the request should be aborted.
@@ -93,10 +111,10 @@
    * <p>If any of these deadlines is exceeded the request is not be aborted. Instead the {@code
    * cancellation/advisory_deadline_count} metric is incremented and a log is written.
    */
-  private final ImmutableList<ServerDeadline> advisoryDeadlines;
+  private final Map<String, ServerDeadline> advisoryDeadlines;
 
   /**
-   * Creates a {@code ClientProvidedDeadlineChecker}.
+   * Creates a {@link DeadlineChecker}.
    *
    * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
    *
@@ -117,13 +135,13 @@
     this(
         serverConfig,
         cancellationsMetrics,
-        System.nanoTime(),
+        TimeUtil.nowNanos(),
         requestInfo,
         clientProvidedTimeoutValue);
   }
 
   /**
-   * Creates a {@code ClientProvidedDeadlineChecker}.
+   * Creates a {@link DeadlineChecker}.
    *
    * <p>No deadline is enforced if the client provided deadline value is {@code null} or {@code 0}.
    *
@@ -144,6 +162,7 @@
       @Assisted @Nullable String clientProvidedTimeoutValue)
       throws InvalidDeadlineException {
     this.cancellationsMetrics = cancellationsMetrics;
+    this.start = start;
     this.requestInfo = requestInfo;
 
     ImmutableList<RequestConfig> deadlineConfigs =
@@ -152,6 +171,26 @@
     Optional<ServerDeadline> serverSideDeadline =
         getServerSideDeadline(deadlineConfigs, requestInfo);
     Optional<Long> clientedProvidedTimeout = parseTimeout(clientProvidedTimeoutValue);
+    logDeadlines(serverSideDeadline, clientedProvidedTimeout);
+
+    this.cancellationReason =
+        clientedProvidedTimeout.isPresent()
+            ? RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED
+            : RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED;
+    this.timeoutName =
+        clientedProvidedTimeout
+            .map(clientTimeout -> "client.timeout")
+            .orElse(
+                serverSideDeadline
+                    .map(serverDeadline -> serverDeadline.id() + ".timeout")
+                    .orElse("timeout"));
+    this.timeout =
+        clientedProvidedTimeout.orElse(serverSideDeadline.map(ServerDeadline::timeout).orElse(0L));
+    this.deadline = timeout > 0 ? Optional.of(start + timeout) : Optional.empty();
+  }
+
+  private void logDeadlines(
+      Optional<ServerDeadline> serverSideDeadline, Optional<Long> clientedProvidedTimeout) {
     if (serverSideDeadline.isPresent()) {
       if (clientedProvidedTimeout.isPresent()) {
         logger.atFine().log(
@@ -167,14 +206,11 @@
             TimeUnit.MILLISECONDS.convert(
                 serverSideDeadline.get().timeout(), TimeUnit.NANOSECONDS));
       }
+    } else if (clientedProvidedTimeout.isPresent()) {
+      logger.atFine().log(
+          "applying client provided deadline (timeout = %sms)",
+          TimeUnit.MILLISECONDS.convert(clientedProvidedTimeout.get(), TimeUnit.NANOSECONDS));
     }
-    this.cancellationReason =
-        clientedProvidedTimeout.isPresent()
-            ? RequestStateProvider.Reason.CLIENT_PROVIDED_DEADLINE_EXCEEDED
-            : RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED;
-    this.timeout =
-        clientedProvidedTimeout.orElse(serverSideDeadline.map(ServerDeadline::timeout).orElse(0L));
-    this.deadline = timeout > 0 ? Optional.of(start + timeout) : Optional.empty();
   }
 
   private Optional<ServerDeadline> getServerSideDeadline(
@@ -189,32 +225,40 @@
         .findFirst();
   }
 
-  private ImmutableList<ServerDeadline> getAdvisoryDeadlines(
+  private Map<String, ServerDeadline> getAdvisoryDeadlines(
       ImmutableList<RequestConfig> deadlineConfigs, RequestInfo requestInfo) {
     return deadlineConfigs.stream()
         .filter(deadlineConfig -> deadlineConfig.matches(requestInfo))
         .map(ServerDeadline::readFrom)
         .filter(ServerDeadline::hasTimeout)
         .filter(ServerDeadline::isAdvisory)
-        .collect(toImmutableList());
+        .collect(toMap(ServerDeadline::id, Function.identity()));
   }
 
   @Override
   public void checkIfCancelled(OnCancelled onCancelled) {
-    long now = System.nanoTime();
+    long now = TimeUtil.nowNanos();
 
-    advisoryDeadlines.forEach(
-        advisoryDeadline -> {
-          if (now > advisoryDeadline.timeout()) {
-            logger.atWarning().log(
-                "advisory deadline %s exceeded (%s)",
-                advisoryDeadline.id(), TIMEOUT_FORMATTER.apply(advisoryDeadline.timeout()));
-            cancellationsMetrics.countAdvisoryDeadline(requestInfo, advisoryDeadline.id());
-          }
-        });
+    Set<String> exceededAdvisoryDeadlines = new HashSet<>();
+    advisoryDeadlines
+        .values()
+        .forEach(
+            advisoryDeadline -> {
+              if (now > start + advisoryDeadline.timeout()) {
+                exceededAdvisoryDeadlines.add(advisoryDeadline.id());
+                logger.atFine().log(
+                    "advisory deadline exceeded (%s)",
+                    getTimeoutFormatter(advisoryDeadline.id() + ".timeout")
+                        .apply(advisoryDeadline.timeout()));
+                cancellationsMetrics.countAdvisoryDeadline(requestInfo, advisoryDeadline.id());
+              }
+            });
+    // remove advisory deadlines which have already been reported as exceeded so that they don't get
+    // reported again for this request
+    exceededAdvisoryDeadlines.forEach(advisoryDeadlines::remove);
 
     if (deadline.isPresent() && now > deadline.get()) {
-      onCancelled.onCancel(cancellationReason, TIMEOUT_FORMATTER.apply(timeout));
+      onCancelled.onCancel(cancellationReason, getTimeoutFormatter(timeoutName).apply(timeout));
     }
   }
 
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 24ea9d2..eb3e324 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -343,15 +343,15 @@
   }
 
   /**
-   * @return the user's user name; null if one has not been selected/assigned or if the user name is
-   *     empty.
+   * Returns the user's user name; null if one has not been selected/assigned or if the user name is
+   * empty.
    */
   @Override
   public Optional<String> getUserName() {
     return state().userName();
   }
 
-  /** @return unique name of the user for logging, never {@code null} */
+  /** Returns unique name of the user for logging, never {@code null} */
   @Override
   public String getLoggableName() {
     return getUserName()
diff --git a/java/com/google/gerrit/server/PerformanceMetrics.java b/java/com/google/gerrit/server/PerformanceMetrics.java
new file mode 100644
index 0000000..85452ce
--- /dev/null
+++ b/java/com/google/gerrit/server/PerformanceMetrics.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogger;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+
+/** Performance logger that records the execution times as a metric. */
+@Singleton
+public class PerformanceMetrics implements PerformanceLogger {
+  private static final String OPERATION_LATENCY_METRIC_NAME = "performance/operations";
+  private static final String OPERATION_COUNT_METRIC_NAME = "performance/operations_count";
+  private static final String PLUGIN_OPERATION_COUNT_METRIC_NAME =
+      "performance/plugin_operations_count";
+
+  public final Timer3<String, String, String> operationsLatency;
+  public final Counter3<String, String, String> operationsCounter;
+  public final Counter3<String, String, String> pluginOperationsCounter;
+
+  @Inject
+  PerformanceMetrics(MetricMaker metricMaker) {
+    Field<String> operationNameField =
+        Field.ofString(
+                "operation_name",
+                (metadataBuilder, fieldValue) -> metadataBuilder.operationName(fieldValue))
+            .description("The operation that was performed.")
+            .build();
+    Field<String> changeIdentifierField =
+        Field.ofString("change_identifier", (metadataBuilder, fieldValue) -> {})
+            .description(
+                "The ID of the change for which the operation was performed"
+                    + " (format = '<project>~<numeric-change-id>').")
+            .build();
+    Field<String> traceIdField =
+        Field.ofString("trace_id", (metadataBuilder, fieldValue) -> {})
+            .description("The ID of the trace if tracing was done.")
+            .build();
+    Field<String> requestField =
+        Field.ofString("request", (metadataBuilder, fieldValue) -> {})
+            .description(
+                "The request for which the operation was performed"
+                    + " (format = '<request-type> <redacted-request-uri>').")
+            .build();
+    Field<String> pluginField =
+        Field.ofString(
+                "plugin", (metadataBuilder, fieldValue) -> metadataBuilder.pluginName(fieldValue))
+            .description("The name of the plugin that performed the operation.")
+            .build();
+
+    this.operationsLatency =
+        metricMaker.newTimer(
+            OPERATION_LATENCY_METRIC_NAME,
+            new Description("Latency of performing operations")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS),
+            operationNameField,
+            changeIdentifierField,
+            traceIdField);
+    this.operationsCounter =
+        metricMaker.newCounter(
+            OPERATION_COUNT_METRIC_NAME,
+            new Description("Number of performed operations").setRate(),
+            operationNameField,
+            traceIdField,
+            requestField);
+    this.pluginOperationsCounter =
+        metricMaker.newCounter(
+            PLUGIN_OPERATION_COUNT_METRIC_NAME,
+            new Description("Number of performed operations by plugin").setRate(),
+            operationNameField,
+            pluginField,
+            traceIdField);
+  }
+
+  @Override
+  public void log(String operation, long durationMs) {
+    log(operation, durationMs, /* metadata= */ null);
+  }
+
+  @Override
+  public void log(String operation, long durationMs, @Nullable Metadata metadata) {
+    if (OPERATION_LATENCY_METRIC_NAME.equals(operation)) {
+      // Recording the timer metric below triggers writing a performance log entry. If we are called
+      // for this performance log entry we must abort to avoid an endless loop.
+      // In practice this should not happen since PerformanceLoggers are only called on close() of
+      // the PerformanceLogContext, and hence the performance log that gets written by the metric
+      // below gets ignored.
+      return;
+    }
+
+    String traceId = TraceContext.getTraceId().orElse("");
+
+    operationsLatency.record(
+        operation, formatChangeIdentifier(metadata), traceId, durationMs, TimeUnit.MILLISECONDS);
+
+    String requestTag = TraceContext.getTag(TraceRequestListener.TAG_REQUEST).orElse("");
+    operationsCounter.increment(operation, traceId, requestTag);
+
+    TraceContext.getPluginTag()
+        .ifPresent(pluginName -> pluginOperationsCounter.increment(operation, pluginName, traceId));
+  }
+
+  private String formatChangeIdentifier(@Nullable Metadata metadata) {
+    if (metadata == null
+        || (!metadata.projectName().isPresent() && !metadata.changeId().isPresent())) {
+      return "";
+    }
+
+    StringBuilder sb = new StringBuilder();
+    sb.append(metadata.projectName().orElse("n/a"));
+    sb.append('~');
+    sb.append(metadata.changeId().map(String::valueOf).orElse("n/a"));
+    return sb.toString();
+  }
+}
diff --git a/java/com/google/gerrit/server/RequestConfig.java b/java/com/google/gerrit/server/RequestConfig.java
index 960907d..83cea5b 100644
--- a/java/com/google/gerrit/server/RequestConfig.java
+++ b/java/com/google/gerrit/server/RequestConfig.java
@@ -41,6 +41,7 @@
         RequestConfig.Builder requestConfig = RequestConfig.builder(cfg, section, id);
         requestConfig.requestTypes(parseRequestTypes(cfg, section, id));
         requestConfig.requestUriPatterns(parseRequestUriPatterns(cfg, section, id));
+        requestConfig.excludedRequestUriPatterns(parseExcludedRequestUriPatterns(cfg, section, id));
         requestConfig.accountIds(parseAccounts(cfg, section, id));
         requestConfig.projectPatterns(parseProjectPatterns(cfg, section, id));
         requestConfigs.add(requestConfig.build());
@@ -61,6 +62,11 @@
     return parsePatterns(cfg, section, id, "requestUriPattern");
   }
 
+  private static ImmutableSet<Pattern> parseExcludedRequestUriPatterns(
+      Config cfg, String section, String id) throws ConfigInvalidException {
+    return parsePatterns(cfg, section, id, "excludedRequestUriPattern");
+  }
+
   private static ImmutableSet<Account.Id> parseAccounts(Config cfg, String section, String id)
       throws ConfigInvalidException {
     ImmutableSet.Builder<Account.Id> accountIds = ImmutableSet.builder();
@@ -115,6 +121,9 @@
   /** pattern matching request URIs */
   abstract ImmutableSet<Pattern> requestUriPatterns();
 
+  /** pattern matching request URIs to be excluded */
+  abstract ImmutableSet<Pattern> excludedRequestUriPatterns();
+
   /** accounts IDs matching calling user */
   abstract ImmutableSet<Account.Id> accountIds();
 
@@ -154,6 +163,13 @@
       }
     }
 
+    // If the request URI matches an excluded request URI pattern, then the request is not matched.
+    if (requestInfo.requestUri().isPresent()
+        && excludedRequestUriPatterns().stream()
+            .anyMatch(p -> p.matcher(requestInfo.requestUri().get()).matches())) {
+      return false;
+    }
+
     // If in the request config accounts are set and none of them matches, then the request is not
     // matched.
     if (!accountIds().isEmpty()) {
@@ -200,6 +216,8 @@
 
     abstract Builder requestUriPatterns(ImmutableSet<Pattern> requestUriPatterns);
 
+    abstract Builder excludedRequestUriPatterns(ImmutableSet<Pattern> excludedRequestUriPatterns);
+
     abstract Builder accountIds(ImmutableSet<Account.Id> accountIds);
 
     abstract Builder projectPatterns(ImmutableSet<Pattern> projectPatterns);
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
index f369239..791e228 100644
--- a/java/com/google/gerrit/server/RequestInfo.java
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -14,7 +14,13 @@
 
 package com.google.gerrit.server;
 
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+import com.google.common.base.Splitter;
+import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.logging.TraceContext;
 import java.util.Optional;
@@ -54,6 +60,16 @@
    */
   public abstract Optional<String> requestUri();
 
+  /**
+   * Redacted request URI.
+   *
+   * <p>Request URI where resource IDs are replaced by '*'.
+   */
+  @Memoized
+  public Optional<String> redactedRequestUri() {
+    return requestUri().map(RequestInfo::redactRequestUri);
+  }
+
   /** The user that has sent the request. */
   public abstract CurrentUser callingUser();
 
@@ -67,12 +83,75 @@
    */
   public abstract Optional<Project.NameKey> project();
 
+  @Memoized
+  public String formatForLogging() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(requestType());
+    redactedRequestUri().ifPresent(redactedRequestUri -> sb.append(' ').append(redactedRequestUri));
+    return sb.toString();
+  }
+
+  /**
+   * Redacts resource IDs from the given request URI.
+   *
+   * <p>resource IDs in the request URI are replaced with '*'.
+   *
+   * @param requestUri a REST URI that has path segments that alternate between view name and
+   *     resource IDs (e.g. "/<view>", "/<view>/<id>", "/<view>/<id>/<view>",
+   *     "/<view>/<id>/<view>/<id>", "/<view>/<id>/<view>/<id>/<view>" etc.), must be given without
+   *     the '/a' prefix
+   * @return the redacted request URI
+   */
+  static String redactRequestUri(String requestUri) {
+    requireNonNull(requestUri, "requestUri");
+    checkState(
+        !requestUri.startsWith("/a/"), "request URI must not start with '/a/': %s", requestUri);
+
+    StringBuilder redactedRequestUri = new StringBuilder();
+
+    boolean hasLeadingSlash = false;
+    boolean hasTrailingSlash = false;
+    if (requestUri.startsWith("/")) {
+      hasLeadingSlash = true;
+      requestUri = requestUri.substring(1);
+    }
+    if (requestUri.endsWith("/")) {
+      hasTrailingSlash = true;
+      requestUri = requestUri.substring(0, requestUri.length() - 1);
+    }
+
+    boolean idPathSegment = false;
+    for (String pathSegment : Splitter.on('/').split(requestUri)) {
+      if (!idPathSegment) {
+        redactedRequestUri.append("/" + pathSegment);
+        idPathSegment = true;
+      } else {
+        redactedRequestUri.append("/");
+        if (!pathSegment.isEmpty()) {
+          redactedRequestUri.append("*");
+        }
+        idPathSegment = false;
+      }
+    }
+
+    if (!hasLeadingSlash) {
+      redactedRequestUri.deleteCharAt(0);
+    }
+    if (hasTrailingSlash) {
+      redactedRequestUri.append('/');
+    }
+
+    return redactedRequestUri.toString();
+  }
+
   public static RequestInfo.Builder builder(
       RequestType requestType, CurrentUser callingUser, TraceContext traceContext) {
-    return new AutoValue_RequestInfo.Builder()
-        .requestType(requestType)
-        .callingUser(callingUser)
-        .traceContext(traceContext);
+    return builder().requestType(requestType).callingUser(callingUser).traceContext(traceContext);
+  }
+
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public static RequestInfo.Builder builder() {
+    return new AutoValue_RequestInfo.Builder();
   }
 
   @AutoValue.Builder
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 93cf0de..5dcbd01 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -27,7 +27,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
@@ -57,7 +56,6 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Optional;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -161,8 +159,6 @@
 
   public static final String DEFAULT_LABEL = "star";
   public static final String IGNORE_LABEL = "ignore";
-  public static final String REVIEWED_LABEL = "reviewed";
-  public static final String UNREVIEWED_LABEL = "unreviewed";
   public static final ImmutableSortedSet<String> DEFAULT_LABELS =
       ImmutableSortedSet.of(DEFAULT_LABEL);
 
@@ -350,40 +346,6 @@
     return isIgnoredBy(rsrc.getChange().getId(), rsrc.getUser().asIdentifiedUser().getAccountId());
   }
 
-  private static String getReviewedLabel(Change change) {
-    return getReviewedLabel(change.currentPatchSetId().get());
-  }
-
-  private static String getReviewedLabel(int ps) {
-    return REVIEWED_LABEL + "/" + ps;
-  }
-
-  private static String getUnreviewedLabel(Change change) {
-    return getUnreviewedLabel(change.currentPatchSetId().get());
-  }
-
-  private static String getUnreviewedLabel(int ps) {
-    return UNREVIEWED_LABEL + "/" + ps;
-  }
-
-  public void markAsReviewed(ChangeResource rsrc) throws IllegalLabelException {
-    star(
-        rsrc.getUser().asIdentifiedUser().getAccountId(),
-        rsrc.getProject(),
-        rsrc.getChange().getId(),
-        ImmutableSet.of(getReviewedLabel(rsrc.getChange())),
-        ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())));
-  }
-
-  public void markAsUnreviewed(ChangeResource rsrc) throws IllegalLabelException {
-    star(
-        rsrc.getUser().asIdentifiedUser().getAccountId(),
-        rsrc.getProject(),
-        rsrc.getChange().getId(),
-        ImmutableSet.of(getUnreviewedLabel(rsrc.getChange())),
-        ImmutableSet.of(getReviewedLabel(rsrc.getChange())));
-  }
-
   public static StarRef readLabels(Repository repo, String refName) throws IOException {
     try (TraceTimer traceTimer =
         TraceContext.newTimer(
@@ -422,23 +384,6 @@
     if (labels.containsAll(ImmutableSet.of(DEFAULT_LABEL, IGNORE_LABEL))) {
       throw new MutuallyExclusiveLabelsException(DEFAULT_LABEL, IGNORE_LABEL);
     }
-
-    Set<Integer> reviewedPatchSets = getStarredPatchSets(labels, REVIEWED_LABEL);
-    Set<Integer> unreviewedPatchSets = getStarredPatchSets(labels, UNREVIEWED_LABEL);
-    Optional<Integer> ps =
-        Sets.intersection(reviewedPatchSets, unreviewedPatchSets).stream().findFirst();
-    if (ps.isPresent()) {
-      throw new MutuallyExclusiveLabelsException(
-          getReviewedLabel(ps.get()), getUnreviewedLabel(ps.get()));
-    }
-  }
-
-  public static Set<Integer> getStarredPatchSets(Set<String> labels, String label) {
-    return labels.stream()
-        .filter(l -> l.startsWith(label + "/"))
-        .filter(l -> Ints.tryParse(l.substring(label.length() + 1)) != null)
-        .map(l -> Integer.valueOf(l.substring(label.length() + 1)))
-        .collect(toSet());
   }
 
   private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
diff --git a/java/com/google/gerrit/server/TraceRequestListener.java b/java/com/google/gerrit/server/TraceRequestListener.java
index 7136e47..6cc0982 100644
--- a/java/com/google/gerrit/server/TraceRequestListener.java
+++ b/java/com/google/gerrit/server/TraceRequestListener.java
@@ -28,6 +28,9 @@
  */
 @Singleton
 public class TraceRequestListener implements RequestListener {
+  public static String TAG_REQUEST = "request";
+
+  private static String TAG_PROJECT = "project";
   private static String SECTION_TRACING = "tracing";
 
   private final ImmutableList<RequestConfig> traceConfigs;
@@ -39,7 +42,8 @@
 
   @Override
   public void onRequest(RequestInfo requestInfo) {
-    requestInfo.project().ifPresent(p -> requestInfo.traceContext().addTag("project", p));
+    requestInfo.traceContext().addTag(TAG_REQUEST, requestInfo.formatForLogging());
+    requestInfo.project().ifPresent(p -> requestInfo.traceContext().addTag(TAG_PROJECT, p));
     traceConfigs.stream()
         .filter(traceConfig -> traceConfig.matches(requestInfo))
         .forEach(
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 53a7661..3c69573 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -92,11 +92,12 @@
   }
 
   /**
+   * Returns links for patch sets
+   *
    * @param project Project name.
    * @param commit SHA1 of commit.
    * @param commitMessage the commit message of the commit.
    * @param branchName branch of the commit.
-   * @return Links for patch sets.
    */
   public ImmutableList<WebLinkInfo> getPatchSetLinks(
       Project.NameKey project, String commit, String commitMessage, String branchName) {
@@ -106,11 +107,12 @@
   }
 
   /**
+   * Returns links for resolving conflicts
+   *
    * @param project Project name.
    * @param commit SHA1 of commit.
    * @param commitMessage the commit message of the commit.
    * @param branchName branch of the commit.
-   * @return Links for resolving comflicts.
    */
   public ImmutableList<WebLinkInfo> getResolveConflictsLinks(
       Project.NameKey project, String commit, String commitMessage, String branchName) {
@@ -121,11 +123,12 @@
   }
 
   /**
+   * Returns links for patch sets
+   *
    * @param project Project name.
    * @param revision SHA1 of the parent revision.
    * @param commitMessage the commit message of the parent revision.
    * @param branchName branch of the revision (and parent revision).
-   * @return Links for patch sets.
    */
   public ImmutableList<WebLinkInfo> getParentLinks(
       Project.NameKey project, String revision, String commitMessage, String branchName) {
@@ -135,10 +138,11 @@
   }
 
   /**
+   * Returns links for editing
+   *
    * @param project Project name.
    * @param revision SHA1 of revision.
    * @param file File name.
-   * @return Links for editing.
    */
   public ImmutableList<WebLinkInfo> getEditLinks(String project, String revision, String file) {
     return Patch.isMagic(file)
@@ -147,10 +151,11 @@
   }
 
   /**
+   * Returns links for files
+   *
    * @param project Project name.
    * @param revision SHA1 of revision.
    * @param file File name.
-   * @return Links for files.
    */
   public ImmutableList<WebLinkInfo> getFileLinks(String project, String revision, String file) {
     return Patch.isMagic(file)
@@ -159,10 +164,11 @@
   }
 
   /**
+   * Returns links for file history
+   *
    * @param project Project name.
    * @param revision SHA1 of revision.
    * @param file File name.
-   * @return Links for file history
    */
   public ImmutableList<WebLinkInfo> getFileHistoryLinks(
       String project, String revision, String file) {
@@ -176,6 +182,8 @@
   }
 
   /**
+   * Returns links for file diffs
+   *
    * @param project Project name.
    * @param patchSetIdA Patch set ID of side A, <code>null</code> if no base patch set was selected.
    * @param revisionA SHA1 of revision of side A.
@@ -183,7 +191,6 @@
    * @param patchSetIdB Patch set ID of side B.
    * @param revisionB SHA1 of revision of side B.
    * @param fileB File name of side B.
-   * @return Links for file diffs.
    */
   public ImmutableList<DiffWebLinkInfo> getDiffLinks(
       String project,
@@ -214,26 +221,29 @@
   }
 
   /**
+   * Returns links for projects
+   *
    * @param project Project name.
-   * @return Links for projects.
    */
   public ImmutableList<WebLinkInfo> getProjectLinks(String project) {
     return filterLinks(projectLinks, webLink -> webLink.getProjectWeblink(project));
   }
 
   /**
+   * Returns links for branches
+   *
    * @param project Project name
    * @param branch Branch name
-   * @return Links for branches.
    */
   public ImmutableList<WebLinkInfo> getBranchLinks(String project, String branch) {
     return filterLinks(branchLinks, webLink -> webLink.getBranchWebLink(project, branch));
   }
 
   /**
+   * Returns links for the tag
+   *
    * @param project Project name
    * @param tag Tag name
-   * @return Links for tags.
    */
   public ImmutableList<WebLinkInfo> getTagLinks(String project, String tag) {
     return filterLinks(tagLinks, webLink -> webLink.getTagWebLink(project, tag));
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 93e04880..093af68 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllUsersName;
@@ -45,7 +45,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
@@ -77,6 +76,7 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private final DefaultPreferencesCache defaultPreferenceCache;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
   AccountCacheImpl(
@@ -85,12 +85,14 @@
           LoadingCache<CachedAccountDetails.Key, CachedAccountDetails> accountDetailsCache,
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
-      DefaultPreferencesCache defaultPreferenceCache) {
+      DefaultPreferencesCache defaultPreferenceCache,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.externalIds = externalIds;
     this.accountDetailsCache = accountDetailsCache;
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.defaultPreferenceCache = defaultPreferenceCache;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   @Override
@@ -141,10 +143,10 @@
   public Optional<AccountState> getByUsername(String username) {
     try {
       return externalIds
-          .get(ExternalId.Key.create(SCHEME_USERNAME, username))
+          .get(externalIdKeyFactory.create(SCHEME_USERNAME, username))
           .map(e -> get(e.accountId()))
           .orElseGet(Optional::empty);
-    } catch (IOException | ConfigInvalidException e) {
+    } catch (IOException e) {
       logger.atWarning().withCause(e).log("Cannot load AccountState for username %s", username);
       return Optional.empty();
     }
diff --git a/java/com/google/gerrit/server/account/AccountLimits.java b/java/com/google/gerrit/server/account/AccountLimits.java
index 1845f5b..5549d28 100644
--- a/java/com/google/gerrit/server/account/AccountLimits.java
+++ b/java/com/google/gerrit/server/account/AccountLimits.java
@@ -51,7 +51,7 @@
     user = currentUser;
   }
 
-  /** @return which priority queue the user's tasks should be submitted to. */
+  /** Returns which priority queue the user's tasks should be submitted to. */
   public QueueProvider.QueueType getQueueType() {
     // If a non-generic group (that is not Anonymous Users or Registered Users)
     // grants us INTERACTIVE permission, use the INTERACTIVE queue even if
@@ -99,7 +99,7 @@
     return getRange(GlobalCapability.QUERY_LIMIT).getMax();
   }
 
-  /** @return true if the user has a permission rule specifying the range. */
+  /** Returns true if the user has a permission rule specifying the range. */
   public boolean hasExplicitRange(String permission) {
     return GlobalCapability.hasRange(permission) && !getRules(permission).isEmpty();
   }
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 2152e1e..407d2f7 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -36,6 +36,8 @@
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.auth.NoSuchUserException;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -77,6 +79,8 @@
   private final GroupsUpdate.Factory groupsUpdateFactory;
   private final boolean autoUpdateAccountActiveStatus;
   private final SetInactiveFlag setInactiveFlag;
+  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @VisibleForTesting
   @Inject
@@ -92,7 +96,9 @@
       ProjectCache projectCache,
       ExternalIds externalIds,
       GroupsUpdate.Factory groupsUpdateFactory,
-      SetInactiveFlag setInactiveFlag) {
+      SetInactiveFlag setInactiveFlag,
+      ExternalIdFactory externalIdFactory,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.sequences = sequences;
     this.accounts = accounts;
     this.accountsUpdateProvider = accountsUpdateProvider;
@@ -108,13 +114,15 @@
     this.autoUpdateAccountActiveStatus =
         cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
     this.setInactiveFlag = setInactiveFlag;
+    this.externalIdFactory = externalIdFactory;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
-  /** @return user identified by this external identity string */
+  /** Returns a user identified by this external identity string */
   public Optional<Account.Id> lookup(String externalId) throws AccountException {
     try {
-      return externalIds.get(ExternalId.Key.parse(externalId)).map(ExternalId::accountId);
-    } catch (IOException | ConfigInvalidException e) {
+      return externalIds.get(externalIdKeyFactory.parse(externalId)).map(ExternalId::accountId);
+    } catch (IOException e) {
       throw new AccountException("Cannot lookup account " + externalId, e);
     }
   }
@@ -229,7 +237,7 @@
     String oldEmail = extId.email();
     if (newEmail != null && !newEmail.equals(oldEmail)) {
       ExternalId extIdWithNewEmail =
-          ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password());
+          externalIdFactory.create(extId.key(), extId.accountId(), newEmail, extId.password());
       checkEmailNotUsed(extId.accountId(), extIdWithNewEmail);
       accountUpdates.add(u -> u.replaceExternalId(extId, extIdWithNewEmail));
 
@@ -273,7 +281,7 @@
     logger.atFine().log("Assigning new Id %s to account", newId);
 
     ExternalId extId =
-        ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
+        externalIdFactory.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
     logger.atFine().log("Created external Id: %s", extId);
     checkEmailNotUsed(newId, extId);
     ExternalId userNameExtId =
@@ -348,7 +356,7 @@
               "Cannot assign user name \"%s\" to account %s; name does not conform.",
               username, accountId));
     }
-    return ExternalId.create(SCHEME_USERNAME, username, accountId);
+    return externalIdFactory.create(SCHEME_USERNAME, username, accountId);
   }
 
   private void checkEmailNotUsed(Account.Id accountId, ExternalId extIdToBeCreated)
@@ -414,7 +422,7 @@
       update(who, extId);
     } else {
       ExternalId newExtId =
-          ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
+          externalIdFactory.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
       checkEmailNotUsed(to, newExtId);
       accountsUpdateProvider
           .get()
diff --git a/java/com/google/gerrit/server/account/AccountModule.java b/java/com/google/gerrit/server/account/AccountModule.java
new file mode 100644
index 0000000..c1305cf
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AccountModule.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.inject.AbstractModule;
+
+public class AccountModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(AuthRequest.Factory.class);
+  }
+}
diff --git a/java/com/google/gerrit/server/account/AccountTagProvider.java b/java/com/google/gerrit/server/account/AccountTagProvider.java
new file mode 100644
index 0000000..ddb1331
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AccountTagProvider.java
@@ -0,0 +1,14 @@
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.List;
+
+/**
+ * An extension point for plugins to define their own account tags in addition to the ones defined
+ * at {@link com.google.gerrit.extensions.common.AccountInfo.Tags}.
+ */
+@ExtensionPoint
+public interface AccountTagProvider {
+  List<String> getTags(Account.Id id);
+}
diff --git a/java/com/google/gerrit/server/account/AuthRequest.java b/java/com/google/gerrit/server/account/AuthRequest.java
index ddb54a6..50ed532 100644
--- a/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/java/com/google/gerrit/server/account/AuthRequest.java
@@ -21,6 +21,9 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import java.util.Optional;
 
 /**
@@ -32,31 +35,52 @@
  * not all OpenID providers return them, and not all non-OpenID systems can use them.
  */
 public class AuthRequest {
-  /** Create a request for a local username, such as from LDAP. */
-  public static AuthRequest forUser(String username) {
-    AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_GERRIT, username));
-    r.setUserName(username);
-    return r;
+  @Singleton
+  public static class Factory {
+    private final ExternalIdKeyFactory externalIdKeyFactory;
+
+    @Inject
+    public Factory(ExternalIdKeyFactory externalIdKeyFactory) {
+      this.externalIdKeyFactory = externalIdKeyFactory;
+    }
+
+    public AuthRequest create(ExternalId.Key externalIdKey) {
+      return new AuthRequest(externalIdKey, externalIdKeyFactory);
+    }
+
+    /** Create a request for a local username, such as from LDAP. */
+    public AuthRequest createForUser(String username) {
+      AuthRequest r =
+          new AuthRequest(
+              externalIdKeyFactory.create(SCHEME_GERRIT, username), externalIdKeyFactory);
+      r.setUserName(username);
+      return r;
+    }
+
+    /** Create a request for an external username. */
+    public AuthRequest createForExternalUser(String username) {
+      AuthRequest r =
+          new AuthRequest(
+              externalIdKeyFactory.create(SCHEME_EXTERNAL, username), externalIdKeyFactory);
+      r.setUserName(username);
+      return r;
+    }
+
+    /**
+     * Create a request for an email address registration.
+     *
+     * <p>This type of request should be used only to attach a new email address to an existing user
+     * account.
+     */
+    public AuthRequest createForEmail(String email) {
+      AuthRequest r =
+          new AuthRequest(externalIdKeyFactory.create(SCHEME_MAILTO, email), externalIdKeyFactory);
+      r.setEmailAddress(email);
+      return r;
+    }
   }
 
-  /** Create a request for an external username. */
-  public static AuthRequest forExternalUser(String username) {
-    AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, username));
-    r.setUserName(username);
-    return r;
-  }
-
-  /**
-   * Create a request for an email address registration.
-   *
-   * <p>This type of request should be used only to attach a new email address to an existing user
-   * account.
-   */
-  public static AuthRequest forEmail(String email) {
-    AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_MAILTO, email));
-    r.setEmailAddress(email);
-    return r;
-  }
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   private ExternalId.Key externalId;
   private String password;
@@ -69,8 +93,9 @@
   private boolean authProvidesAccountActiveStatus;
   private boolean active;
 
-  public AuthRequest(ExternalId.Key externalId) {
+  private AuthRequest(ExternalId.Key externalId, ExternalIdKeyFactory externalIdKeyFactory) {
     this.externalId = externalId;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   public ExternalId.Key getExternalIdKey() {
@@ -86,7 +111,7 @@
 
   public void setLocalUser(String localUser) {
     if (externalId.isScheme(SCHEME_GERRIT)) {
-      externalId = ExternalId.Key.create(SCHEME_GERRIT, localUser);
+      externalId = externalIdKeyFactory.create(SCHEME_GERRIT, localUser);
     }
   }
 
diff --git a/java/com/google/gerrit/server/account/GroupBackend.java b/java/com/google/gerrit/server/account/GroupBackend.java
index d6360c5..91edaf2 100644
--- a/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/java/com/google/gerrit/server/account/GroupBackend.java
@@ -26,7 +26,7 @@
 /** Implementations of GroupBackend provide lookup and membership accessors to a group system. */
 @ExtensionPoint
 public interface GroupBackend {
-  /** @return {@code true} if the backend can operate on the UUID. */
+  /** Returns {@code true} if the backend can operate on the UUID. */
   boolean handles(AccountGroup.UUID uuid);
 
   /**
@@ -38,12 +38,12 @@
   @Nullable
   GroupDescription.Basic get(AccountGroup.UUID uuid);
 
-  /** @return suggestions for the group name sorted by name. */
+  /** Returns suggestions for the group name sorted by name. */
   Collection<GroupReference> suggest(String name, @Nullable ProjectState project);
 
-  /** @return the group membership checker for the backend. */
+  /** Returns the group membership checker for the backend. */
   GroupMembership membershipsOf(CurrentUser user);
 
-  /** @return {@code true} if the group with the given UUID is visible to all registered users. */
+  /** Returns {@code true} if the group with the given UUID is visible to all registered users. */
   boolean isVisibleToAll(AccountGroup.UUID uuid);
 }
diff --git a/java/com/google/gerrit/server/account/GroupCache.java b/java/com/google/gerrit/server/account/GroupCache.java
index d8cac71..1e28d7d 100644
--- a/java/com/google/gerrit/server/account/GroupCache.java
+++ b/java/com/google/gerrit/server/account/GroupCache.java
@@ -103,6 +103,10 @@
    */
   void evict(AccountGroup.UUID groupUuid);
 
-  /** @see #evict(AccountGroup.UUID) */
+  /**
+   * Removes the association of the given UUIDs with groups
+   *
+   * <p>See {@link #evict(AccountGroup.UUID)}
+   */
   void evict(Collection<AccountGroup.UUID> groupUuid);
 }
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCache.java b/java/com/google/gerrit/server/account/GroupIncludeCache.java
index 6547619..d92d9fc 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -37,7 +37,7 @@
    */
   Collection<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId);
 
-  /** @return set of any UUIDs that are not internal groups. */
+  /** Returns set of any UUIDs that are not internal groups. */
   Collection<AccountGroup.UUID> allExternalMembers();
 
   void evictGroupsWithMember(Account.Id memberId);
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 13b71cf..9d684ac 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -14,17 +14,19 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.common.collect.Streams.stream;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
+import static java.util.stream.Stream.concat;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.AccountInfo.Tags;
 import com.google.gerrit.extensions.common.AvatarInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -45,6 +47,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Stream;
 
 @Singleton
 public class InternalAccountDirectory extends AccountDirectory {
@@ -63,6 +66,7 @@
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
   private final ServiceUserClassifier serviceUserClassifier;
+  private final DynamicMap<AccountTagProvider> accountTagProviders;
 
   @Inject
   InternalAccountDirectory(
@@ -71,13 +75,15 @@
       IdentifiedUser.GenericFactory userFactory,
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
-      ServiceUserClassifier serviceUserClassifier) {
+      ServiceUserClassifier serviceUserClassifier,
+      DynamicMap<AccountTagProvider> accountTagProviders) {
     this.accountCache = accountCache;
     this.avatar = avatar;
     this.userFactory = userFactory;
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.serviceUserClassifier = serviceUserClassifier;
+    this.accountTagProviders = accountTagProviders;
   }
 
   @Override
@@ -102,7 +108,7 @@
 
     Set<FillOptions> fillOptionsWithoutSecondaryEmails =
         Sets.difference(options, EnumSet.of(FillOptions.SECONDARY_EMAILS));
-    Set<Account.Id> ids = Streams.stream(in).map(a -> Account.id(a._accountId)).collect(toSet());
+    Set<Account.Id> ids = stream(in).map(a -> Account.id(a._accountId)).collect(toSet());
     Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
     for (AccountInfo info : in) {
       Account.Id id = Account.id(info._accountId);
@@ -160,10 +166,10 @@
     }
 
     if (options.contains(FillOptions.TAGS)) {
-      info.tags =
-          serviceUserClassifier.isServiceUser(account.id())
-              ? ImmutableList.of(AccountInfo.Tag.SERVICE_USER)
-              : null;
+      List<String> tags = getTags(account.id());
+      if (!tags.isEmpty()) {
+        info.tags = tags;
+      }
     }
 
     if (options.contains(FillOptions.AVATARS)) {
@@ -194,6 +200,15 @@
         .collect(toList());
   }
 
+  private List<String> getTags(Account.Id id) {
+    Stream<String> tagsFromProviders =
+        stream(accountTagProviders.iterator())
+            .flatMap(accountTagProvider -> accountTagProvider.get().getTags(id).stream());
+    Stream<String> tagsFromServiceUserClassifier =
+        serviceUserClassifier.isServiceUser(id) ? Stream.of(Tags.SERVICE_USER) : Stream.empty();
+    return concat(tagsFromProviders, tagsFromServiceUserClassifier).collect(toList());
+  }
+
   private static void addAvatar(
       AvatarProvider provider, AccountInfo account, IdentifiedUser user, int size) {
     String url = provider.getUrl(user, size);
diff --git a/java/com/google/gerrit/server/account/Realm.java b/java/com/google/gerrit/server/account/Realm.java
index d56ed07..3f642f7 100644
--- a/java/com/google/gerrit/server/account/Realm.java
+++ b/java/com/google/gerrit/server/account/Realm.java
@@ -41,10 +41,10 @@
 
   void onCreateAccount(AuthRequest who, Account account);
 
-  /** @return true if the user has the given email address. */
+  /** Returns true if the user has the given email address. */
   boolean hasEmailAddress(IdentifiedUser who, String email);
 
-  /** @return all known email addresses for the identified user. */
+  /** Returns all known email addresses for the identified user. */
   Set<String> getEmailAddresses(IdentifiedUser who);
 
   /**
@@ -56,19 +56,13 @@
    */
   Account.Id lookup(String accountName) throws IOException;
 
-  /**
-   * @return true if the account is active.
-   * @throws NamingException
-   * @throws LoginException
-   * @throws AccountException
-   * @throws IOException
-   */
+  /** Returns true if the account is active. */
   default boolean isActive(@SuppressWarnings("unused") String username)
       throws LoginException, NamingException, AccountException, IOException {
     return true;
   }
 
-  /** @return true if the account is backed by the realm, false otherwise. */
+  /** Returns true if the account is backed by the realm, false otherwise. */
   default boolean accountBelongsToRealm(
       @SuppressWarnings("unused") Collection<ExternalId> externalIds) {
     return false;
diff --git a/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java b/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
index 27ac9f4..db030f9 100644
--- a/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
+++ b/java/com/google/gerrit/server/account/ServiceUserClassifierImpl.java
@@ -76,7 +76,7 @@
               .get(toTraverse.remove(0))
               .orElseThrow(() -> new IllegalStateException("invalid subgroup"));
       if (seen.contains(currentGroup.getGroupUUID())) {
-        logger.atWarning().log(
+        logger.atFine().log(
             "Skipping %s because it's a cyclic subgroup", currentGroup.getGroupUUID());
         continue;
       }
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 30021e6..555a2c1 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -206,7 +206,6 @@
    *
    * @param pub the public SSH key to be added
    * @return the new SSH key
-   * @throws InvalidSshKeyException
    */
   private AccountSshKey addKey(String pub) throws InvalidSshKeyException {
     checkLoaded();
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index f1fc5cb..e718bcb 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -68,7 +68,8 @@
       ExternalIdProto.Builder b =
           ExternalIdProto.newBuilder()
               .setKey(externalId.key().get())
-              .setAccountId(externalId.accountId().get());
+              .setAccountId(externalId.accountId().get())
+              .setIsCaseInsensitive(externalId.isCaseInsensitive());
       if (externalId.email() != null) {
         b.setEmail(externalId.email());
       }
@@ -91,7 +92,7 @@
 
     private static ExternalId toExternalId(ObjectIdConverter idConverter, ExternalIdProto proto) {
       return ExternalId.create(
-          ExternalId.Key.parse(proto.getKey()),
+          ExternalId.Key.parse(proto.getKey(), proto.getIsCaseInsensitive()),
           Account.id(proto.getAccountId()),
           // ExternalId treats null and empty strings the same, so no need to distinguish here.
           proto.getEmail(),
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 268812c..30f4094 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -17,35 +17,29 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.git.ObjectIds;
-import com.google.gerrit.server.account.HashedPassword;
 import java.io.Serializable;
 import java.util.Collection;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 
 @AutoValue
 public abstract class ExternalId implements Serializable {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   // If these regular expressions are modified the same modifications should be done to the
   // corresponding regular expressions in the
   // com.google.gerrit.client.account.UsernameField class.
@@ -106,10 +100,10 @@
 
   private static final long serialVersionUID = 1L;
 
-  private static final String EXTERNAL_ID_SECTION = "externalId";
-  private static final String ACCOUNT_ID_KEY = "accountId";
-  private static final String EMAIL_KEY = "email";
-  private static final String PASSWORD_KEY = "password";
+  static final String EXTERNAL_ID_SECTION = "externalId";
+  static final String ACCOUNT_ID_KEY = "accountId";
+  static final String EMAIL_KEY = "email";
+  static final String PASSWORD_KEY = "password";
 
   /**
    * Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link
@@ -118,6 +112,8 @@
    * <p>The name {@code gerrit:} was a very poor choice.
    *
    * <p>Scheme names must not contain colons (':').
+   *
+   * <p>Will be handled case insensitive, if auth.userNameCaseInsensitive = true.
    */
   public static final String SCHEME_GERRIT = "gerrit";
 
@@ -127,7 +123,11 @@
   /** Scheme used to represent only an email address. */
   public static final String SCHEME_MAILTO = "mailto";
 
-  /** Scheme for the username used to authenticate an account, e.g. over SSH. */
+  /**
+   * Scheme for the username used to authenticate an account, e.g. over SSH.
+   *
+   * <p>Will be handled case insensitive, if auth.userNameCaseInsensitive = true.
+   */
   public static final String SCHEME_USERNAME = "username";
 
   /** Scheme used for GPG public keys. */
@@ -154,10 +154,12 @@
      *
      * @param scheme the scheme name, must not contain colons (':'), can be {@code null}
      * @param id the external ID, must not contain colons (':')
+     * @param isCaseInsensitive whether the external ID key is matched case insensitively
      * @return the created external ID key
      */
-    public static Key create(@Nullable String scheme, String id) {
-      return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id);
+    @VisibleForTesting
+    public static Key create(@Nullable String scheme, String id, boolean isCaseInsensitive) {
+      return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id, isCaseInsensitive);
     }
 
     /**
@@ -165,18 +167,21 @@
      *
      * @return the parsed external ID key
      */
-    public static Key parse(String externalId) {
+    @VisibleForTesting
+    public static Key parse(String externalId, boolean isCaseInsensitive) {
       int c = externalId.indexOf(':');
       if (c < 1 || c >= externalId.length() - 1) {
-        return create(null, externalId);
+        return create(null, externalId, isCaseInsensitive);
       }
-      return create(externalId.substring(0, c), externalId.substring(c + 1));
+      return create(externalId.substring(0, c), externalId.substring(c + 1), isCaseInsensitive);
     }
 
     public abstract @Nullable String scheme();
 
     public abstract String id();
 
+    public abstract boolean isCaseInsensitive();
+
     public boolean isScheme(String scheme) {
       return scheme.equals(scheme());
     }
@@ -186,8 +191,10 @@
      * notes branch.
      */
     @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
+    @Memoized
     public ObjectId sha1() {
-      return ObjectId.fromRaw(Hashing.sha1().hashString(get(), UTF_8).asBytes());
+      String keyString = isCaseInsensitive() ? get().toLowerCase(Locale.US) : get();
+      return ObjectId.fromRaw(Hashing.sha1().hashString(keyString, UTF_8).asBytes());
     }
 
     /**
@@ -209,100 +216,27 @@
       return get();
     }
 
+    @Override
+    public final boolean equals(Object obj) {
+      if (!(obj instanceof ExternalId.Key)) {
+        return false;
+      }
+      ExternalId.Key o = (ExternalId.Key) obj;
+
+      return sha1().equals(o.sha1());
+    }
+
+    @Override
+    @Memoized
+    public int hashCode() {
+      return Objects.hash(sha1());
+    }
+
     public static ImmutableSet<ExternalId.Key> from(Collection<ExternalId> extIds) {
       return extIds.stream().map(ExternalId::key).collect(toImmutableSet());
     }
   }
 
-  /**
-   * Creates an external ID.
-   *
-   * @param scheme the scheme name, must not contain colons (':')
-   * @param id the external ID, must not contain colons (':')
-   * @param accountId the ID of the account to which the external ID belongs
-   * @return the created external ID
-   */
-  public static ExternalId create(String scheme, String id, Account.Id accountId) {
-    return create(Key.create(scheme, id), accountId, null, null);
-  }
-
-  /**
-   * Creates an external ID.
-   *
-   * @param scheme the scheme name, must not contain colons (':')
-   * @param id the external ID, must not contain colons (':')
-   * @param accountId the ID of the account to which the external ID belongs
-   * @param email the email of the external ID, may be {@code null}
-   * @param hashedPassword the hashed password of the external ID, may be {@code null}
-   * @return the created external ID
-   */
-  public static ExternalId create(
-      String scheme,
-      String id,
-      Account.Id accountId,
-      @Nullable String email,
-      @Nullable String hashedPassword) {
-    return create(Key.create(scheme, id), accountId, email, hashedPassword);
-  }
-
-  public static ExternalId create(Key key, Account.Id accountId) {
-    return create(key, accountId, null, null);
-  }
-
-  public static ExternalId create(
-      Key key, Account.Id accountId, @Nullable String email, @Nullable String hashedPassword) {
-    return create(
-        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
-  }
-
-  public static ExternalId createWithPassword(
-      Key key, Account.Id accountId, @Nullable String email, @Nullable String plainPassword) {
-    plainPassword = Strings.emptyToNull(plainPassword);
-    String hashedPassword =
-        plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
-    return create(key, accountId, email, hashedPassword);
-  }
-
-  /**
-   * Create a external ID for a username (scheme "username").
-   *
-   * @param id the external ID, must not contain colons (':')
-   * @param accountId the ID of the account to which the external ID belongs
-   * @param plainPassword the plain HTTP password, may be {@code null}
-   * @return the created external ID
-   */
-  public static ExternalId createUsername(
-      String id, Account.Id accountId, @Nullable String plainPassword) {
-    return createWithPassword(Key.create(SCHEME_USERNAME, id), accountId, null, plainPassword);
-  }
-
-  /**
-   * Creates an external ID with an email.
-   *
-   * @param scheme the scheme name, must not contain colons (':')
-   * @param id the external ID, must not contain colons (':')
-   * @param accountId the ID of the account to which the external ID belongs
-   * @param email the email of the external ID, may be {@code null}
-   * @return the created external ID
-   */
-  public static ExternalId createWithEmail(
-      String scheme, String id, Account.Id accountId, @Nullable String email) {
-    return createWithEmail(Key.create(scheme, id), accountId, email);
-  }
-
-  public static ExternalId createWithEmail(Key key, Account.Id accountId, @Nullable String email) {
-    return create(key, accountId, Strings.emptyToNull(email), null);
-  }
-
-  public static ExternalId createEmail(Account.Id accountId, String email) {
-    return createWithEmail(SCHEME_MAILTO, email, accountId, requireNonNull(email));
-  }
-
-  static ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
-    return new AutoValue_ExternalId(
-        extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
-  }
-
   @VisibleForTesting
   public static ExternalId create(
       Key key,
@@ -311,111 +245,20 @@
       @Nullable String hashedPassword,
       @Nullable ObjectId blobId) {
     return new AutoValue_ExternalId(
-        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
-  }
-
-  /**
-   * Parses an external ID from a byte array that contain the external ID as an Git config file
-   * text.
-   *
-   * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
-   * email and password:
-   *
-   * <pre>
-   * [externalId "username:jdoe"]
-   *   accountId = 1003407
-   *   email = jdoe@example.com
-   *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
-   * </pre>
-   */
-  public static ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
-      throws ConfigInvalidException {
-    requireNonNull(blobId);
-
-    Config externalIdConfig = new Config();
-    try {
-      externalIdConfig.fromText(new String(raw, UTF_8));
-    } catch (ConfigInvalidException e) {
-      throw invalidConfig(noteId, e.getMessage());
-    }
-
-    Set<String> externalIdKeys = externalIdConfig.getSubsections(EXTERNAL_ID_SECTION);
-    if (externalIdKeys.size() != 1) {
-      throw invalidConfig(
-          noteId,
-          String.format(
-              "Expected exactly 1 '%s' section, found %d",
-              EXTERNAL_ID_SECTION, externalIdKeys.size()));
-    }
-
-    String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
-    Key externalIdKey = Key.parse(externalIdKeyStr);
-    if (externalIdKey == null) {
-      throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
-    }
-
-    if (!externalIdKey.sha1().getName().equals(noteId)) {
-      throw invalidConfig(
-          noteId,
-          String.format(
-              "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
-    }
-
-    String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
-    String password =
-        externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY);
-    int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
-
-    return create(
-        externalIdKey,
-        Account.id(accountId),
+        key,
+        accountId,
+        key.isCaseInsensitive(),
         Strings.emptyToNull(email),
-        Strings.emptyToNull(password),
+        Strings.emptyToNull(hashedPassword),
         blobId);
   }
 
-  private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
-      throws ConfigInvalidException {
-    String accountIdStr =
-        externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY);
-    if (accountIdStr == null) {
-      throw invalidConfig(
-          noteId,
-          String.format(
-              "Value for '%s.%s.%s' is missing, expected account ID",
-              EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
-    }
-
-    try {
-      int accountId =
-          externalIdConfig.getInt(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY, -1);
-      if (accountId < 0) {
-        throw invalidConfig(
-            noteId,
-            String.format(
-                "Value %s for '%s.%s.%s' is invalid, expected account ID",
-                accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
-      }
-      return accountId;
-    } catch (IllegalArgumentException e) {
-      String msg =
-          String.format(
-              "Value %s for '%s.%s.%s' is invalid, expected account ID",
-              accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY);
-      logger.atSevere().withCause(e).log(msg);
-      throw invalidConfig(noteId, msg);
-    }
-  }
-
-  private static ConfigInvalidException invalidConfig(String noteId, String message) {
-    return new ConfigInvalidException(
-        String.format("Invalid external ID config for note '%s': %s", noteId, message));
-  }
-
   public abstract Key key();
 
   public abstract Account.Id accountId();
 
+  public abstract boolean isCaseInsensitive();
+
   public abstract @Nullable String email();
 
   public abstract @Nullable String password();
@@ -456,13 +299,15 @@
     ExternalId o = (ExternalId) obj;
     return Objects.equals(key(), o.key())
         && Objects.equals(accountId(), o.accountId())
+        && Objects.equals(isCaseInsensitive(), o.isCaseInsensitive())
         && Objects.equals(email(), o.email())
         && Objects.equals(password(), o.password());
   }
 
+  @Memoized
   @Override
-  public final int hashCode() {
-    return Objects.hash(key(), accountId(), email(), password());
+  public int hashCode() {
+    return Objects.hash(key(), accountId(), isCaseInsensitive(), email(), password());
   }
 
   /**
@@ -479,7 +324,8 @@
    * </pre>
    */
   @Override
-  public final String toString() {
+  @Memoized
+  public String toString() {
     Config c = new Config();
     writeToConfig(c);
     return c.toText();
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
index 902c18b..72d703b 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
@@ -73,6 +73,7 @@
   private final Timer0 reloadDifferential;
   private final boolean enablePartialReloads;
   private final boolean isPersistentCache;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   ExternalIdCacheLoader(
@@ -82,7 +83,8 @@
       @Named(ExternalIdCacheImpl.CACHE_NAME)
           Provider<Cache<ObjectId, AllExternalIds>> externalIdCache,
       MetricMaker metricMaker,
-      @GerritServerConfig Config config) {
+      @GerritServerConfig Config config,
+      ExternalIdFactory externalIdFactory) {
     this.externalIdReader = externalIdReader;
     this.externalIdCache = externalIdCache;
     this.gitRepositoryManager = gitRepositoryManager;
@@ -93,7 +95,9 @@
             new Description("Total number of external ID cache reloads from Git.")
                 .setRate()
                 .setUnit("updates"),
-            Field.ofBoolean("partial", Metadata.Builder::partial).build());
+            Field.ofBoolean("partial", Metadata.Builder::partial)
+                .description("Whether the reload was partial.")
+                .build());
     this.reloadDifferential =
         metricMaker.newTimer(
             "notedb/external_id_partial_read_latency",
@@ -105,6 +109,7 @@
         config.getBoolean("cache", ExternalIdCacheImpl.CACHE_NAME, "enablePartialReloads", true);
     this.isPersistentCache =
         config.getInt("cache", ExternalIdCacheImpl.CACHE_NAME, "diskLimit", 0) > 0;
+    this.externalIdFactory = externalIdFactory;
   }
 
   @Override
@@ -216,7 +221,7 @@
    * @param additions map of name to blob ID for each external ID that should be added
    * @param removals set of name {@link ObjectId}s that should be removed
    */
-  private static AllExternalIds buildAllExternalIds(
+  private AllExternalIds buildAllExternalIds(
       Repository repo,
       AllExternalIds oldExternalIds,
       Map<ObjectId, ObjectId> additions,
@@ -245,7 +250,7 @@
         ExternalId parsedExternalId;
         try {
           parsedExternalId =
-              ExternalId.parse(
+              externalIdFactory.parse(
                   nameToBlob.getKey().name(),
                   reader.open(nameToBlob.getValue()).getCachedBytes(),
                   nameToBlob.getValue());
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheModule.java
new file mode 100644
index 0000000..f0ad1b2
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheModule.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
+import com.google.inject.TypeLiteral;
+import java.time.Duration;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ExternalIdCacheModule extends CacheModule {
+  @Override
+  protected void configure() {
+    persist(ExternalIdCacheImpl.CACHE_NAME, ObjectId.class, new TypeLiteral<AllExternalIds>() {})
+        // The cached data is potentially pretty large and we are always only interested
+        // in the latest value. However, due to a race condition, it is possible for different
+        // threads to observe different values of the meta ref, and hence request different keys
+        // from the cache. Extend the cache size by 1 to cover this case, but expire the extra
+        // object after a short period of time, since it may be a potentially large amount of
+        // memory.
+        // When loading a new value because the primary data advanced, we want to leverage the old
+        // cache state to recompute only what changed. This doesn't affect cache size though as
+        // Guava calls the loader first and evicts later on.
+        .maximumWeight(2)
+        .expireFromMemoryAfterAccess(Duration.ofMinutes(1))
+        .loader(ExternalIdCacheLoader.class)
+        .diskLimit(-1)
+        .version(1)
+        .keySerializer(ObjectIdCacheSerializer.INSTANCE)
+        .valueSerializer(AllExternalIds.Serializer.INSTANCE);
+
+    bind(ExternalIdCacheImpl.class);
+    bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class);
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
new file mode 100644
index 0000000..ee42d67
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Strings;
+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.server.account.HashedPassword;
+import com.google.gerrit.server.account.externalids.ExternalId.Key;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+
+@Singleton
+public class ExternalIdFactory {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+
+  @Inject
+  public ExternalIdFactory(ExternalIdKeyFactory externalIdKeyFactory) {
+    this.externalIdKeyFactory = externalIdKeyFactory;
+  }
+
+  /**
+   * Creates an external ID.
+   *
+   * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
+   *     ExternalId#SCHEME_USERNAME}.
+   * @param id the external ID, must not contain colons (':')
+   * @param accountId the ID of the account to which the external ID belongs
+   * @return the created external ID
+   */
+  public ExternalId create(String scheme, String id, Account.Id accountId) {
+    return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
+  }
+
+  /**
+   * Creates an external ID.
+   *
+   * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
+   *     ExternalId#SCHEME_USERNAME}.
+   * @param id the external ID, must not contain colons (':')
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @param hashedPassword the hashed password of the external ID, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId create(
+      String scheme,
+      String id,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String hashedPassword) {
+    return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
+  }
+
+  /**
+   * Creates an external ID.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @return the created external ID
+   */
+  public ExternalId create(Key key, Account.Id accountId) {
+    return create(key, accountId, null, null);
+  }
+
+  /**
+   * Creates an external ID.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @param hashedPassword the hashed password of the external ID, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId create(
+      Key key, Account.Id accountId, @Nullable String email, @Nullable String hashedPassword) {
+    return create(
+        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
+  }
+
+  /**
+   * Creates an external ID adding a hashed password computed from a plain password.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @param plainPassword the plain HTTP password, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId createWithPassword(
+      Key key, Account.Id accountId, @Nullable String email, @Nullable String plainPassword) {
+    plainPassword = Strings.emptyToNull(plainPassword);
+    String hashedPassword =
+        plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
+    return create(key, accountId, email, hashedPassword);
+  }
+
+  /**
+   * Create a external ID for a username (scheme "username").
+   *
+   * @param id the external ID, must not contain colons (':')
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param plainPassword the plain HTTP password, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId createUsername(
+      String id, Account.Id accountId, @Nullable String plainPassword) {
+    return createWithPassword(
+        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
+        accountId,
+        null,
+        plainPassword);
+  }
+
+  /**
+   * Creates an external ID with an email.
+   *
+   * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
+   *     ExternalId#SCHEME_USERNAME}.
+   * @param id the external ID, must not contain colons (':')
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId createWithEmail(
+      String scheme, String id, Account.Id accountId, @Nullable String email) {
+    return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
+  }
+
+  /**
+   * Creates an external ID with an email.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId createWithEmail(Key key, Account.Id accountId, @Nullable String email) {
+    return create(key, accountId, Strings.emptyToNull(email), null);
+  }
+
+  /**
+   * Creates an external ID using the `mailto`-scheme.
+   *
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @return the created external ID
+   */
+  public ExternalId createEmail(Account.Id accountId, String email) {
+    return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
+  }
+
+  ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
+    return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
+  }
+
+  /**
+   * Creates an external ID.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @param hashedPassword the hashed password of the external ID, may be {@code null}
+   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+   *     {@code null} if the external ID was created in code and is not yet stored in Git.
+   * @return the created external ID
+   */
+  public ExternalId create(
+      Key key,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String hashedPassword,
+      @Nullable ObjectId blobId) {
+    return ExternalId.create(
+        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
+  }
+
+  /**
+   * Parses an external ID from a byte array that contains the external ID as a Git config file
+   * text.
+   *
+   * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
+   * email and password:
+   *
+   * <pre>
+   * [externalId "username:jdoe"]
+   *   accountId = 1003407
+   *   email = jdoe@example.com
+   *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
+   * </pre>
+   *
+   * @param noteId the SHA-1 sum of the external ID used as the note's ID
+   * @param raw a byte array that contains the external ID as a Git config file text.
+   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+   *     {@code null} if the external ID was created in code and is not yet stored in Git.
+   * @return the parsed external ID
+   */
+  public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
+      throws ConfigInvalidException {
+    requireNonNull(blobId);
+
+    Config externalIdConfig = new Config();
+    try {
+      externalIdConfig.fromText(new String(raw, UTF_8));
+    } catch (ConfigInvalidException e) {
+      throw invalidConfig(noteId, e.getMessage());
+    }
+
+    Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
+    if (externalIdKeys.size() != 1) {
+      throw invalidConfig(
+          noteId,
+          String.format(
+              "Expected exactly 1 '%s' section, found %d",
+              ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
+    }
+
+    String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
+    Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
+    if (externalIdKey == null) {
+      throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
+    }
+
+    if (!externalIdKey.sha1().getName().equals(noteId)) {
+      throw invalidConfig(
+          noteId,
+          String.format(
+              "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
+    }
+
+    String email =
+        externalIdConfig.getString(
+            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
+    String password =
+        externalIdConfig.getString(
+            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
+    int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
+
+    return create(
+        externalIdKey,
+        Account.id(accountId),
+        Strings.emptyToNull(email),
+        Strings.emptyToNull(password),
+        blobId);
+  }
+
+  private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
+      throws ConfigInvalidException {
+    String accountIdStr =
+        externalIdConfig.getString(
+            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
+    if (accountIdStr == null) {
+      throw invalidConfig(
+          noteId,
+          String.format(
+              "Value for '%s.%s.%s' is missing, expected account ID",
+              ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
+    }
+
+    try {
+      int accountId =
+          externalIdConfig.getInt(
+              ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
+      if (accountId < 0) {
+        throw invalidConfig(
+            noteId,
+            String.format(
+                "Value %s for '%s.%s.%s' is invalid, expected account ID",
+                accountIdStr,
+                ExternalId.EXTERNAL_ID_SECTION,
+                externalIdKeyStr,
+                ExternalId.ACCOUNT_ID_KEY));
+      }
+      return accountId;
+    } catch (IllegalArgumentException e) {
+      String msg =
+          String.format(
+              "Value %s for '%s.%s.%s' is invalid, expected account ID",
+              accountIdStr,
+              ExternalId.EXTERNAL_ID_SECTION,
+              externalIdKeyStr,
+              ExternalId.ACCOUNT_ID_KEY);
+      logger.atSevere().withCause(e).log(msg);
+      throw invalidConfig(noteId, msg);
+    }
+  }
+
+  private static ConfigInvalidException invalidConfig(String noteId, String message) {
+    return new ConfigInvalidException(
+        String.format("Invalid external ID config for note '%s': %s", noteId, message));
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
new file mode 100644
index 0000000..95df4a9
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdKeyFactory.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.ImplementedBy;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class ExternalIdKeyFactory {
+  @ImplementedBy(ConfigImpl.class)
+  public interface Config {
+    boolean isUserNameCaseInsensitive();
+  }
+
+  /**
+   * Default implementation {@link Config}
+   *
+   * <p>Internally in google we are using different implementation.
+   */
+  @Singleton
+  public static class ConfigImpl implements Config {
+    private final boolean isUserNameCaseInsensitive;
+
+    @VisibleForTesting
+    @Inject
+    public ConfigImpl(AuthConfig authConfig) {
+      this.isUserNameCaseInsensitive = authConfig.isUserNameCaseInsensitive();
+    }
+
+    @Override
+    public boolean isUserNameCaseInsensitive() {
+      return isUserNameCaseInsensitive;
+    }
+  }
+
+  private final boolean isUserNameCaseInsensitive;
+
+  @Inject
+  public ExternalIdKeyFactory(Config config) {
+    this.isUserNameCaseInsensitive = config.isUserNameCaseInsensitive();
+  }
+
+  /**
+   * Creates an external ID key.
+   *
+   * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
+   *     ExternalId#SCHEME_USERNAME}.
+   * @param id the external ID, must not contain colons (':')
+   * @return the created external ID key
+   */
+  public ExternalId.Key create(@Nullable String scheme, String id) {
+    if (scheme != null
+        && (scheme.equals(ExternalId.SCHEME_USERNAME) || scheme.equals(ExternalId.SCHEME_GERRIT))) {
+      return ExternalId.Key.create(scheme, id, isUserNameCaseInsensitive);
+    }
+
+    return ExternalId.Key.create(scheme, id, false);
+  }
+
+  /**
+   * Parses an external ID key from its String representation
+   *
+   * @param externalId String representation of external ID key (e.g. username:johndoe)
+   * @return the external Id key object
+   */
+  public ExternalId.Key parse(String externalId) {
+    int c = externalId.indexOf(':');
+    if (c < 1 || c >= externalId.length() - 1) {
+      return create(null, externalId);
+    }
+    return create(externalId.substring(0, c), externalId.substring(c + 1));
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
index 3e5d7b8..da7b357 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2021 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,34 +14,13 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
-import com.google.inject.TypeLiteral;
-import java.time.Duration;
-import org.eclipse.jgit.lib.ObjectId;
+import com.google.inject.AbstractModule;
 
-public class ExternalIdModule extends CacheModule {
+public class ExternalIdModule extends AbstractModule {
   @Override
   protected void configure() {
-    persist(ExternalIdCacheImpl.CACHE_NAME, ObjectId.class, new TypeLiteral<AllExternalIds>() {})
-        // The cached data is potentially pretty large and we are always only interested
-        // in the latest value. However, due to a race condition, it is possible for different
-        // threads to observe different values of the meta ref, and hence request different keys
-        // from the cache. Extend the cache size by 1 to cover this case, but expire the extra
-        // object after a short period of time, since it may be a potentially large amount of
-        // memory.
-        // When loading a new value because the primary data advanced, we want to leverage the old
-        // cache state to recompute only what changed. This doesn't affect cache size though as
-        // Guava calls the loader first and evicts later on.
-        .maximumWeight(2)
-        .expireFromMemoryAfterAccess(Duration.ofMinutes(1))
-        .loader(ExternalIdCacheLoader.class)
-        .diskLimit(-1)
-        .version(1)
-        .keySerializer(ObjectIdCacheSerializer.INSTANCE)
-        .valueSerializer(AllExternalIds.Serializer.INSTANCE);
-
-    bind(ExternalIdCacheImpl.class);
-    bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class);
+    bind(ExternalIdFactory.class);
+    bind(ExternalIdKeyFactory.class);
+    bind(PasswordVerifier.class);
   }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 435a524..2b9c00a9 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -99,16 +99,19 @@
     protected final MetricMaker metricMaker;
     protected final AllUsersName allUsersName;
     protected final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
+    protected final ExternalIdFactory externalIdFactory;
 
     protected ExternalIdNotesLoader(
         ExternalIdCache externalIdCache,
         MetricMaker metricMaker,
         AllUsersName allUsersName,
-        DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
+        DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
+        ExternalIdFactory externalIdFactory) {
       this.externalIdCache = externalIdCache;
       this.metricMaker = metricMaker;
       this.allUsersName = allUsersName;
       this.upsertPreprocessors = upsertPreprocessors;
+      this.externalIdFactory = externalIdFactory;
     }
 
     /**
@@ -199,22 +202,25 @@
         Provider<AccountIndexer> accountIndexer,
         MetricMaker metricMaker,
         AllUsersName allUsersName,
-        DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
-      super(externalIdCache, metricMaker, allUsersName, upsertPreprocessors);
+        DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
+        ExternalIdFactory externalIdFactory) {
+      super(externalIdCache, metricMaker, allUsersName, upsertPreprocessors, externalIdFactory);
       this.accountIndexer = accountIndexer;
     }
 
     @Override
     public ExternalIdNotes load(Repository allUsersRepo)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+      return new ExternalIdNotes(
+              metricMaker, allUsersName, allUsersRepo, upsertPreprocessors, externalIdFactory)
           .load();
     }
 
     @Override
     public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+      return new ExternalIdNotes(
+              metricMaker, allUsersName, allUsersRepo, upsertPreprocessors, externalIdFactory)
           .load(rev);
     }
 
@@ -232,14 +238,16 @@
         ExternalIdCache externalIdCache,
         MetricMaker metricMaker,
         AllUsersName allUsersName,
-        DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
-      super(externalIdCache, metricMaker, allUsersName, upsertPreprocessors);
+        DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
+        ExternalIdFactory externalIdFactory) {
+      super(externalIdCache, metricMaker, allUsersName, upsertPreprocessors, externalIdFactory);
     }
 
     @Override
     public ExternalIdNotes load(Repository allUsersRepo)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+      return new ExternalIdNotes(
+              metricMaker, allUsersName, allUsersRepo, upsertPreprocessors, externalIdFactory)
           .setNoReindex()
           .load();
     }
@@ -247,7 +255,8 @@
     @Override
     public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
         throws IOException, ConfigInvalidException {
-      return new ExternalIdNotes(metricMaker, allUsersName, allUsersRepo, upsertPreprocessors)
+      return new ExternalIdNotes(
+              metricMaker, allUsersName, allUsersRepo, upsertPreprocessors, externalIdFactory)
           .setNoReindex()
           .load(rev);
     }
@@ -269,10 +278,17 @@
    * @return read-only {@link ExternalIdNotes} instance
    */
   public static ExternalIdNotes loadReadOnly(
-      AllUsersName allUsersName, Repository allUsersRepo, @Nullable ObjectId rev)
+      AllUsersName allUsersName,
+      Repository allUsersRepo,
+      @Nullable ObjectId rev,
+      ExternalIdFactory externalIdFactory)
       throws IOException, ConfigInvalidException {
     return new ExternalIdNotes(
-            new DisabledMetricMaker(), allUsersName, allUsersRepo, DynamicMap.emptyMap())
+            new DisabledMetricMaker(),
+            allUsersName,
+            allUsersRepo,
+            DynamicMap.emptyMap(),
+            externalIdFactory)
         .setReadOnly()
         .setNoCacheUpdate()
         .setNoReindex()
@@ -290,10 +306,14 @@
    * @return {@link ExternalIdNotes} instance that doesn't updates caches on save
    */
   public static ExternalIdNotes loadNoCacheUpdate(
-      AllUsersName allUsersName, Repository allUsersRepo)
+      AllUsersName allUsersName, Repository allUsersRepo, ExternalIdFactory externalIdFactory)
       throws IOException, ConfigInvalidException {
     return new ExternalIdNotes(
-            new DisabledMetricMaker(), allUsersName, allUsersRepo, DynamicMap.emptyMap())
+            new DisabledMetricMaker(),
+            allUsersName,
+            allUsersRepo,
+            DynamicMap.emptyMap(),
+            externalIdFactory)
         .setNoCacheUpdate()
         .setNoReindex()
         .load();
@@ -304,6 +324,7 @@
   private final Repository repo;
   private final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
   private final CallerFinder callerFinder;
+  private final ExternalIdFactory externalIdFactory;
 
   private NoteMap noteMap;
   private ObjectId oldRev;
@@ -334,7 +355,8 @@
       MetricMaker metricMaker,
       AllUsersName allUsersName,
       Repository allUsersRepo,
-      DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors) {
+      DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
+      ExternalIdFactory externalIdFactory) {
     this.updateCount =
         metricMaker.newCounter(
             "notedb/external_id_update_count",
@@ -355,6 +377,7 @@
             // 3. direct callers
             .addTarget(ExternalIdNotes.class)
             .build();
+    this.externalIdFactory = externalIdFactory;
   }
 
   public ExternalIdNotes setAfterReadRevision(Runnable afterReadRevision) {
@@ -434,7 +457,7 @@
     try (RevWalk rw = new RevWalk(repo)) {
       ObjectId noteDataId = noteMap.get(noteId);
       byte[] raw = readNoteData(rw, noteDataId);
-      return Optional.of(ExternalId.parse(noteId.name(), raw, noteDataId));
+      return Optional.of(externalIdFactory.parse(noteId.name(), raw, noteDataId));
     }
   }
 
@@ -468,7 +491,7 @@
       for (Note note : noteMap) {
         byte[] raw = readNoteData(rw, note.getData());
         try {
-          b.add(ExternalId.parse(note.getName(), raw, note.getData()));
+          b.add(externalIdFactory.parse(note.getName(), raw, note.getData()));
         } catch (ConfigInvalidException | RuntimeException e) {
           logger.atSevere().withCause(e).log(
               "Ignoring invalid external ID note %s", note.getName());
@@ -840,8 +863,7 @@
    *
    * <p>If the external ID already exists, it is overwritten.
    */
-  private static ExternalId upsert(
-      RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
+  private ExternalId upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extId.key().sha1();
     Config c = new Config();
@@ -859,7 +881,7 @@
     byte[] raw = c.toText().getBytes(UTF_8);
     ObjectId noteData = ins.insert(OBJ_BLOB, raw);
     noteMap.set(noteId, noteData);
-    return ExternalId.create(extId, noteData);
+    return externalIdFactory.create(extId, noteData);
   }
 
   /**
@@ -868,7 +890,7 @@
    * @throws IllegalStateException is thrown if there is an existing external ID that has the same
    *     key, but otherwise doesn't match the specified external ID.
    */
-  private static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
+  private void remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extId.key().sha1();
     if (!noteMap.contains(noteId)) {
@@ -877,7 +899,7 @@
 
     ObjectId noteDataId = noteMap.get(noteId);
     byte[] raw = readNoteData(rw, noteDataId);
-    ExternalId actualExtId = ExternalId.parse(noteId.name(), raw, noteDataId);
+    ExternalId actualExtId = externalIdFactory.parse(noteId.name(), raw, noteDataId);
     checkState(
         extId.equals(actualExtId),
         "external id %s should be removed, but it doesn't match the actual external id %s",
@@ -894,8 +916,7 @@
    * @return the external ID that was removed, {@code null} if no external ID with the specified key
    *     exists
    */
-  @Nullable
-  private static ExternalId remove(
+  private ExternalId remove(
       RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
       throws IOException, ConfigInvalidException {
     ObjectId noteId = extIdKey.sha1();
@@ -905,7 +926,7 @@
 
     ObjectId noteDataId = noteMap.get(noteId);
     byte[] raw = readNoteData(rw, noteDataId);
-    ExternalId extId = ExternalId.parse(noteId.name(), raw, noteDataId);
+    ExternalId extId = externalIdFactory.parse(noteId.name(), raw, noteDataId);
     if (expectedAccountId != null) {
       checkState(
           expectedAccountId.equals(extId.accountId()),
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index f2505fa..0d715ae 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -69,10 +69,14 @@
   private boolean failOnLoad = false;
   private final Timer0 readAllLatency;
   private final Timer0 readSingleLatency;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   ExternalIdReader(
-      GitRepositoryManager repoManager, AllUsersName allUsersName, MetricMaker metricMaker) {
+      GitRepositoryManager repoManager,
+      AllUsersName allUsersName,
+      MetricMaker metricMaker,
+      ExternalIdFactory externalIdFactory) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.readAllLatency =
@@ -87,6 +91,7 @@
             new Description("Latency for reading a single external ID from NoteDb.")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS));
+    this.externalIdFactory = externalIdFactory;
   }
 
   @VisibleForTesting
@@ -106,7 +111,7 @@
 
     try (Timer0.Context ctx = readAllLatency.start();
         Repository repo = repoManager.openRepository(allUsersName)) {
-      return ExternalIdNotes.loadReadOnly(allUsersName, repo, null).all();
+      return ExternalIdNotes.loadReadOnly(allUsersName, repo, null, externalIdFactory).all();
     }
   }
 
@@ -125,7 +130,7 @@
 
     try (Timer0.Context ctx = readAllLatency.start();
         Repository repo = repoManager.openRepository(allUsersName)) {
-      return ExternalIdNotes.loadReadOnly(allUsersName, repo, rev).all();
+      return ExternalIdNotes.loadReadOnly(allUsersName, repo, rev, externalIdFactory).all();
     }
   }
 
@@ -135,7 +140,7 @@
 
     try (Timer0.Context ctx = readSingleLatency.start();
         Repository repo = repoManager.openRepository(allUsersName)) {
-      return ExternalIdNotes.loadReadOnly(allUsersName, repo, null).get(key);
+      return ExternalIdNotes.loadReadOnly(allUsersName, repo, null, externalIdFactory).get(key);
     }
   }
 
@@ -146,7 +151,7 @@
 
     try (Timer0.Context ctx = readSingleLatency.start();
         Repository repo = repoManager.openRepository(allUsersName)) {
-      return ExternalIdNotes.loadReadOnly(allUsersName, repo, rev).get(key);
+      return ExternalIdNotes.loadReadOnly(allUsersName, repo, rev, externalIdFactory).get(key);
     }
   }
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index 7b0dc57..4e1e524 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -53,7 +53,7 @@
   }
 
   /** Returns the specified external ID. */
-  public Optional<ExternalId> get(ExternalId.Key key) throws IOException, ConfigInvalidException {
+  public Optional<ExternalId> get(ExternalId.Key key) throws IOException {
     return externalIdCache.byKey(key);
   }
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
index 6a4da09..cf0e5d3 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -43,29 +43,32 @@
   private final AllUsersName allUsers;
   private final AccountCache accountCache;
   private final OutgoingEmailValidator validator;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   ExternalIdsConsistencyChecker(
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
       AccountCache accountCache,
-      OutgoingEmailValidator validator) {
+      OutgoingEmailValidator validator,
+      ExternalIdFactory externalIdFactory) {
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.accountCache = accountCache;
     this.validator = validator;
+    this.externalIdFactory = externalIdFactory;
   }
 
   public List<ConsistencyProblemInfo> check() throws IOException, ConfigInvalidException {
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      return check(ExternalIdNotes.loadReadOnly(allUsers, repo, null));
+      return check(ExternalIdNotes.loadReadOnly(allUsers, repo, null, externalIdFactory));
     }
   }
 
   public List<ConsistencyProblemInfo> check(ObjectId rev)
       throws IOException, ConfigInvalidException {
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      return check(ExternalIdNotes.loadReadOnly(allUsers, repo, rev));
+      return check(ExternalIdNotes.loadReadOnly(allUsers, repo, rev, externalIdFactory));
     }
   }
 
@@ -79,7 +82,7 @@
       for (Note note : noteMap) {
         byte[] raw = ExternalIdNotes.readNoteData(rw, note.getData());
         try {
-          ExternalId extId = ExternalId.parse(note.getName(), raw, note.getData());
+          ExternalId extId = externalIdFactory.parse(note.getName(), raw, note.getData());
           problems.addAll(validateExternalId(extId));
 
           if (extId.email() != null) {
diff --git a/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java b/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
index 3f2f774..33443c1 100644
--- a/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
+++ b/java/com/google/gerrit/server/account/externalids/PasswordVerifier.java
@@ -20,21 +20,30 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.account.HashedPassword;
+import com.google.inject.Inject;
 import java.util.Collection;
 
 /** Checks if a given username and password match a user's external IDs. */
 public class PasswordVerifier {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+
+  @Inject
+  public PasswordVerifier(ExternalIdKeyFactory externalIdKeyFactory) {
+    this.externalIdKeyFactory = externalIdKeyFactory;
+  }
+
   /** Returns {@code true} if there is an external ID matching both the username and password. */
-  public static boolean checkPassword(
+  public boolean checkPassword(
       Collection<ExternalId> externalIds, String username, @Nullable String password) {
     if (password == null) {
       return false;
     }
     for (ExternalId id : externalIds) {
       // Only process the "username:$USER" entry, which is unique.
-      if (!id.isScheme(SCHEME_USERNAME) || !username.equals(id.key().id())) {
+      if (!id.isScheme(SCHEME_USERNAME)
+          || !id.key().equals(externalIdKeyFactory.create(SCHEME_USERNAME, username))) {
         continue;
       }
 
diff --git a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
index b8040f7..a42afc3 100644
--- a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
+++ b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
@@ -45,7 +45,9 @@
         rw,
         ident,
         (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), accountId);
+          ExternalId extId =
+              ExternalId.create(
+                  ExternalId.Key.parse(externalId, false), accountId, null, null, null);
           ObjectId noteId = extId.key().sha1();
           Config c = new Config();
           extId.writeToConfig(c);
@@ -65,8 +67,10 @@
         rw,
         ident,
         (ins, noteMap) -> {
-          ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), accountId);
-          ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
+          ExternalId extId =
+              ExternalId.create(
+                  ExternalId.Key.parse(externalId, false), accountId, null, null, null);
+          ObjectId noteId = ExternalId.Key.parse(externalId + "x", false).sha1();
           Config c = new Config();
           extId.writeToConfig(c);
           byte[] raw = c.toText().getBytes(UTF_8);
@@ -83,7 +87,7 @@
         rw,
         ident,
         (ins, noteMap) -> {
-          ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
+          ObjectId noteId = ExternalId.Key.parse(externalId, false).sha1();
           byte[] raw = "bad-config".getBytes(UTF_8);
           ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
           noteMap.set(noteId, dataBlob);
@@ -98,7 +102,7 @@
         rw,
         ident,
         (ins, noteMap) -> {
-          ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
+          ObjectId noteId = ExternalId.Key.parse(externalId, false).sha1();
           byte[] raw = "".getBytes(UTF_8);
           ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
           noteMap.set(noteId, dataBlob);
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 1eee10f..b23782f 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
 import com.google.gerrit.extensions.api.accounts.SshKeyInput;
 import com.google.gerrit.extensions.api.accounts.StatusInput;
-import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -37,7 +36,6 @@
 import com.google.gerrit.extensions.common.AccountExternalIdInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.AgreementInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EmailInfo;
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
@@ -86,13 +84,11 @@
 import com.google.gerrit.server.restapi.account.SetPreferences;
 import com.google.gerrit.server.restapi.account.SshKeys;
 import com.google.gerrit.server.restapi.account.StarredChanges;
-import com.google.gerrit.server.restapi.account.Stars;
 import com.google.gerrit.server.restapi.change.ChangesCollection;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.List;
 import java.util.Map;
-import java.util.SortedSet;
 
 public class AccountApiImpl implements AccountApi {
   interface Factory {
@@ -115,9 +111,6 @@
   private final DeleteWatchedProjects deleteWatchedProjects;
   private final StarredChanges.Create starredChangesCreate;
   private final StarredChanges.Delete starredChangesDelete;
-  private final Stars stars;
-  private final Stars.Get starsGet;
-  private final Stars.Post starsPost;
   private final GetEmails getEmails;
   private final CreateEmail createEmail;
   private final DeleteEmail deleteEmail;
@@ -159,9 +152,6 @@
       DeleteWatchedProjects deleteWatchedProjects,
       StarredChanges.Create starredChangesCreate,
       StarredChanges.Delete starredChangesDelete,
-      Stars stars,
-      Stars.Get starsGet,
-      Stars.Post starsPost,
       GetEmails getEmails,
       CreateEmail createEmail,
       DeleteEmail deleteEmail,
@@ -202,9 +192,6 @@
     this.deleteWatchedProjects = deleteWatchedProjects;
     this.starredChangesCreate = starredChangesCreate;
     this.starredChangesDelete = starredChangesDelete;
-    this.stars = stars;
-    this.starsGet = starsGet;
-    this.starsPost = starsPost;
     this.getEmails = getEmails;
     this.createEmail = createEmail;
     this.deleteEmail = deleteEmail;
@@ -384,35 +371,6 @@
   }
 
   @Override
-  public void setStars(String changeId, StarsInput input) throws RestApiException {
-    try {
-      AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId));
-      starsPost.apply(rsrc, input);
-    } catch (Exception e) {
-      throw asRestApiException("Cannot post stars", e);
-    }
-  }
-
-  @Override
-  public SortedSet<String> getStars(String changeId) throws RestApiException {
-    try {
-      AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId));
-      return starsGet.apply(rsrc).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot get stars", e);
-    }
-  }
-
-  @Override
-  public List<ChangeInfo> getStarredChanges() throws RestApiException {
-    try {
-      return stars.list().apply(account).value();
-    } catch (Exception e) {
-      throw asRestApiException("Cannot get starred changes", e);
-    }
-  }
-
-  @Override
   public List<GroupInfo> getGroups() throws RestApiException {
     try {
       return getGroups.apply(account).value();
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 6a26f53..cbaf49e 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -91,8 +91,6 @@
 import com.google.gerrit.server.restapi.change.ListChangeDrafts;
 import com.google.gerrit.server.restapi.change.ListChangeRobotComments;
 import com.google.gerrit.server.restapi.change.ListReviewers;
-import com.google.gerrit.server.restapi.change.MarkAsReviewed;
-import com.google.gerrit.server.restapi.change.MarkAsUnreviewed;
 import com.google.gerrit.server.restapi.change.Move;
 import com.google.gerrit.server.restapi.change.PostHashtags;
 import com.google.gerrit.server.restapi.change.PostPrivate;
@@ -172,8 +170,6 @@
   private final DeletePrivate deletePrivate;
   private final Ignore ignore;
   private final Unignore unignore;
-  private final MarkAsReviewed markAsReviewed;
-  private final MarkAsUnreviewed markAsUnreviewed;
   private final SetWorkInProgress setWip;
   private final SetReadyForReview setReady;
   private final PutMessage putMessage;
@@ -228,8 +224,6 @@
       DeletePrivate deletePrivate,
       Ignore ignore,
       Unignore unignore,
-      MarkAsReviewed markAsReviewed,
-      MarkAsUnreviewed markAsUnreviewed,
       SetWorkInProgress setWip,
       SetReadyForReview setReady,
       PutMessage putMessage,
@@ -282,8 +276,6 @@
     this.deletePrivate = deletePrivate;
     this.ignore = ignore;
     this.unignore = unignore;
-    this.markAsReviewed = markAsReviewed;
-    this.markAsUnreviewed = markAsUnreviewed;
     this.setWip = setWip;
     this.setReady = setReady;
     this.putMessage = putMessage;
@@ -749,22 +741,6 @@
   }
 
   @Override
-  public void markAsReviewed(boolean reviewed) throws RestApiException {
-    // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into
-    // StarredChangesUtil.
-    try {
-      if (reviewed) {
-        markAsReviewed.apply(change, new Input());
-      } else {
-        markAsUnreviewed.apply(change, new Input());
-      }
-    } catch (StorageException | IllegalLabelException e) {
-      throw asRestApiException(
-          "Cannot mark change as " + (reviewed ? "reviewed" : "unreviewed"), e);
-    }
-  }
-
-  @Override
   public PureRevertInfo pureRevert() throws RestApiException {
     return pureRevert(null);
   }
diff --git a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java b/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
index 93099eb..fd31da9 100644
--- a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
+++ b/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
@@ -36,7 +36,7 @@
 import com.google.protobuf.ByteString;
 import java.util.concurrent.ExecutionException;
 
-/** @see ApprovalCache */
+/** Implementation of the {@link ApprovalCache} interface */
 public class ApprovalCacheImpl implements ApprovalCache {
   private static final String CACHE_NAME = "approvals";
 
@@ -49,7 +49,7 @@
                 CACHE_NAME,
                 Cache.PatchSetApprovalsKeyProto.class,
                 Cache.AllPatchSetApprovalsProto.class)
-            .version(1)
+            .version(2)
             .loader(Loader.class)
             .keySerializer(new ProtobufSerializer<>(Cache.PatchSetApprovalsKeyProto.parser()))
             .valueSerializer(new ProtobufSerializer<>(Cache.AllPatchSetApprovalsProto.parser()));
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 8d409e5..4cb080a 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -101,20 +101,29 @@
    */
   Iterable<PatchSetApproval> forPatchSet(
       ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
+    PatchSet patchset = notes.getPatchSets().get(psId);
+    if (patchset == null) {
+      return Collections.emptyList();
+    }
+    return forPatchSet(notes, patchset, rw, repoConfig);
+  }
+
+  Iterable<PatchSetApproval> forPatchSet(
+      ChangeNotes notes, PatchSet ps, @Nullable RevWalk rw, @Nullable Config repoConfig) {
     ProjectState project;
     try (TraceTimer traceTimer =
         TraceContext.newTimer(
             "Computing labels for patch set",
             Metadata.builder()
                 .changeId(notes.load().getChangeId().get())
-                .patchSetId(psId.get())
+                .patchSetId(ps.id().get())
                 .build())) {
       project =
           projectCache
               .get(notes.getProjectName())
               .orElseThrow(illegalState(notes.getProjectName()));
       Collection<PatchSetApproval> approvals =
-          getForPatchSetWithoutNormalization(notes, project, psId, rw, repoConfig);
+          getForPatchSetWithoutNormalization(notes, project, ps, rw, repoConfig);
       return labelNormalizer.normalize(notes, approvals).getNormalized();
     }
   }
@@ -125,7 +134,8 @@
       PatchSet.Id psId,
       ChangeKind kind,
       LabelType type,
-      @Nullable Map<String, FileDiffOutput> modifiedFiles) {
+      @Nullable Map<String, FileDiffOutput> modifiedFiles,
+      @Nullable Map<String, FileDiffOutput> modifiedFilesLastPatchset) {
     int n = psa.key().patchSetId().get();
     checkArgument(n != psId.get());
 
@@ -175,7 +185,7 @@
           project.getName());
       return true;
     } else if (type.isCopyAllScoresIfListOfFilesDidNotChange()
-        && listOfFilesUnchangedPredicate.match(modifiedFiles)) {
+        && listOfFilesUnchangedPredicate.match(modifiedFiles, modifiedFilesLastPatchset)) {
       logger.atFine().log(
           "approval %d on label %s of patch set %d of change %d can be copied"
               + " to patch set %d because the label has set "
@@ -310,13 +320,13 @@
   private boolean canCopyBasedOnCopyCondition(
       ChangeNotes changeNotes,
       PatchSetApproval psa,
-      PatchSet.Id psId,
+      PatchSet patchSet,
       LabelType type,
       ChangeKind changeKind) {
     if (!type.getCopyCondition().isPresent()) {
       return false;
     }
-    ApprovalContext ctx = ApprovalContext.create(changeNotes, psa, psId, changeKind);
+    ApprovalContext ctx = ApprovalContext.create(changeNotes, psa, patchSet, changeKind);
     try {
       // Use a request context to run checks as an internal user with expanded visibility. This is
       // so that the output of the copy condition does not depend on who is running the current
@@ -335,7 +345,7 @@
   private Collection<PatchSetApproval> getForPatchSetWithoutNormalization(
       ChangeNotes notes,
       ProjectState project,
-      PatchSet.Id psId,
+      PatchSet patchSet,
       @Nullable RevWalk rw,
       @Nullable Config repoConfig) {
     checkState(
@@ -344,15 +354,11 @@
         project.getNameKey(),
         notes.getProjectName());
 
-    PatchSet ps = notes.load().getPatchSets().get(psId);
-    if (ps == null) {
-      return Collections.emptyList();
-    }
-
+    PatchSet.Id psId = patchSet.id();
     // Add approvals on the given patch set to the result
     Table<String, Account.Id, PatchSetApproval> resultByUser = HashBasedTable.create();
     ImmutableList<PatchSetApproval> approvalsForGivenPatchSet =
-        notes.load().getApprovals().get(ps.id());
+        notes.load().getApprovals().get(patchSet.id());
     approvalsForGivenPatchSet.forEach(psa -> resultByUser.put(psa.label(), psa.accountId(), psa));
 
     // Bail out immediately if this is the first patch set. Return only approvals granted on the
@@ -375,24 +381,29 @@
 
     Iterable<PatchSetApproval> priorApprovals =
         getForPatchSetWithoutNormalization(
-            notes, project, priorPatchSet.getValue().id(), rw, repoConfig);
+            notes, project, priorPatchSet.getValue(), rw, repoConfig);
     if (!priorApprovals.iterator().hasNext()) {
       return resultByUser.values();
     }
 
     // Add labels from the previous patch set to the result in case the label isn't already there
     // and settings as well as change kind allow copying.
-    ChangeKind kind =
+    ChangeKind changeKind =
         changeKindCache.getChangeKind(
             project.getNameKey(),
             rw,
             repoConfig,
             priorPatchSet.getValue().commitId(),
-            ps.commitId());
+            patchSet.commitId());
     logger.atFine().log(
         "change kind for patch set %d of change %d against prior patch set %s is %s",
-        ps.id().get(), ps.id().changeId().get(), priorPatchSet.getValue().id().changeId(), kind);
+        patchSet.id().get(),
+        patchSet.id().changeId().get(),
+        priorPatchSet.getValue().id().changeId(),
+        changeKind);
+
     Map<String, FileDiffOutput> modifiedFiles = null;
+    Map<String, FileDiffOutput> modifiedFilesLastPatchSet = null;
     LabelTypes labelTypes = project.getLabelTypes();
     for (PatchSetApproval psa : priorApprovals) {
       if (resultByUser.contains(psa.label(), psa.accountId())) {
@@ -403,7 +414,8 @@
       if (modifiedFiles == null
           && type.isPresent()
           && type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
-        modifiedFiles = listModifiedFiles(project, ps, priorPatchSet);
+        modifiedFiles = listModifiedFiles(project, patchSet);
+        modifiedFilesLastPatchSet = listModifiedFiles(project, priorPatchSet.getValue());
       }
       if (!type.isPresent()) {
         logger.atFine().log(
@@ -417,11 +429,18 @@
             project.getName());
         continue;
       }
-      if (!canCopyBasedOnBooleanLabelConfigs(project, psa, ps.id(), kind, type.get(), modifiedFiles)
-          && !canCopyBasedOnCopyCondition(notes, psa, ps.id(), type.get(), kind)) {
+      if (!canCopyBasedOnBooleanLabelConfigs(
+              project,
+              psa,
+              patchSet.id(),
+              changeKind,
+              type.get(),
+              modifiedFiles,
+              modifiedFilesLastPatchSet)
+          && !canCopyBasedOnCopyCondition(notes, psa, patchSet, type.get(), changeKind)) {
         continue;
       }
-      resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(ps.id()));
+      resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(patchSet.id()));
     }
     return resultByUser.values();
   }
@@ -430,11 +449,14 @@
    * Gets the modified files between the two latest patch-sets. Can be used to compute difference in
    * files between those two patch-sets .
    */
-  private Map<String, FileDiffOutput> listModifiedFiles(
-      ProjectState project, PatchSet ps, Map.Entry<PatchSet.Id, PatchSet> priorPatchSet) {
+  private Map<String, FileDiffOutput> listModifiedFiles(ProjectState project, PatchSet ps) {
     try {
-      return diffOperations.listModifiedFiles(
-          project.getNameKey(), priorPatchSet.getValue().commitId(), ps.commitId());
+      Integer parentNum =
+          listOfFilesUnchangedPredicate.isInitialCommit(project.getNameKey(), ps.commitId())
+              ? 0
+              : 1;
+      return diffOperations.listModifiedFilesAgainstParent(
+          project.getNameKey(), ps.commitId(), parentNum);
     } catch (DiffNotAvailableException ex) {
       throw new StorageException(
           "failed to compute difference in files, so won't copy"
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index a1cdd99..c2e35d2 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -278,7 +278,6 @@
    * @param ps patch set being approved.
    * @param user user adding approvals.
    * @param approvals approvals to add.
-   * @throws RestApiException
    */
   public Iterable<PatchSetApproval> addApprovalsForNewPatchSet(
       ChangeUpdate update,
@@ -349,6 +348,10 @@
     return approvalInference.forPatchSet(notes, psId, rw, repoConfig);
   }
 
+  public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet patchSet) {
+    return approvalInference.forPatchSet(notes, patchSet, /* rw= */ null, /* repoConfig= */ null);
+  }
+
   public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
     return approvalCache.get(notes, psId);
   }
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 73a970b..5df4d28 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -44,12 +44,14 @@
   private final AccountResolver accountResolver;
   private final AccountManager accountManager;
   private final AuthType authType;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   public AccountIdHandler(
       AccountResolver accountResolver,
       AccountManager accountManager,
       AuthConfig authConfig,
+      AuthRequest.Factory authRequestFactory,
       @Assisted CmdLineParser parser,
       @Assisted OptionDef option,
       @Assisted Setter<Account.Id> setter) {
@@ -57,6 +59,7 @@
     this.accountResolver = accountResolver;
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -105,7 +108,7 @@
     }
 
     try {
-      AuthRequest req = AuthRequest.forUser(user);
+      AuthRequest req = authRequestFactory.createForUser(user);
       req.setSkipAuthentication(true);
       return accountManager.authenticate(req).getAccountId();
     } catch (AccountException e) {
diff --git a/java/com/google/gerrit/server/auth/AuthBackend.java b/java/com/google/gerrit/server/auth/AuthBackend.java
index 9ec3366..424ee43 100644
--- a/java/com/google/gerrit/server/auth/AuthBackend.java
+++ b/java/com/google/gerrit/server/auth/AuthBackend.java
@@ -20,7 +20,7 @@
 @ExtensionPoint
 public interface AuthBackend {
 
-  /** @return an identifier that uniquely describes the backend. */
+  /** Returns an identifier that uniquely describes the backend. */
   String getDomain();
 
   /**
diff --git a/java/com/google/gerrit/server/auth/AuthUser.java b/java/com/google/gerrit/server/auth/AuthUser.java
index 987f086..9e1c5ec 100644
--- a/java/com/google/gerrit/server/auth/AuthUser.java
+++ b/java/com/google/gerrit/server/auth/AuthUser.java
@@ -52,18 +52,18 @@
     this.username = username;
   }
 
-  /** @return the globally unique identifier. */
+  /** Returns the globally unique identifier. */
   public final UUID getUUID() {
     return uuid;
   }
 
-  /** @return the backend specific user name, or null if one does not exist. */
+  /** Returns the backend specific user name, or null if one does not exist. */
   @Nullable
   public final String getUsername() {
     return username;
   }
 
-  /** @return {@code true} if {@link #getUsername()} is not null. */
+  /** Returns {@code true} if {@link #getUsername()} is not null. */
   public final boolean hasUsername() {
     return getUsername() != null;
   }
diff --git a/java/com/google/gerrit/server/auth/InternalAuthBackend.java b/java/com/google/gerrit/server/auth/InternalAuthBackend.java
index 2f8886b..ce536f6 100644
--- a/java/com/google/gerrit/server/auth/InternalAuthBackend.java
+++ b/java/com/google/gerrit/server/auth/InternalAuthBackend.java
@@ -26,11 +26,14 @@
 public class InternalAuthBackend implements AuthBackend {
   private final AccountCache accountCache;
   private final AuthConfig authConfig;
+  private final PasswordVerifier passwordVerifier;
 
   @Inject
-  InternalAuthBackend(AccountCache accountCache, AuthConfig authConfig) {
+  InternalAuthBackend(
+      AccountCache accountCache, AuthConfig authConfig, PasswordVerifier passwordVerifier) {
     this.accountCache = accountCache;
     this.authConfig = authConfig;
+    this.passwordVerifier = passwordVerifier;
   }
 
   @Override
@@ -63,7 +66,7 @@
               + ": account inactive or not provisioned in Gerrit");
     }
 
-    if (!PasswordVerifier.checkPassword(who.externalIds(), username, req.getPassword().get())) {
+    if (!passwordVerifier.checkPassword(who.externalIds(), username, req.getPassword().get())) {
       throw new InvalidCredentialsException();
     }
     return new AuthUser(AuthUser.UUID.create(username), username);
diff --git a/java/com/google/gerrit/server/cache/CacheMetrics.java b/java/com/google/gerrit/server/cache/CacheMetrics.java
index 12194e7..f1fd4a8 100644
--- a/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -35,7 +35,9 @@
 @Singleton
 public class CacheMetrics {
   private static final Field<String> F_NAME =
-      Field.ofString("cache_name", Metadata.Builder::cacheName).build();
+      Field.ofString("cache_name", Metadata.Builder::cacheName)
+          .description("The name of the cache.")
+          .build();
 
   @Inject
   public CacheMetrics(
diff --git a/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java b/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
index ee672cd..28d57e6 100644
--- a/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
+++ b/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
@@ -25,9 +25,6 @@
 /**
  * This listener dispatches removal events to all other RemovalListeners attached via the DynamicSet
  * API.
- *
- * @param <K>
- * @param <V>
  */
 @SuppressWarnings("rawtypes")
 public class ForwardingRemovalListener<K, V> implements RemovalListener<K, V> {
diff --git a/java/com/google/gerrit/server/change/ChangeFinder.java b/java/com/google/gerrit/server/change/ChangeFinder.java
index ba104d8..9f253de 100644
--- a/java/com/google/gerrit/server/change/ChangeFinder.java
+++ b/java/com/google/gerrit/server/change/ChangeFinder.java
@@ -96,6 +96,7 @@
                 .setRate()
                 .setUnit("requests"),
             Field.ofEnum(ChangeIdType.class, "change_id_type", Metadata.Builder::changeIdType)
+                .description("The type of the change identifier.")
                 .build());
   }
 
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 89069e2..db25dc7 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -79,6 +79,7 @@
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.SubmitRecordInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementExpressionInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
 import com.google.gerrit.extensions.common.TrackingIdInfo;
@@ -98,6 +99,7 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountInfoComparator;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.cancellation.RequestCancelledException;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.index.change.ChangeField;
@@ -368,6 +370,14 @@
     return reqInfos;
   }
 
+  private Collection<SubmitRecordInfo> submitRecordsFor(ChangeData cd) {
+    List<SubmitRecordInfo> submitRecordInfos = new ArrayList<>();
+    for (SubmitRecord record : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
+      submitRecordInfos.add(submitRecordToInfo(record));
+    }
+    return submitRecordInfos;
+  }
+
   private static Collection<SubmitRequirementResultInfo> submitRequirementsFor(ChangeData cd) {
     Collection<SubmitRequirementResultInfo> reqInfos = new ArrayList<>();
     Map<SubmitRequirement, SubmitRequirementResult> requirements = cd.submitRequirements();
@@ -382,6 +392,34 @@
     return new LegacySubmitRequirementInfo(status.name(), req.fallbackText(), req.type());
   }
 
+  private SubmitRecordInfo submitRecordToInfo(SubmitRecord record) {
+    SubmitRecordInfo info = new SubmitRecordInfo();
+    if (record.status != null) {
+      info.status = SubmitRecordInfo.Status.valueOf(record.status.name());
+    }
+    info.ruleName = record.ruleName;
+    info.errorMessage = record.errorMessage;
+    if (record.labels != null) {
+      info.labels = new ArrayList<>();
+      for (SubmitRecord.Label label : record.labels) {
+        SubmitRecordInfo.Label labelInfo = new SubmitRecordInfo.Label();
+        labelInfo.label = label.label;
+        if (label.status != null) {
+          labelInfo.status = SubmitRecordInfo.Label.Status.valueOf(label.status.name());
+        }
+        labelInfo.appliedBy = accountLoader.get(label.appliedBy);
+        info.labels.add(labelInfo);
+      }
+    }
+    if (record.requirements != null) {
+      info.requirements = new ArrayList<>();
+      for (LegacySubmitRequirement requirement : record.requirements) {
+        info.requirements.add(requirementToInfo(requirement, record.status));
+      }
+    }
+    return info;
+  }
+
   private static SubmitRequirementResultInfo submitRequirementToInfo(
       SubmitRequirement req, SubmitRequirementResult result) {
     SubmitRequirementResultInfo info = new SubmitRequirementResultInfo();
@@ -401,6 +439,7 @@
         submitRequirementExpressionToInfo(
             req.submittabilityExpression(), result.submittabilityExpressionResult());
     info.status = SubmitRequirementResultInfo.Status.valueOf(result.status().toString());
+    info.isLegacy = result.legacy();
     return info;
   }
 
@@ -509,6 +548,11 @@
             cache.put(Change.id(info._number), info);
           }
         } catch (RuntimeException e) {
+          Optional<RequestCancelledException> requestCancelledException =
+              RequestCancelledException.getFromCausalChain(e);
+          if (requestCancelledException.isPresent()) {
+            throw e;
+          }
           logger.atWarning().withCause(e).log(
               "Omitting corrupt change %s from results", cd.getId());
         }
@@ -655,6 +699,7 @@
 
     out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
     out.requirements = requirementsFor(cd);
+    out.submitRecords = submitRecordsFor(cd);
     if (has(SUBMIT_REQUIREMENTS)) {
       out.submitRequirements = submitRequirementsFor(cd);
     }
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 0d0df0d..970f1b5 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -140,7 +140,7 @@
     return changeData.getId();
   }
 
-  /** @return true if {@link #getUser()} is the change's owner. */
+  /** Returns true if {@link #getUser()} is the change's owner. */
   public boolean isUserOwner() {
     Account.Id owner = getChange().getOwner();
     return user.isIdentifiedUser() && user.asIdentifiedUser().getAccountId().equals(owner);
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index d433c4e..3c7ea44 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -45,6 +45,8 @@
     // TODO(dborowitz/wyatta): Rationalize these arguments so HTML and text templates are operating
     // on the same set of inputs.
     /**
+     * Creates handle for sending email
+     *
      * @param notify setting for handling notification.
      * @param notes change notes.
      * @param patchSet patch set corresponding to the top-level op
@@ -57,7 +59,6 @@
      *     contents should *not* include a "Patch set N" header or "(M comments)" footer, as these
      *     will be added automatically in soy in a structured way.
      * @param labels labels applied as part of this review operation.
-     * @return handle for sending email.
      */
     EmailReviewComments create(
         NotifyResolver.Result notify,
diff --git a/java/com/google/gerrit/server/change/FileContentUtil.java b/java/com/google/gerrit/server/change/FileContentUtil.java
index 49c1fe2..c54b902 100644
--- a/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -76,8 +76,6 @@
    * @param parent A 1-based parent index to get the content from instead. Null if the content
    *     should be obtained from {@code revstr} instead.
    * @return Content of the file as {@code BinaryResult}.
-   * @throws ResourceNotFoundException
-   * @throws IOException
    */
   public BinaryResult getContent(
       ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonComparingImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonComparingImpl.java
deleted file mode 100644
index a926147..0000000
--- a/java/com/google/gerrit/server/change/FileInfoJsonComparingImpl.java
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.metrics.Counter1;
-import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.Field;
-import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.server.logging.Metadata;
-import com.google.gerrit.server.patch.DiffExecutor;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * Implementation of FileInfoJson which uses {@link FileInfoJsonOldImpl}, but also runs {@link
- * FileInfoJsonNewImpl} asynchronously and compares the results. This implementation is temporary
- * and will be used to verify that the results are the same.
- */
-public class FileInfoJsonComparingImpl implements FileInfoJson {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final FileInfoJsonOldImpl oldImpl;
-  private final FileInfoJsonNewImpl newImpl;
-  private final ExecutorService executor;
-  private final Metrics metrics;
-
-  /**
-   * TODO(ghareeb): These metrics are temporary for launching the new diff cache redesign and are
-   * not documented. These will be removed soon.
-   */
-  @VisibleForTesting
-  @Singleton
-  static class Metrics {
-    private enum Status {
-      MATCH,
-      MISMATCH,
-      ERROR
-    }
-
-    final Counter1<Status> diffs;
-
-    @Inject
-    Metrics(MetricMaker metricMaker) {
-      diffs =
-          metricMaker.newCounter(
-              "diff/list_files/dark_launch",
-              new Description(
-                      "Total number of matching, non-matching, or error in list-files diffs in the old and new diff cache implementations.")
-                  .setRate()
-                  .setUnit("count"),
-              Field.ofEnum(Status.class, "type", Metadata.Builder::eventType).build());
-    }
-  }
-
-  @Inject
-  public FileInfoJsonComparingImpl(
-      FileInfoJsonOldImpl oldImpl,
-      FileInfoJsonNewImpl newImpl,
-      @DiffExecutor ExecutorService executor,
-      Metrics metrics) {
-    this.oldImpl = oldImpl;
-    this.newImpl = newImpl;
-    this.executor = executor;
-    this.metrics = metrics;
-  }
-
-  @Override
-  public Map<String, FileInfo> getFileInfoMap(
-      Change change, ObjectId objectId, @Nullable PatchSet base)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    Map<String, FileInfo> result = oldImpl.getFileInfoMap(change, objectId, base);
-    @SuppressWarnings("unused")
-    Future<?> ignored =
-        executor.submit(
-            () -> {
-              try {
-                Map<String, FileInfo> fileInfoNew = newImpl.getFileInfoMap(change, objectId, base);
-                compareAndLogMetrics(
-                    result,
-                    fileInfoNew,
-                    String.format(
-                        "Mismatch comparing old and new diff implementations for change: %s, objectId: %s and base: %s",
-                        change, objectId, base == null ? "none" : base.id()));
-              } catch (ResourceConflictException | PatchListNotAvailableException e) {
-                // If an exception happens while evaluating the new diff, increment the non-matching
-                // counter
-                metrics.diffs.increment(Metrics.Status.ERROR);
-                logger.atWarning().withCause(e).log(
-                    "Error comparing old and new diff implementations.");
-              }
-            });
-    return result;
-  }
-
-  @Override
-  public Map<String, FileInfo> getFileInfoMap(
-      Project.NameKey project, ObjectId objectId, int parentNum)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    Map<String, FileInfo> result = oldImpl.getFileInfoMap(project, objectId, parentNum);
-    @SuppressWarnings("unused")
-    Future<?> ignored =
-        executor.submit(
-            () -> {
-              try {
-                Map<String, FileInfo> resultNew =
-                    newImpl.getFileInfoMap(project, objectId, parentNum);
-                compareAndLogMetrics(
-                    result,
-                    resultNew,
-                    String.format(
-                        "Mismatch comparing old and new diff implementations for project: %s, objectId: %s and parentNum: %d",
-                        project, objectId, parentNum));
-              } catch (ResourceConflictException | PatchListNotAvailableException e) {
-                // If an exception happens while evaluating the new diff, increment the non-matching
-                // ctr
-                metrics.diffs.increment(Metrics.Status.ERROR);
-                logger.atWarning().withCause(e).log(
-                    "Error comparing old and new diff implementations.");
-              }
-            });
-    return result;
-  }
-
-  private void compareAndLogMetrics(
-      Map<String, FileInfo> fileInfoMapOld,
-      Map<String, FileInfo> fileInfoMapNew,
-      String warningMessage) {
-    if (fileInfoMapOld.equals(fileInfoMapNew)) {
-      metrics.diffs.increment(Metrics.Status.MATCH);
-      return;
-    }
-    metrics.diffs.increment(Metrics.Status.MISMATCH);
-    logger.atWarning().log(
-        warningMessage
-            + "\n"
-            + "Result using old impl: "
-            + fileInfoMapOld
-            + "\n"
-            + "Result using new impl: "
-            + fileInfoMapNew);
-  }
-}
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java
deleted file mode 100644
index 81f014d..0000000
--- a/java/com/google/gerrit/server/change/FileInfoJsonExperimentImpl.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import java.util.Map;
-import javax.inject.Inject;
-import org.eclipse.jgit.lib.ObjectId;
-
-/**
- * An experimental implementation of FileInfoJson that uses {@link FileInfoJsonNewImpl} if the
- * experiment flag "GerritBackendRequestFeature__use_new_diff_cache" is enabled, or {@link
- * FileInfoJsonOldImpl} otherwise. This would enable a gradual rollout of {@link
- * FileInfoJsonNewImpl}.
- */
-public class FileInfoJsonExperimentImpl implements FileInfoJson {
-  @VisibleForTesting
-  public static final String NEW_DIFF_CACHE_FEATURE =
-      "GerritBackendRequestFeature__use_new_diff_cache";
-
-  private final FileInfoJsonOldImpl oldImpl;
-  private final FileInfoJsonNewImpl newImpl;
-  private final ExperimentFeatures experimentFeatures;
-
-  @Inject
-  public FileInfoJsonExperimentImpl(
-      FileInfoJsonOldImpl oldImpl,
-      FileInfoJsonNewImpl newImpl,
-      ExperimentFeatures experimentFeatures) {
-    this.oldImpl = oldImpl;
-    this.newImpl = newImpl;
-    this.experimentFeatures = experimentFeatures;
-  }
-
-  @Override
-  public Map<String, FileInfo> getFileInfoMap(
-      Change change, ObjectId objectId, @Nullable PatchSet base)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    return experimentFeatures.isFeatureEnabled(NEW_DIFF_CACHE_FEATURE)
-        ? newImpl.getFileInfoMap(change, objectId, base)
-        : oldImpl.getFileInfoMap(change, objectId, base);
-  }
-
-  @Override
-  public Map<String, FileInfo> getFileInfoMap(
-      Project.NameKey project, ObjectId objectId, int parentNum)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    return experimentFeatures.isFeatureEnabled(NEW_DIFF_CACHE_FEATURE)
-        ? newImpl.getFileInfoMap(project, objectId, parentNum)
-        : oldImpl.getFileInfoMap(project, objectId, parentNum);
-  }
-}
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
similarity index 95%
rename from java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
rename to java/com/google/gerrit/server/change/FileInfoJsonImpl.java
index 7277404..b729c11 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonNewImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
@@ -32,12 +32,12 @@
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.lib.ObjectId;
 
-/** Implementation of {@link FileInfoJson} using the new diff cache {@link DiffOperations}. */
-public class FileInfoJsonNewImpl implements FileInfoJson {
+/** Implementation of {@link FileInfoJson} using {@link DiffOperations}. */
+public class FileInfoJsonImpl implements FileInfoJson {
   private final DiffOperations diffs;
 
   @Inject
-  FileInfoJsonNewImpl(DiffOperations diffOperations) {
+  FileInfoJsonImpl(DiffOperations diffOperations) {
     this.diffs = diffOperations;
   }
 
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonModule.java b/java/com/google/gerrit/server/change/FileInfoJsonModule.java
index 952b503..b8e05f0 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonModule.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonModule.java
@@ -20,7 +20,6 @@
 
   @Override
   public void configure() {
-    // Binding to the experimental implementation to enable gradual rollout of the new diff cache.
-    bind(FileInfoJson.class).to(FileInfoJsonExperimentImpl.class);
+    bind(FileInfoJson.class).to(FileInfoJsonImpl.class);
   }
 }
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
deleted file mode 100644
index 0570296..0000000
--- a/java/com/google/gerrit/server/change/FileInfoJsonOldImpl.java
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.concurrent.ExecutionException;
-import org.eclipse.jgit.errors.NoMergeBaseException;
-import org.eclipse.jgit.lib.ObjectId;
-
-/** Implementation of {@link FileInfoJson} using the old diff cache {@link PatchListCache}. */
-@Deprecated
-@Singleton
-class FileInfoJsonOldImpl implements FileInfoJson {
-  private final PatchListCache patchListCache;
-
-  @Inject
-  FileInfoJsonOldImpl(PatchListCache patchListCache) {
-    this.patchListCache = patchListCache;
-  }
-
-  @Override
-  public Map<String, FileInfo> getFileInfoMap(
-      Change change, ObjectId objectId, @Nullable PatchSet base)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    ObjectId a = base != null ? base.commitId() : null;
-    return toFileInfoMap(change, PatchListKey.againstCommit(a, objectId, Whitespace.IGNORE_NONE));
-  }
-
-  @Override
-  public Map<String, FileInfo> getFileInfoMap(
-      Project.NameKey project, ObjectId objectId, int parentNum)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    PatchListKey key =
-        parentNum == 0
-            ? PatchListKey.againstDefaultBase(objectId, Whitespace.IGNORE_NONE)
-            : PatchListKey.againstParentNum(
-                parentNum, objectId, DiffPreferencesInfo.Whitespace.IGNORE_NONE);
-    return toFileInfoMap(project, key);
-  }
-
-  private Map<String, FileInfo> toFileInfoMap(Change change, PatchListKey key)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    return toFileInfoMap(change.getProject(), key);
-  }
-
-  Map<String, FileInfo> toFileInfoMap(Project.NameKey project, PatchListKey key)
-      throws ResourceConflictException, PatchListNotAvailableException {
-    PatchList list;
-    try {
-      list = patchListCache.get(key, project);
-    } catch (PatchListNotAvailableException e) {
-      Throwable cause = e.getCause();
-      if (cause instanceof ExecutionException) {
-        cause = cause.getCause();
-      }
-      if (cause instanceof NoMergeBaseException) {
-        throw new ResourceConflictException(
-            String.format("Cannot create auto merge commit: %s", e.getMessage()), e);
-      }
-      throw e;
-    }
-
-    Map<String, FileInfo> files = new TreeMap<>();
-    for (PatchListEntry e : list.getPatches()) {
-      FileInfo fileInfo = new FileInfo();
-      fileInfo.status =
-          e.getChangeType() != Patch.ChangeType.MODIFIED ? e.getChangeType().getCode() : null;
-      fileInfo.oldPath = e.getOldName();
-      fileInfo.sizeDelta = e.getSizeDelta();
-      fileInfo.size = e.getSize();
-      if (e.getPatchType() == Patch.PatchType.BINARY) {
-        fileInfo.binary = true;
-      } else {
-        fileInfo.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
-        fileInfo.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
-      }
-
-      FileInfo o = files.put(e.getNewName(), fileInfo);
-      if (o != null) {
-        // This should only happen on a delete-add break created by JGit
-        // when the file was rewritten and too little content survived. Write
-        // a single record with data from both sides.
-        fileInfo.status = Patch.ChangeType.REWRITE.getCode();
-        fileInfo.sizeDelta = o.sizeDelta;
-        fileInfo.size = o.size;
-        if (o.binary != null && o.binary) {
-          fileInfo.binary = true;
-        }
-        if (o.linesInserted != null) {
-          fileInfo.linesInserted = o.linesInserted;
-        }
-        if (o.linesDeleted != null) {
-          fileInfo.linesDeleted = o.linesDeleted;
-        }
-      }
-    }
-    return files;
-  }
-}
diff --git a/java/com/google/gerrit/server/change/IncludedIn.java b/java/com/google/gerrit/server/change/IncludedIn.java
index 3c66c2c..c06ce82 100644
--- a/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/java/com/google/gerrit/server/change/IncludedIn.java
@@ -14,6 +14,12 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.util.Comparator.naturalOrder;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.gerrit.entities.Project;
@@ -23,13 +29,18 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Collection;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -37,17 +48,21 @@
 @Singleton
 public class IncludedIn {
   private final GitRepositoryManager repoManager;
+  private final PermissionBackend permissionBackend;
   private final PluginSetContext<ExternalIncludedIn> externalIncludedIn;
 
   @Inject
   IncludedIn(
-      GitRepositoryManager repoManager, PluginSetContext<ExternalIncludedIn> externalIncludedIn) {
+      GitRepositoryManager repoManager,
+      PermissionBackend permissionBackend,
+      PluginSetContext<ExternalIncludedIn> externalIncludedIn) {
     this.repoManager = repoManager;
+    this.permissionBackend = permissionBackend;
     this.externalIncludedIn = externalIncludedIn;
   }
 
   public IncludedInInfo apply(Project.NameKey project, String revisionId)
-      throws RestApiException, IOException {
+      throws RestApiException, IOException, PermissionBackendException {
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
       rw.setRetainBody(false);
@@ -61,18 +76,48 @@
       }
 
       IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev);
+
+      // Filter branches and tags according to their visbility by the user
+      ImmutableSortedSet<String> filteredBranches =
+          sortedShortNames(filterReadableRefs(project, d.branches()));
+      ImmutableSortedSet<String> filteredTags =
+          sortedShortNames(filterReadableRefs(project, d.tags()));
+
       ListMultimap<String, String> external = MultimapBuilder.hashKeys().arrayListValues().build();
       externalIncludedIn.runEach(
           ext -> {
             ListMultimap<String, String> extIncludedIns =
-                ext.getIncludedIn(project.get(), rev.name(), d.tags(), d.branches());
+                ext.getIncludedIn(project.get(), rev.name(), filteredBranches, filteredTags);
             if (extIncludedIns != null) {
               external.putAll(extIncludedIns);
             }
           });
 
       return new IncludedInInfo(
-          d.branches(), d.tags(), (!external.isEmpty() ? external.asMap() : null));
+          filteredBranches, filteredTags, (!external.isEmpty() ? external.asMap() : null));
     }
   }
+
+  /**
+   * Filter readable branches or tags according to the caller's refs visibility.
+   *
+   * @param project specific Gerrit project.
+   * @param inputRefs a list of branches (in short name) as strings
+   */
+  private Collection<String> filterReadableRefs(
+      Project.NameKey project, ImmutableList<Ref> inputRefs)
+      throws IOException, PermissionBackendException {
+    PermissionBackend.ForProject perm = permissionBackend.currentUser().project(project);
+    try (Repository repo = repoManager.openRepository(project)) {
+      return perm.filter(inputRefs, repo, RefFilterOptions.defaults()).stream()
+          .map(Ref::getName)
+          .collect(toImmutableList());
+    }
+  }
+
+  private ImmutableSortedSet<String> sortedShortNames(Collection<String> refs) {
+    return refs.stream()
+        .map(Repository::shortenRefName)
+        .collect(toImmutableSortedSet(naturalOrder()));
+  }
 }
diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java
index 3e1b69b..b2b0a64 100644
--- a/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -14,13 +14,11 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
 import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
 import static java.util.stream.Collectors.toList;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
@@ -117,13 +115,12 @@
    * Returns the short names of refs which are as well in the matchingRefs list as well as in the
    * allRef list.
    */
-  private static ImmutableSortedSet<String> getMatchingRefNames(
+  private static ImmutableList<Ref> getMatchingRefNames(
       Set<String> matchingRefs, Collection<Ref> allRefs) {
     return allRefs.stream()
-        .map(Ref::getName)
-        .filter(matchingRefs::contains)
-        .map(Repository::shortenRefName)
-        .collect(toImmutableSortedSet(naturalOrder()));
+        .filter(r -> matchingRefs.contains(r.getName()))
+        .distinct()
+        .collect(ImmutableList.toImmutableList());
   }
 
   /** Parse commit of ref and store the relation between ref and commit. */
@@ -157,8 +154,8 @@
 
   @AutoValue
   public abstract static class Result {
-    public abstract ImmutableSortedSet<String> branches();
+    public abstract ImmutableList<Ref> branches();
 
-    public abstract ImmutableSortedSet<String> tags();
+    public abstract ImmutableList<Ref> tags();
   }
 }
diff --git a/java/com/google/gerrit/server/change/LabelNormalizer.java b/java/com/google/gerrit/server/change/LabelNormalizer.java
index b5527d7..aeb9db0 100644
--- a/java/com/google/gerrit/server/change/LabelNormalizer.java
+++ b/java/com/google/gerrit/server/change/LabelNormalizer.java
@@ -77,10 +77,11 @@
   }
 
   /**
+   * Returns copies of approvals normalized to the defined ranges for the label type. Approvals for
+   * unknown labels are not included in the output
+   *
    * @param notes change notes containing the given approvals.
    * @param approvals list of approvals.
-   * @return copies of approvals normalized to the defined ranges for the label type. Approvals for
-   *     unknown labels are not included in the output.
    */
   public Result normalize(ChangeNotes notes, Collection<PatchSetApproval> approvals) {
     List<PatchSetApproval> unchanged = Lists.newArrayListWithCapacity(approvals.size());
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index d25dba0..f093958 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -285,6 +285,11 @@
         throw new BadRequestException(ex.getMessage());
       }
     }
+
+    // Approvals that are being set in the new patch-set during this operation are not available yet
+    // outside of the scope of this method. Only copied approvals are set here.
+    approvalsUtil.byPatchSet(ctx.getNotes(), patchSet).forEach(a -> update.putCopiedApproval(a));
+
     return true;
   }
 
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index f3c5193..fffb107 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -201,9 +201,6 @@
    * @return handle describing the addition operation. If the {@code op} field is present, this
    *     operation may be added to a {@code BatchUpdate}. Otherwise, the {@code error} field
    *     contains information about an error that occurred
-   * @throws IOException
-   * @throws PermissionBackendException
-   * @throws ConfigInvalidException
    */
   public ReviewerModification prepare(
       ChangeNotes notes, CurrentUser user, ReviewerInput input, boolean allowGroup)
diff --git a/java/com/google/gerrit/server/config/AllProjectsConfigProvider.java b/java/com/google/gerrit/server/config/AllProjectsConfigProvider.java
new file mode 100644
index 0000000..27ae41f
--- /dev/null
+++ b/java/com/google/gerrit/server/config/AllProjectsConfigProvider.java
@@ -0,0 +1,8 @@
+package com.google.gerrit.server.config;
+
+import java.util.Optional;
+import org.eclipse.jgit.lib.StoredConfig;
+
+public interface AllProjectsConfigProvider {
+  Optional<StoredConfig> get(AllProjectsName allProjectsName);
+}
diff --git a/java/com/google/gerrit/server/config/AuthConfig.java b/java/com/google/gerrit/server/config/AuthConfig.java
index de57d04..1760378 100644
--- a/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/java/com/google/gerrit/server/config/AuthConfig.java
@@ -64,6 +64,7 @@
   private final boolean cookieSecure;
   private final SignedToken emailReg;
   private final boolean allowRegisterNewEmail;
+  private final boolean userNameCaseInsensitive;
   private GitBasicAuthPolicy gitBasicAuthPolicy;
 
   @Inject
@@ -95,6 +96,7 @@
     useContributorAgreements = cfg.getBoolean("auth", "contributoragreements", false);
     userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
     allowRegisterNewEmail = cfg.getBoolean("auth", "allowRegisterNewEmail", true);
+    userNameCaseInsensitive = cfg.getBoolean("auth", "userNameCaseInsensitive", false);
 
     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP
         && authType != AuthType.LDAP
@@ -227,7 +229,7 @@
     return trustContainerAuth;
   }
 
-  /** @return true if users with Run As capability can impersonate others. */
+  /** Returns true if users with Run As capability can impersonate others. */
   public boolean isRunAsEnabled() {
     return enableRunAs;
   }
@@ -237,6 +239,11 @@
     return userNameToLowerCase;
   }
 
+  /** Whether user name should be matched case insenitive */
+  public boolean isUserNameCaseInsensitive() {
+    return userNameCaseInsensitive;
+  }
+
   public GitBasicAuthPolicy getGitBasicAuthPolicy() {
     return gitBasicAuthPolicy;
   }
diff --git a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
index b37e489..4032e63 100644
--- a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -32,9 +32,9 @@
  * <p>1. Help the callers figure out if any action should be taken, depending on which entries are
  * updated in gerrit.config.
  *
- * <p>2. Provide the callers with a mechanism to accept/reject the entries of interest: @see
- * accept(Set<ConfigKey> entries), @see accept(String section), @see reject(Set<ConfigKey> entries)
- * (+ various overloaded versions of these)
+ * <p>2. Provide the callers with a mechanism to accept/reject the entries of interest: {@link
+ * #accept(Set)}, {@link #accept(String)}, {@link #reject(Set)} (+ various overloaded versions of
+ * these)
  */
 public class ConfigUpdatedEvent {
   public static final ImmutableMultimap<UpdateResult, ConfigUpdateEntry> NO_UPDATES =
diff --git a/java/com/google/gerrit/server/config/ConfigUtil.java b/java/com/google/gerrit/server/config/ConfigUtil.java
index 27ded63..c44b0fd 100644
--- a/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -282,7 +282,6 @@
    * @param sub subsection
    * @param s instance of class with config values
    * @param defaults instance of class with default values
-   * @throws ConfigInvalidException
    */
   public static <T> void storeSection(Config cfg, String section, String sub, T s, T defaults)
       throws ConfigInvalidException {
@@ -341,7 +340,6 @@
    * @param i instance to merge during the load. When present, the boolean fields are not nullified
    *     when their values are false
    * @return loaded instance
-   * @throws ConfigInvalidException
    */
   public static <T> T loadSection(Config cfg, String section, String sub, T s, T defaults, T i)
       throws ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java b/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java
new file mode 100644
index 0000000..ebb0e50
--- /dev/null
+++ b/java/com/google/gerrit/server/config/FileBasedAllProjectsConfigProvider.java
@@ -0,0 +1,33 @@
+package com.google.gerrit.server.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
+import java.util.Optional;
+import javax.inject.Singleton;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+@Singleton
+public class FileBasedAllProjectsConfigProvider implements AllProjectsConfigProvider {
+  private final SitePaths sitePaths;
+
+  @VisibleForTesting
+  @Inject
+  public FileBasedAllProjectsConfigProvider(SitePaths sitePaths) {
+    this.sitePaths = sitePaths;
+  }
+
+  @Override
+  public Optional<StoredConfig> get(AllProjectsName allProjectsName) {
+    return Optional.of(
+        new FileBasedConfig(
+            sitePaths
+                .etc_dir
+                .resolve(allProjectsName.get())
+                .resolve(ProjectConfig.PROJECT_CONFIG)
+                .toFile(),
+            FS.DETECTED));
+  }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index ac4a8d9..35b16b4 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -85,6 +85,7 @@
 import com.google.gerrit.server.ExceptionHookImpl;
 import com.google.gerrit.server.ExternalUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PerformanceMetrics;
 import com.google.gerrit.server.RequestListener;
 import com.google.gerrit.server.TraceRequestListener;
 import com.google.gerrit.server.account.AccountCacheImpl;
@@ -92,6 +93,8 @@
 import com.google.gerrit.server.account.AccountDeactivator;
 import com.google.gerrit.server.account.AccountExternalIdCreator;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountModule;
+import com.google.gerrit.server.account.AccountTagProvider;
 import com.google.gerrit.server.account.AccountVisibilityProvider;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.account.EmailExpander;
@@ -100,6 +103,7 @@
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.ServiceUserClassifierImpl;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
 import com.google.gerrit.server.account.externalids.ExternalIdModule;
 import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
 import com.google.gerrit.server.approval.ApprovalCacheImpl;
@@ -130,6 +134,7 @@
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MergedByPushOp;
+import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.PureRevertCache;
 import com.google.gerrit.server.git.ReceivePackInitializer;
@@ -182,6 +187,7 @@
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectNameLockManager;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.approval.ApprovalModule;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -260,10 +266,13 @@
     install(TagCache.module());
     install(PureRevertCache.module());
     install(CommentContextCacheImpl.module());
+    install(SubmitRequirementsEvaluatorImpl.module());
 
     install(new AccessControlModule());
+    install(new AccountModule());
     install(new CmdLineParserModule());
     install(new EmailModule());
+    install(new ExternalIdCacheModule());
     install(new ExternalIdModule());
     install(new GitModule());
     install(new GroupDbModule());
@@ -284,6 +293,7 @@
     factory(ChangeIsVisibleToPredicate.Factory.class);
     factory(DeadlineChecker.Factory.class);
     factory(MergeUtil.Factory.class);
+    factory(MultiProgressMonitor.Factory.class);
     factory(PatchScriptFactory.Factory.class);
     factory(PatchScriptFactoryForAutoFix.Factory.class);
     factory(ProjectState.Factory.class);
@@ -422,6 +432,7 @@
     DynamicSet.setOf(binder(), SubmitRule.class);
     DynamicSet.setOf(binder(), QuotaEnforcer.class);
     DynamicSet.setOf(binder(), PerformanceLogger.class);
+    DynamicSet.bind(binder(), PerformanceLogger.class).to(PerformanceMetrics.class);
     DynamicSet.setOf(binder(), RequestListener.class);
     DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
     DynamicSet.setOf(binder(), ChangeETagComputation.class);
@@ -429,6 +440,7 @@
     DynamicSet.bind(binder(), ExceptionHook.class).to(ExceptionHookImpl.class);
     DynamicSet.setOf(binder(), MailSoyTemplateProvider.class);
     DynamicSet.setOf(binder(), OnPostReview.class);
+    DynamicMap.mapOf(binder(), AccountTagProvider.class);
 
     DynamicMap.mapOf(binder(), MailFilter.class);
     bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/config/GerritIsReplica.java b/java/com/google/gerrit/server/config/GerritIsReplica.java
index 154fdcd..ab6aa8b 100644
--- a/java/com/google/gerrit/server/config/GerritIsReplica.java
+++ b/java/com/google/gerrit/server/config/GerritIsReplica.java
@@ -19,7 +19,7 @@
 import com.google.inject.BindingAnnotation;
 import java.lang.annotation.Retention;
 
-/* Marker on {@link Boolean} indicating whether Gerrit is run as a read-only replica. */
+/** Marker on {@link Boolean} indicating whether Gerrit is run as a read-only replica. */
 @Retention(RUNTIME)
 @BindingAnnotation
 public @interface GerritIsReplica {}
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index da85834..8ddcdac 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -66,6 +66,7 @@
     bind(Config.class)
         .annotatedWith(GerritServerConfig.class)
         .toProvider(GerritServerConfigProvider.class);
+    bind(AllProjectsConfigProvider.class).to(FileBasedAllProjectsConfigProvider.class);
     bind(GlobalPluginConfigProvider.class).to(FileBasedGlobalPluginConfigProvider.class);
     bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
     bind(Boolean.class)
diff --git a/java/com/google/gerrit/server/config/GitwebCgiConfig.java b/java/com/google/gerrit/server/config/GitwebCgiConfig.java
index d7fb83c..1ed0f16 100644
--- a/java/com/google/gerrit/server/config/GitwebCgiConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebCgiConfig.java
@@ -118,22 +118,22 @@
     this.logoPng = null;
   }
 
-  /** @return local path to the CGI executable; null if we shouldn't execute. */
+  /** Returns local path to the CGI executable; null if we shouldn't execute. */
   public Path getGitwebCgi() {
     return cgi;
   }
 
-  /** @return local path of the {@code gitweb.css} matching the CGI. */
+  /** Returns local path of the {@code gitweb.css} matching the CGI. */
   public Path getGitwebCss() {
     return css;
   }
 
-  /** @return local path of the {@code gitweb.js} for the CGI. */
+  /** Returns local path of the {@code gitweb.js} for the CGI. */
   public Path getGitwebJs() {
     return js;
   }
 
-  /** @return local path of the {@code git-logo.png} for the CGI. */
+  /** Returns local path of the {@code git-logo.png} for the CGI. */
   public Path getGitLogoPng() {
     return logoPng;
   }
diff --git a/java/com/google/gerrit/server/config/GitwebConfig.java b/java/com/google/gerrit/server/config/GitwebConfig.java
index f90a72e..5632978 100644
--- a/java/com/google/gerrit/server/config/GitwebConfig.java
+++ b/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -213,16 +213,16 @@
     }
   }
 
-  /** @return GitwebType for gitweb viewer. */
+  /** Returns GitwebType for gitweb viewer. */
   @Nullable
   public GitwebType getGitwebType() {
     return type;
   }
 
   /**
-   * @return URL of the entry point into gitweb. This URL may be relative to our context if gitweb
-   *     is hosted by ourselves; or absolute if its hosted elsewhere; or null if gitweb has not been
-   *     configured.
+   * Returns URL of the entry point into gitweb. This URL may be relative to our context if gitweb
+   * is hosted by ourselves; or absolute if its hosted elsewhere; or null if gitweb has not been
+   * configured.
    */
   public String getUrl() {
     return url;
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index fcfa5e9..c09988e3 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -206,16 +206,18 @@
   }
 
   /**
+   * Returns whether the project is editable
+   *
    * @param project project state.
-   * @return whether the project is editable.
    */
   public boolean isEditable(ProjectState project) {
     return true;
   }
 
   /**
+   * Returns any warning associated with the project
+   *
    * @param project project state.
-   * @return any warning associated with the project.
    */
   public String getWarning(ProjectState project) {
     return null;
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 710916e..6b018ce 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -146,9 +146,6 @@
    * @param edit change edit to publish
    * @param notify Notify handling that defines to whom email notifications should be sent after the
    *     change edit is published.
-   * @throws IOException
-   * @throws UpdateException
-   * @throws RestApiException
    */
   public void publish(
       BatchUpdate.Factory updateFactory,
@@ -209,7 +206,6 @@
    * Delete change edit.
    *
    * @param edit change edit to delete
-   * @throws IOException
    */
   public void delete(ChangeEdit edit) throws IOException {
     Change change = edit.getChange();
diff --git a/java/com/google/gerrit/server/events/EventsMetrics.java b/java/com/google/gerrit/server/events/EventsMetrics.java
index 3c87cca..6d48c37 100644
--- a/java/com/google/gerrit/server/events/EventsMetrics.java
+++ b/java/com/google/gerrit/server/events/EventsMetrics.java
@@ -32,7 +32,9 @@
         metricMaker.newCounter(
             "events",
             new Description("Triggered events").setRate().setUnit("triggered events"),
-            Field.ofString("type", Metadata.Builder::eventType).build());
+            Field.ofString("type", Metadata.Builder::eventType)
+                .description("The type of the event.")
+                .build());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 65662ba..b060d3e 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -26,11 +26,12 @@
       "GerritBackendRequestFeature__remove_revision_etag";
 
   /**
-   * Whether git pushes are cancelled if the client disconnects or the configured receive.timeout is
-   * exceeded.
+   * Allow legacy {@link com.google.gerrit.entities.SubmitRecord}s to be converted and returned as
+   * submit requirements by the {@link
+   * com.google.gerrit.server.project.SubmitRequirementsEvaluator}.
    */
-  public static final String GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION =
-      "GerritBackendRequestFeature__enable_push_cencallation";
+  public static final String GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS =
+      "GerritBackendRequestFeature__enable_legacy_submit_requirements";
 
   /** Features, enabled by default in the current release. */
   public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
diff --git a/java/com/google/gerrit/server/extensions/webui/UiActions.java b/java/com/google/gerrit/server/extensions/webui/UiActions.java
index a7f6b48..34c3c20 100644
--- a/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -72,7 +72,9 @@
             new com.google.gerrit.metrics.Description("Latency for RestView#getDescription calls")
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
-            Field.ofString("view", Metadata.Builder::restViewName).build());
+            Field.ofString("view", Metadata.Builder::restViewName)
+                .description("view implementation class")
+                .build());
   }
 
   public <R extends RestResource> Iterable<UiAction.Description> from(
diff --git a/java/com/google/gerrit/server/git/DelegateRefDatabase.java b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
index decae05..bc5dd00 100644
--- a/java/com/google/gerrit/server/git/DelegateRefDatabase.java
+++ b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server.git;
 
 import java.io.IOException;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
@@ -34,7 +36,7 @@
 
   private Repository delegate;
 
-  DelegateRefDatabase(Repository delegate) {
+  public DelegateRefDatabase(Repository delegate) {
     this.delegate = delegate;
   }
 
@@ -49,11 +51,21 @@
   }
 
   @Override
+  public boolean hasVersioning() {
+    return delegate.getRefDatabase().hasVersioning();
+  }
+
+  @Override
   public boolean isNameConflicting(String name) throws IOException {
     return delegate.getRefDatabase().isNameConflicting(name);
   }
 
   @Override
+  public Collection<String> getConflictingNames(String name) throws IOException {
+    return delegate.getRefDatabase().getConflictingNames(name);
+  }
+
+  @Override
   public RefUpdate newUpdate(String name, boolean detach) throws IOException {
     return delegate.getRefDatabase().newUpdate(name, detach);
   }
@@ -64,10 +76,35 @@
   }
 
   @Override
+  public BatchRefUpdate newBatchUpdate() {
+    return delegate.getRefDatabase().newBatchUpdate();
+  }
+
+  @Override
+  public boolean performsAtomicTransactions() {
+    return delegate.getRefDatabase().performsAtomicTransactions();
+  }
+
+  @Override
   public Ref exactRef(String name) throws IOException {
     return delegate.getRefDatabase().exactRef(name);
   }
 
+  @Override
+  public Map<String, Ref> exactRef(String... refs) throws IOException {
+    return delegate.getRefDatabase().exactRef(refs);
+  }
+
+  @Override
+  public Ref firstExactRef(String... refs) throws IOException {
+    return delegate.getRefDatabase().firstExactRef(refs);
+  }
+
+  @Override
+  public List<Ref> getRefs() throws IOException {
+    return delegate.getRefDatabase().getRefs();
+  }
+
   @SuppressWarnings("deprecation")
   @Override
   public Map<String, Ref> getRefs(String prefix) throws IOException {
@@ -75,12 +112,38 @@
   }
 
   @Override
+  public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+    return delegate.getRefDatabase().getRefsByPrefix(prefix);
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
+      throws IOException {
+    return delegate.getRefDatabase().getRefsByPrefixWithExclusions(include, excludes);
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String... prefixes) throws IOException {
+    return delegate.getRefDatabase().getRefsByPrefix(prefixes);
+  }
+
+  @Override
   @NonNull
   public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
     return delegate.getRefDatabase().getTipsWithSha1(id);
   }
 
   @Override
+  public boolean hasFastTipsWithSha1() throws IOException {
+    return delegate.getRefDatabase().hasFastTipsWithSha1();
+  }
+
+  @Override
+  public boolean hasRefs() throws IOException {
+    return delegate.getRefDatabase().hasRefs();
+  }
+
+  @Override
   public List<Ref> getAdditionalRefs() throws IOException {
     return delegate.getRefDatabase().getAdditionalRefs();
   }
@@ -90,7 +153,12 @@
     return delegate.getRefDatabase().peel(ref);
   }
 
-  Repository getDelegate() {
+  @Override
+  public void refresh() {
+    delegate.getRefDatabase().refresh();
+  }
+
+  protected Repository getDelegate() {
     return delegate;
   }
 }
diff --git a/java/com/google/gerrit/server/git/GitRepositoryManager.java b/java/com/google/gerrit/server/git/GitRepositoryManager.java
index e4d0696..8dba3e1 100644
--- a/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -30,6 +30,23 @@
  */
 @ImplementedBy(value = LocalDiskRepositoryManager.class)
 public interface GitRepositoryManager {
+
+  /** Status of the repository. */
+  enum Status {
+    /** Repository exists and is available on host. */
+    ACTIVE,
+    /** Repository does not exist. */
+    NON_EXISTENT,
+    /**
+     * Repository might exist but can not be opened. This can for example be the case when the
+     * repository is pending deletion / the caller does not have permissions / repository is broken.
+     */
+    UNAVAILABLE;
+  }
+
+  /** Get {@link Status} of the repository by name. */
+  Status getRepositoryStatus(Project.NameKey name);
+
   /**
    * Get (or open) a repository by name.
    *
@@ -47,15 +64,16 @@
    * @param name the repository name, relative to the base directory.
    * @return the cached Repository instance. Caller must call {@code close()} when done to decrement
    *     the resource handle.
+   * @throws RepositoryExistsException repository exists.
    * @throws RepositoryCaseMismatchException the name collides with an existing repository name, but
    *     only in case of a character within the name.
    * @throws RepositoryNotFoundException the name is invalid.
    * @throws IOException the repository cannot be created.
    */
   Repository createRepository(Project.NameKey name)
-      throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException;
+      throws RepositoryNotFoundException, RepositoryExistsException, IOException;
 
-  /** @return set of all known projects, sorted by natural NameKey order. */
+  /** Returns set of all known projects, sorted by natural NameKey order. */
   SortedSet<Project.NameKey> list();
 
   /**
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 10220d8..1dc5c16 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -16,6 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -128,6 +129,29 @@
   }
 
   @Override
+  public Status getRepositoryStatus(NameKey name) {
+    if (isUnreasonableName(name)) {
+      return Status.NON_EXISTENT;
+    }
+    Path path = getBasePath(name);
+    File dir = FileKey.resolve(path.resolve(name.get()).toFile(), FS.DETECTED);
+    if (dir == null) {
+      return Status.NON_EXISTENT;
+    }
+    Repository repo;
+    try {
+      // Try to open with mustExist, so that it does not attempt to create a repository.
+      repo = RepositoryCache.open(FileKey.lenient(dir, FS.DETECTED), /*mustExist=*/ true);
+    } catch (RepositoryNotFoundException e) {
+      return Status.NON_EXISTENT;
+    } catch (IOException e) {
+      return Status.UNAVAILABLE;
+    }
+    // If object database does not exist, the repository is unusable
+    return repo.getObjectDatabase().exists() ? Status.ACTIVE : Status.UNAVAILABLE;
+  }
+
+  @Override
   public Repository openRepository(Project.NameKey name) throws RepositoryNotFoundException {
     return openRepository(getBasePath(name), name);
   }
@@ -147,7 +171,7 @@
 
   @Override
   public Repository createRepository(Project.NameKey name)
-      throws RepositoryNotFoundException, RepositoryCaseMismatchException, IOException {
+      throws RepositoryNotFoundException, RepositoryExistsException, IOException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
@@ -162,8 +186,7 @@
       if (!onDiskName.equals(name)) {
         throw new RepositoryCaseMismatchException(name);
       }
-
-      throw new IllegalStateException("Repository already exists: " + name);
+      throw new RepositoryExistsException(name);
     }
 
     // It doesn't exist under any of the standard permutations
diff --git a/java/com/google/gerrit/server/git/MergeTip.java b/java/com/google/gerrit/server/git/MergeTip.java
index 204f453..4ffa1a8 100644
--- a/java/com/google/gerrit/server/git/MergeTip.java
+++ b/java/com/google/gerrit/server/git/MergeTip.java
@@ -52,8 +52,8 @@
   }
 
   /**
-   * @return the initial tip of the branch before the merge operation started; may be null,
-   *     indicating a previously unborn branch.
+   * Returns the initial tip of the branch before the merge operation started; may be null,
+   * indicating a previously unborn branch.
    */
   public CodeReviewCommit getInitialTip() {
     return initialTip;
@@ -82,8 +82,8 @@
   }
 
   /**
-   * @return The current tip of the current merge operation; may be null, indicating an unborn
-   *     branch.
+   * Returns The current tip of the current merge operation; may be null, indicating an unborn
+   * branch.
    */
   @Nullable
   public CodeReviewCommit getCurrentTip() {
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index 3385969..3a4d407 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -512,9 +512,6 @@
    *   <li>Change-Id
    * </ul>
    *
-   * @param n
-   * @param notes
-   * @param psId
    * @return new message
    */
   private String createDetailedCommitMessage(RevCommit n, ChangeNotes notes, PatchSet.Id psId) {
@@ -630,10 +627,6 @@
    * Plugins implementing {@link ChangeMessageModifier} can modify the resulting commit message
    * arbitrarily.
    *
-   * @param n
-   * @param mergeTip
-   * @param notes
-   * @param id
    * @return new message
    */
   public String createCommitMessageOnSubmit(
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 22385c7..a4b1033 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -14,16 +14,17 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.gerrit.server.DeadlineChecker.TIMEOUT_FORMATTER;
+import static com.google.gerrit.server.DeadlineChecker.getTimeoutFormatter;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.gerrit.server.CancellationMetrics;
 import com.google.gerrit.server.cancellation.RequestStateProvider;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
-import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
@@ -80,6 +81,11 @@
   private static final char[] SPINNER_STATES = new char[] {'-', '\\', '|', '/'};
   private static final char NO_SPINNER = ' ';
 
+  public enum TaskKind {
+    INDEXING,
+    RECEIVE_COMMITS;
+  }
+
   /** Handle for a sub-task. */
   public class Task implements ProgressMonitor {
     private final String name;
@@ -149,8 +155,20 @@
     }
   }
 
-  private final ExperimentFeatures experimentFeatures;
+  public interface Factory {
+    MultiProgressMonitor create(OutputStream out, TaskKind taskKind, String taskName);
+
+    MultiProgressMonitor create(
+        OutputStream out,
+        TaskKind taskKind,
+        String taskName,
+        long maxIntervalTime,
+        TimeUnit maxIntervalUnit);
+  }
+
+  private final CancellationMetrics cancellationMetrics;
   private final OutputStream out;
+  private final TaskKind taskKind;
   private final String taskName;
   private final List<Task> tasks = new CopyOnWriteArrayList<>();
   private int spinnerIndex;
@@ -158,6 +176,7 @@
   private boolean done;
   private boolean clientDisconnected;
   private boolean deadlineExceeded;
+  private boolean forcefulTermination;
   private Optional<Long> timeout = Optional.empty();
 
   private final long maxIntervalNanos;
@@ -168,9 +187,13 @@
    * @param out stream for writing progress messages.
    * @param taskName name of the overall task.
    */
-  public MultiProgressMonitor(
-      ExperimentFeatures experimentFeatures, OutputStream out, String taskName) {
-    this(experimentFeatures, out, taskName, 500, MILLISECONDS);
+  @AssistedInject
+  private MultiProgressMonitor(
+      CancellationMetrics cancellationMetrics,
+      @Assisted OutputStream out,
+      @Assisted TaskKind taskKind,
+      @Assisted String taskName) {
+    this(cancellationMetrics, out, taskKind, taskName, 500, MILLISECONDS);
   }
 
   /**
@@ -181,14 +204,17 @@
    * @param maxIntervalTime maximum interval between progress messages.
    * @param maxIntervalUnit time unit for progress interval.
    */
-  public MultiProgressMonitor(
-      ExperimentFeatures experimentFeatures,
-      OutputStream out,
-      String taskName,
-      long maxIntervalTime,
-      TimeUnit maxIntervalUnit) {
-    this.experimentFeatures = experimentFeatures;
+  @AssistedInject
+  private MultiProgressMonitor(
+      CancellationMetrics cancellationMetrics,
+      @Assisted OutputStream out,
+      @Assisted TaskKind taskKind,
+      @Assisted String taskName,
+      @Assisted long maxIntervalTime,
+      @Assisted TimeUnit maxIntervalUnit) {
+    this.cancellationMetrics = cancellationMetrics;
     this.out = out;
+    this.taskKind = taskKind;
     this.taskName = taskName;
     maxIntervalNanos = NANOSECONDS.convert(maxIntervalTime, maxIntervalUnit);
   }
@@ -264,10 +290,14 @@
         long now = System.nanoTime();
 
         if (deadline > 0 && now > deadline) {
-          logger.atFine().log(
-              "deadline exceeded after %sms: (timeout %sms, signaling cancellation)",
-              MILLISECONDS.convert(now - overallStart, NANOSECONDS),
-              MILLISECONDS.convert(now - deadline, NANOSECONDS));
+          if (!deadlineExceeded) {
+            logger.atFine().log(
+                "deadline exceeded after %sms, signaling cancellation (timeout=%sms, task=%s(%s))",
+                MILLISECONDS.convert(now - overallStart, NANOSECONDS),
+                MILLISECONDS.convert(now - deadline, NANOSECONDS),
+                taskKind,
+                taskName);
+          }
           deadlineExceeded = true;
 
           // After setting deadlineExceeded = true give the cancellationNanos to react to the
@@ -275,11 +305,17 @@
           if (now > deadline + cancellationNanos) {
             // The worker didn't react to the cancellation, cancel it forcefully by an interrupt.
             workerFuture.cancel(true);
+            forcefulTermination = true;
             if (workerFuture.isCancelled()) {
               logger.atWarning().log(
-                  "MultiProgressMonitor worker killed after %sms: (timeout %sms, cancelled)",
+                  "MultiProgressMonitor worker killed after %sms, cancelled (timeout=%sms, task=%s(%s))",
                   MILLISECONDS.convert(now - overallStart, NANOSECONDS),
-                  MILLISECONDS.convert(now - deadline, NANOSECONDS));
+                  MILLISECONDS.convert(now - deadline, NANOSECONDS),
+                  taskKind,
+                  taskName);
+              if (taskKind == TaskKind.RECEIVE_COMMITS) {
+                cancellationMetrics.countForcefulReceiveTimeout();
+              }
             }
             break;
           }
@@ -294,10 +330,15 @@
         if (!done && workerFuture.isDone()) {
           // The worker may not have called end() explicitly, which is likely a
           // programming error.
-          logger.atWarning().log("MultiProgressMonitor worker did not call end() before returning");
+          logger.atWarning().log(
+              "MultiProgressMonitor worker did not call end() before returning (task=%s(%s))",
+              taskKind, taskName);
           end();
         }
       }
+      if (deadlineExceeded && !forcefulTermination && taskKind == TaskKind.RECEIVE_COMMITS) {
+        cancellationMetrics.countGracefulReceiveTimeout();
+      }
       sendDone();
     }
 
@@ -306,7 +347,8 @@
     try {
       return workerFuture.get(maxIntervalNanos, NANOSECONDS);
     } catch (InterruptedException | CancellationException e) {
-      logger.atWarning().withCause(e).log("unable to finish processing");
+      logger.atWarning().withCause(e).log(
+          "unable to finish processing (task=%s(%s))", taskKind, taskName);
       throw new UncheckedExecutionException(e);
     } catch (TimeoutException e) {
       workerFuture.cancel(true);
@@ -412,7 +454,8 @@
         out.flush();
       } catch (IOException e) {
         logger.atWarning().withCause(e).log(
-            "Sending progress to client failed. Stop sending updates for task %s", taskName);
+            "Sending progress to client failed. Stop sending updates for task %s(%s)",
+            taskKind, taskName);
         clientDisconnected = true;
       }
     }
@@ -420,17 +463,17 @@
 
   @Override
   public void checkIfCancelled(OnCancelled onCancelled) {
-    if (!experimentFeatures.isFeatureEnabled(
-        ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION)) {
-      return;
-    }
-
     if (clientDisconnected) {
       onCancelled.onCancel(RequestStateProvider.Reason.CLIENT_CLOSED_REQUEST, /* message= */ null);
     } else if (deadlineExceeded) {
       onCancelled.onCancel(
           RequestStateProvider.Reason.SERVER_DEADLINE_EXCEEDED,
-          timeout.map(TIMEOUT_FORMATTER).orElse(null));
+          timeout
+              .map(
+                  taskKind == TaskKind.RECEIVE_COMMITS
+                      ? getTimeoutFormatter("receive.timeout")
+                      : getTimeoutFormatter("timeout"))
+              .orElse(null));
     }
   }
 }
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index b7dc2b3..99a66f8 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -33,8 +34,10 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefRename;
@@ -61,6 +64,11 @@
   }
 
   @Override
+  public Collection<String> getConflictingNames(String name) throws IOException {
+    throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
+  }
+
+  @Override
   public RefUpdate newUpdate(String name, boolean detach) {
     throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
   }
@@ -71,6 +79,11 @@
   }
 
   @Override
+  public BatchRefUpdate newBatchUpdate() {
+    throw new UnsupportedOperationException("PermissionAwareReadOnlyRefDatabase is read-only");
+  }
+
+  @Override
   public Ref exactRef(String name) throws IOException {
     Ref ref = getDelegate().getRefDatabase().exactRef(name);
     if (ref == null) {
@@ -148,6 +161,25 @@
   }
 
   @Override
+  public List<Ref> getRefsByPrefixWithExclusions(String include, Set<String> excludes)
+      throws IOException {
+    Stream<Ref> refs = getRefs(include).values().stream();
+    for (String exclude : excludes) {
+      refs = refs.filter(r -> !r.getName().startsWith(exclude));
+    }
+    return Collections.unmodifiableList(refs.collect(Collectors.toList()));
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String... prefixes) throws IOException {
+    List<Ref> result = new ArrayList<>();
+    for (String prefix : prefixes) {
+      result.addAll(getRefsByPrefix(prefix));
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  @Override
   @NonNull
   public Map<String, Ref> exactRef(String... refs) throws IOException {
     Map<String, Ref> result = new HashMap<>(refs.length);
@@ -173,6 +205,11 @@
   }
 
   @Override
+  public List<Ref> getRefs() throws IOException {
+    return getRefsByPrefix(ALL);
+  }
+
+  @Override
   @NonNull
   public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
     Set<Ref> unfiltered = super.getTipsWithSha1(id);
@@ -184,4 +221,9 @@
     }
     return result;
   }
+
+  @Override
+  public boolean hasRefs() throws IOException {
+    return !getRefs().isEmpty();
+  }
 }
diff --git a/java/com/google/gerrit/server/git/RepoRefCache.java b/java/com/google/gerrit/server/git/RepoRefCache.java
index 6b2493a..c69f9a6 100644
--- a/java/com/google/gerrit/server/git/RepoRefCache.java
+++ b/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -46,7 +46,7 @@
     return id;
   }
 
-  /** @return an unmodifiable view of the refs that have been cached by this instance. */
+  /** Returns an unmodifiable view of the refs that have been cached by this instance. */
   public Map<String, Optional<ObjectId>> getCachedRefs() {
     return Collections.unmodifiableMap(ids);
   }
diff --git a/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java b/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
index 8535cd2..9e10c67 100644
--- a/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
+++ b/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.entities.Project;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
 /**
  * This exception is thrown if a project cannot be created because a project with the same name in a
@@ -23,12 +22,12 @@
  * (e.g. Windows), because in this case the name for the git repository in the file system is
  * already occupied by the existing project.
  */
-public class RepositoryCaseMismatchException extends RepositoryNotFoundException {
+public class RepositoryCaseMismatchException extends RepositoryExistsException {
 
   private static final long serialVersionUID = 1L;
 
   /** @param projectName name of the project that cannot be created */
   public RepositoryCaseMismatchException(Project.NameKey projectName) {
-    super("Name occupied in other case. Project " + projectName.get() + " cannot be created.");
+    super(projectName, "Name occupied in other case.");
   }
 }
diff --git a/java/com/google/gerrit/server/git/RepositoryExistsException.java b/java/com/google/gerrit/server/git/RepositoryExistsException.java
new file mode 100644
index 0000000..563b078
--- /dev/null
+++ b/java/com/google/gerrit/server/git/RepositoryExistsException.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.entities.Project;
+import java.io.IOException;
+
+/** Thrown when trying to create a repository that exist. */
+public class RepositoryExistsException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * @param projectName name of the project that cannot be created
+   * @param reason reason why the project cannot be created
+   */
+  public RepositoryExistsException(Project.NameKey projectName, String reason) {
+    super(
+        String.format("Repository %s exists and cannot be created. %s", projectName.get(), reason));
+  }
+
+  /** @param projectName name of the project that cannot be created */
+  public RepositoryExistsException(Project.NameKey projectName) {
+    super(String.format("Repository %s exists and cannot be created.", projectName.get()));
+  }
+}
diff --git a/java/com/google/gerrit/server/git/TransferConfig.java b/java/com/google/gerrit/server/git/TransferConfig.java
index 55b9448..728e4ed 100644
--- a/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/java/com/google/gerrit/server/git/TransferConfig.java
@@ -52,7 +52,7 @@
     packConfig.fromConfig(cfg);
   }
 
-  /** @return configured timeout, in seconds. 0 if the timeout is infinite. */
+  /** Returns configured timeout, in seconds. 0 if the timeout is infinite. */
   public int getTimeout() {
     return timeout;
   }
diff --git a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
index 4afff2b..1619add 100644
--- a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
+++ b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -45,7 +45,9 @@
   @Inject
   UploadPackMetricsHook(MetricMaker metricMaker) {
     Field<Operation> operationField =
-        Field.ofEnum(Operation.class, "operation", Metadata.Builder::gitOperation).build();
+        Field.ofEnum(Operation.class, "operation", Metadata.Builder::gitOperation)
+            .description("The name of the operation (CLONE, FETCH).")
+            .build();
     requestCount =
         metricMaker.newCounter(
             "git/upload-pack/request_count",
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index e90f58b..27d5da9 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -160,7 +160,7 @@
       return create(name, null);
     }
 
-    /** @see User#create(Project.NameKey, IdentifiedUser, BatchRefUpdate) */
+    /** See {@link User#create(Project.NameKey, IdentifiedUser, BatchRefUpdate)} */
     public MetaDataUpdate create(Project.NameKey name, BatchRefUpdate batch)
         throws RepositoryNotFoundException, IOException {
       Repository repo = mgr.openRepository(name);
@@ -234,7 +234,7 @@
     this.closeRepository = closeRepository;
   }
 
-  /** @return batch in which to run the update, or {@code null} for no batch. */
+  /** Returns batch in which to run the update, or {@code null} for no batch. */
   BatchRefUpdate getBatch() {
     return batch;
   }
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index feb038a..a42ab8f 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -35,6 +35,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -99,7 +100,7 @@
   protected ObjectInserter inserter;
   protected DirCache newTree;
 
-  /** @return name of the reference storing this configuration. */
+  /** Returns name of the reference storing this configuration. */
   protected abstract String getRefName();
 
   /** Set up the metadata, parsing any state from the loaded revision. */
@@ -109,13 +110,11 @@
    * Save any changes to the metadata in a commit.
    *
    * @return true if the commit should proceed, false to abort.
-   * @throws IOException
-   * @throws ConfigInvalidException
    */
   protected abstract boolean onSave(CommitBuilder commit)
       throws IOException, ConfigInvalidException;
 
-  /** @return revision of the metadata that was loaded. */
+  /** Returns revision of the metadata that was loaded. */
   @Nullable
   public ObjectId getRevision() {
     return ObjectIds.copyOrNull(revision);
@@ -129,8 +128,6 @@
    *
    * @param projectName the name of the project
    * @param db repository to access.
-   * @throws IOException
-   * @throws ConfigInvalidException
    */
   public void load(Project.NameKey projectName, Repository db)
       throws IOException, ConfigInvalidException {
@@ -151,8 +148,6 @@
    * @param projectName the name of the project
    * @param db repository to access.
    * @param id revision to load.
-   * @throws IOException
-   * @throws ConfigInvalidException
    */
   public void load(Project.NameKey projectName, Repository db, @Nullable ObjectId id)
       throws IOException, ConfigInvalidException {
@@ -175,8 +170,6 @@
    * @param projectName the name of the project
    * @param walk open walk to access to access.
    * @param id revision to load.
-   * @throws IOException
-   * @throws ConfigInvalidException
    */
   public void load(Project.NameKey projectName, RevWalk walk, ObjectId id)
       throws IOException, ConfigInvalidException {
@@ -461,12 +454,12 @@
   }
 
   protected Config readConfig(String fileName) throws IOException, ConfigInvalidException {
-    return readConfig(fileName, null);
+    return readConfig(fileName, Optional.empty());
   }
 
-  protected Config readConfig(String fileName, Config baseConfig)
+  protected Config readConfig(String fileName, Optional<? extends Config> baseConfig)
       throws IOException, ConfigInvalidException {
-    Config rc = new Config(baseConfig);
+    Config rc = new Config(baseConfig.isPresent() ? baseConfig.get() : null);
     String text = readUTF8(fileName);
     if (!text.isEmpty()) {
       try {
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 5b26f61..488b008 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -40,8 +40,8 @@
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.ReceiveCommitsExecutor;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
 import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.TransferConfig;
@@ -141,9 +141,8 @@
   }
 
   private static MultiProgressMonitor newMultiProgressMonitor(
-      ExperimentFeatures experimentFeatures, MessageSender messageSender) {
-    return new MultiProgressMonitor(
-        experimentFeatures,
+      MultiProgressMonitor.Factory multiProgressMonitorFactory, MessageSender messageSender) {
+    return multiProgressMonitorFactory.create(
         new OutputStream() {
           @Override
           public void write(int b) {
@@ -165,6 +164,7 @@
             messageSender.flush();
           }
         },
+        TaskKind.RECEIVE_COMMITS,
         "Processing changes");
   }
 
@@ -222,7 +222,7 @@
     }
   }
 
-  private final ExperimentFeatures experimentFeatures;
+  private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
   private final Metrics metrics;
   private final ReceiveCommits receiveCommits;
   private final PermissionBackend.ForProject perm;
@@ -240,7 +240,7 @@
 
   @Inject
   AsyncReceiveCommits(
-      ExperimentFeatures experimentFeatures,
+      MultiProgressMonitor.Factory multiProgressMonitorFactory,
       ReceiveCommits.Factory factory,
       PermissionBackend permissionBackend,
       Provider<InternalChangeQuery> queryProvider,
@@ -261,7 +261,7 @@
       @Assisted Repository repo,
       @Assisted @Nullable MessageSender messageSender)
       throws PermissionBackendException {
-    this.experimentFeatures = experimentFeatures;
+    this.multiProgressMonitorFactory = multiProgressMonitorFactory;
     this.executor = executor;
     this.scopePropagator = scopePropagator;
     this.receiveConfig = receiveConfig;
@@ -386,7 +386,7 @@
     }
     String currentThreadName = Thread.currentThread().getName();
     MultiProgressMonitor monitor =
-        newMultiProgressMonitor(experimentFeatures, receiveCommits.getMessageSender());
+        newMultiProgressMonitor(multiProgressMonitorFactory, receiveCommits.getMessageSender());
     Callable<ReceiveCommitsResult> callable =
         () -> {
           String oldName = Thread.currentThread().getName();
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 1e77cc4..c1cd30c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -102,7 +102,9 @@
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter3;
 import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.CancellationMetrics;
 import com.google.gerrit.server.ChangeUtil;
@@ -323,6 +325,7 @@
   @Singleton
   private static class Metrics {
     private final Counter0 psRevisionMissing;
+    private final Counter3<String, String, String> pushCount;
 
     @Inject
     Metrics(MetricMaker metricMaker) {
@@ -330,6 +333,23 @@
           metricMaker.newCounter(
               "receivecommits/ps_revision_missing",
               new Description("errors due to patch set revision missing"));
+      pushCount =
+          metricMaker.newCounter(
+              "receivecommits/push_count",
+              new Description("number of pushes"),
+              Field.ofString("kind", (metadataBuilder, fieldValue) -> {})
+                  .description("The push kind (direct vs. magic).")
+                  .build(),
+              Field.ofString(
+                      "project",
+                      (metadataBuilder, fieldValue) -> metadataBuilder.projectName(fieldValue))
+                  .description("The name of the project for which the push is done.")
+                  .build(),
+              Field.ofString("type", (metadataBuilder, fieldValue) -> {})
+                  .description(
+                      "The type of the update (CREATE, UPDATE, CREATE/UPDATE,"
+                          + " UPDATE_NONFASTFORWARD, DELETE).")
+                  .build());
     }
   }
 
@@ -627,7 +647,7 @@
   ReceiveCommitsResult processCommands(
       Collection<ReceiveCommand> commands, MultiProgressMonitor progress) throws StorageException {
     checkState(!used, "Tried to re-use a ReceiveCommits objects that is single-use only");
-    long start = System.nanoTime();
+    long start = TimeUtil.nowNanos();
     parsePushOptions();
     String clientProvidedDeadlineValue =
         Iterables.getLast(pushOptions.get("deadline"), /* defaultValue= */ null);
@@ -637,10 +657,10 @@
                 tracePushOption.isPresent(),
                 tracePushOption.orElse(null),
                 (tagName, traceId) -> addMessage(tagName + ": " + traceId));
-        TraceTimer traceTimer =
-            newTimer("processCommands", Metadata.builder().resourceCount(commandCount));
         PerformanceLogContext performanceLogContext =
-            new PerformanceLogContext(config, performanceLoggers)) {
+            new PerformanceLogContext(config, performanceLoggers);
+        TraceTimer traceTimer =
+            newTimer("processCommands", Metadata.builder().resourceCount(commandCount))) {
       RequestInfo requestInfo =
           RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
               .project(project.getNameKey())
@@ -727,6 +747,13 @@
       return;
     }
 
+    if (!magicCommands.isEmpty()) {
+      metrics.pushCount.increment("magic", project.getName(), getUpdateType(magicCommands));
+    }
+    if (!regularCommands.isEmpty()) {
+      metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
+    }
+
     try {
       if (!regularCommands.isEmpty()) {
         handleRegularCommands(regularCommands, progress);
@@ -777,6 +804,15 @@
         lazy(() -> commands.stream().map(ReceiveCommits::commandToString).collect(joining(","))));
   }
 
+  private String getUpdateType(List<ReceiveCommand> commands) {
+    return commands.stream()
+        .map(ReceiveCommand::getType)
+        .map(ReceiveCommand.Type::name)
+        .distinct()
+        .sorted()
+        .collect(joining("/"));
+  }
+
   private void sendErrorMessages() {
     if (!errors.isEmpty()) {
       logger.atFine().log("Handling error conditions: %s", errors.keySet());
@@ -1726,6 +1762,7 @@
     }
 
     @UsedAt(UsedAt.Project.GOOGLE)
+    @SuppressWarnings("unused") // unused in upstream, but used at Google
     @Option(name = "--create-cod-token", usage = "create a token for consistency-on-demand")
     private boolean createCodToken;
 
@@ -2882,8 +2919,6 @@
      * </ul>
      *
      * @return whether the new commit is valid
-     * @throws IOException
-     * @throws PermissionBackendException
      */
     boolean validateNewPatchSet() throws IOException, PermissionBackendException {
       try (TraceTimer traceTimer = newTimer("validateNewPatchSet")) {
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index b2a31b9..a9ef70e 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -345,8 +345,11 @@
       update.putReviewer(ctx.getAccountId(), REVIEWER);
     }
 
-    mailMessage = insertChangeMessage(update, ctx, reviewMessage);
+    // Approvals that are being set in the new patch-set during this operation are not available yet
+    // outside of the scope of this method. Only copied approvals are set here.
+    approvalsUtil.byPatchSet(ctx.getNotes(), newPatchSet).forEach(a -> update.putCopiedApproval(a));
 
+    mailMessage = insertChangeMessage(update, ctx, reviewMessage);
     if (mergedByPushOp == null) {
       resetChange(ctx);
     } else {
diff --git a/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java b/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
index 432dda3..98f2aa2 100644
--- a/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
+++ b/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
@@ -76,8 +76,8 @@
     }
 
     /**
-     * @return a map from ref to commands covering all ref operations to be performed on this
-     *     repository as part of the ongoing submit operation.
+     * Returns a map from ref to commands covering all ref operations to be performed on this
+     * repository as part of the ongoing submit operation.
      */
     public ImmutableMap<String, ReceiveCommand> getCommands() {
       return commands;
diff --git a/java/com/google/gerrit/server/group/db/GroupDelta.java b/java/com/google/gerrit/server/group/db/GroupDelta.java
index 4ef2450..69cb936 100644
--- a/java/com/google/gerrit/server/group/db/GroupDelta.java
+++ b/java/com/google/gerrit/server/group/db/GroupDelta.java
@@ -121,19 +121,39 @@
   @AutoValue.Builder
   public abstract static class Builder {
 
-    /** @see #getName() */
+    /**
+     * Defines the new name of the group
+     *
+     * <p>See {@link #getName}.
+     */
     public abstract Builder setName(AccountGroup.NameKey name);
 
-    /** @see #getDescription() */
+    /**
+     * Defines the new description of the group
+     *
+     * <p>See {@link #getDescription()}}
+     */
     public abstract Builder setDescription(String description);
 
-    /** @see #getOwnerGroupUUID() */
+    /**
+     * Defines the new owner of the group
+     *
+     * <p>See {@link #getOwnerGroupUUID()}
+     */
     public abstract Builder setOwnerGroupUUID(AccountGroup.UUID ownerGroupUUID);
 
-    /** @see #getVisibleToAll() */
+    /**
+     * Defines the new state of the 'visibleToAll' flag of the group
+     *
+     * <p>See {@link #getVisibleToAll()}
+     */
     public abstract Builder setVisibleToAll(boolean visibleToAll);
 
-    /** @see #getMemberModification() */
+    /**
+     * Set {@link MemberModification} for the prospective {@link GroupDelta}
+     *
+     * <p>See {@link #getMemberModification()}
+     */
     public abstract Builder setMemberModification(MemberModification memberModification);
 
     /**
@@ -146,7 +166,11 @@
      */
     public abstract MemberModification getMemberModification();
 
-    /** @see #getSubgroupModification() */
+    /**
+     * Set {@link SubgroupModification} for the prospective {@link GroupDelta}
+     *
+     * <p>See {@link #getSubgroupModification()}
+     */
     public abstract Builder setSubgroupModification(SubgroupModification subgroupModification);
 
     /**
@@ -159,7 +183,12 @@
      */
     public abstract SubgroupModification getSubgroupModification();
 
-    /** @see #getUpdatedOn() */
+    /**
+     * Defines the {@code Timestamp} to be used for the NoteDb commits of the update. If not
+     * specified, the current {@code Timestamp} when creating the commit will be used.
+     *
+     * <p>See {@link #getUpdatedOn()}
+     */
     public abstract Builder setUpdatedOn(Timestamp timestamp);
 
     public abstract GroupDelta build();
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 01ee811..24bcaf0 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.FormatMethod;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupReference;
@@ -138,8 +139,7 @@
     Optional<Ref> maybeRef =
         refs.stream().filter(r -> r.getName().equals(RefNames.REFS_GROUPNAMES)).findFirst();
     if (!maybeRef.isPresent()) {
-      String msg = String.format("ref %s does not exist", RefNames.REFS_GROUPNAMES);
-      result.problems.add(error(msg));
+      result.problems.add(error("ref %s does not exist", RefNames.REFS_GROUPNAMES));
       return;
     }
     Ref ref = maybeRef.get();
@@ -280,6 +280,7 @@
     }
   }
 
+  @FormatMethod
   public static void logConsistencyProblemAsWarning(String fmt, Object... args) {
     logConsistencyProblem(warning(fmt, args));
   }
diff --git a/java/com/google/gerrit/server/group/db/InternalGroupCreation.java b/java/com/google/gerrit/server/group/db/InternalGroupCreation.java
index f4bf6e6..291c354 100644
--- a/java/com/google/gerrit/server/group/db/InternalGroupCreation.java
+++ b/java/com/google/gerrit/server/group/db/InternalGroupCreation.java
@@ -26,13 +26,13 @@
 @AutoValue
 public abstract class InternalGroupCreation {
 
-  /** Defines the numeric ID the group should have. */
+  /** Defines the numeric ID the group should have */
   public abstract AccountGroup.Id getId();
 
-  /** Defines the name the group should have. */
+  /** Defines the name the group should have */
   public abstract AccountGroup.NameKey getNameKey();
 
-  /** Defines the UUID the group should have. */
+  /** Defines the UUID the group should have */
   public abstract AccountGroup.UUID getGroupUUID();
 
   public static Builder builder() {
@@ -41,13 +41,13 @@
 
   @AutoValue.Builder
   public abstract static class Builder {
-    /** @see #getId() */
+    /** Defines the name the group should have */
     public abstract InternalGroupCreation.Builder setId(AccountGroup.Id id);
 
-    /** @see #getNameKey() */
+    /** Defines the name the group should have */
     public abstract InternalGroupCreation.Builder setNameKey(AccountGroup.NameKey name);
 
-    /** @see #getGroupUUID() */
+    /** Defines the UUID the group should have */
     public abstract InternalGroupCreation.Builder setGroupUUID(AccountGroup.UUID groupUuid);
 
     public abstract InternalGroupCreation build();
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index 6db00f5..e580f50 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.index.account.AccountIndexDefinition;
@@ -110,6 +111,7 @@
 
   @Override
   protected void configure() {
+    factory(MultiProgressMonitor.Factory.class);
 
     bind(AccountIndexRewriter.class);
     bind(AccountIndexCollection.class);
diff --git a/java/com/google/gerrit/server/index/VersionManager.java b/java/com/google/gerrit/server/index/VersionManager.java
index 56ce604..cdb69c6 100644
--- a/java/com/google/gerrit/server/index/VersionManager.java
+++ b/java/com/google/gerrit/server/index/VersionManager.java
@@ -107,7 +107,6 @@
    * @param name index name
    * @param force start re-index
    * @return true if started, otherwise false.
-   * @throws ReindexerAlreadyRunningException
    */
   public synchronized boolean startReindexer(String name, boolean force)
       throws ReindexerAlreadyRunningException {
@@ -125,7 +124,6 @@
    *
    * @param name index name
    * @return true if index was activated, otherwise false.
-   * @throws ReindexerAlreadyRunningException
    */
   public synchronized boolean activateLatestIndex(String name)
       throws ReindexerAlreadyRunningException {
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index ab90e32..1b51703 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -30,10 +30,10 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.index.SiteIndexer;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.OnlineReindexMode;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -63,7 +63,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final int PROJECT_SLICE_MAX_REFS = 1000;
 
-  private final ExperimentFeatures experimentFeatures;
+  private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
   private final ChangeData.Factory changeDataFactory;
   private final GitRepositoryManager repoManager;
   private final ListeningExecutorService executor;
@@ -73,14 +73,14 @@
 
   @Inject
   AllChangesIndexer(
-      ExperimentFeatures experimentFeatures,
+      MultiProgressMonitor.Factory multiProgressMonitorFactory,
       ChangeData.Factory changeDataFactory,
       GitRepositoryManager repoManager,
       @IndexExecutor(BATCH) ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
       ChangeNotes.Factory notesFactory,
       ProjectCache projectCache) {
-    this.experimentFeatures = experimentFeatures;
+    this.multiProgressMonitorFactory = multiProgressMonitorFactory;
     this.changeDataFactory = changeDataFactory;
     this.repoManager = repoManager;
     this.executor = executor;
@@ -185,7 +185,7 @@
   private SiteIndexer.Result indexAll(ChangeIndex index, List<ProjectSlice> projectSlices) {
     Stopwatch sw = Stopwatch.createStarted();
     MultiProgressMonitor mpm =
-        new MultiProgressMonitor(experimentFeatures, progressOut, "Reindexing changes");
+        multiProgressMonitorFactory.create(progressOut, TaskKind.INDEXING, "Reindexing changes");
     Task projTask = mpm.beginSubTask("project-slices", projectSlices.size());
     checkState(totalWork >= 0);
     Task doneTask = mpm.beginSubTask(null, totalWork);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 5caceef..bfe1ee1 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -342,6 +342,11 @@
       integer(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS)
           .buildRepeatable(ChangeField::getAttentionSetUserIds);
 
+  /** Number of changes that contain attention set. */
+  public static final FieldDef<ChangeData, Integer> ATTENTION_SET_USERS_COUNT =
+      intRange(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS_COUNT)
+          .build(cd -> additionsOnly(cd.attentionSet()).size());
+
   /**
    * The full attention set data including timestamp, reason and possible future fields.
    *
@@ -598,9 +603,9 @@
 
   /** List of labels on the current patch set including change owner votes. */
   public static final FieldDef<ChangeData, Iterable<String>> LABEL =
-      exact("label2").buildRepeatable(cd -> getLabels(cd, true));
+      exact("label2").buildRepeatable(cd -> getLabels(cd));
 
-  private static Iterable<String> getLabels(ChangeData cd, boolean owners) {
+  private static Iterable<String> getLabels(ChangeData cd) {
     Set<String> allApprovals = new HashSet<>();
     Set<String> distinctApprovals = new HashSet<>();
     for (PatchSetApproval a : cd.currentApprovals()) {
@@ -608,12 +613,19 @@
         allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
         Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
         allApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, a.accountId()));
-        if (owners && cd.change().getOwner().equals(a.accountId())) {
+        if (cd.change().getOwner().equals(a.accountId())) {
           allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
           allApprovals.addAll(
               getMaxMinAnyLabels(
                   a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
         }
+        if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
+          allApprovals.add(
+              formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+          allApprovals.addAll(
+              getMaxMinAnyLabels(
+                  a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+        }
         distinctApprovals.add(formatLabel(a.label(), a.value()));
         distinctApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, null));
       }
@@ -735,6 +747,8 @@
   private static String formatAccount(Account.Id accountId) {
     if (ChangeQueryBuilder.OWNER_ACCOUNT_ID.equals(accountId)) {
       return ChangeQueryBuilder.ARG_ID_OWNER;
+    } else if (ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID.equals(accountId)) {
+      return ChangeQueryBuilder.ARG_ID_NON_UPLOADER;
     }
     return Integer.toString(accountId.get());
   }
@@ -796,6 +810,12 @@
                 return m ? "1" : "0";
               });
 
+  /** Whether the change is a cherry pick of another change. */
+  public static final FieldDef<ChangeData, String> CHERRY_PICK =
+      exact(ChangeQueryBuilder.FIELD_CHERRYPICK)
+          .stored()
+          .build(cd -> cd.change().getCherryPickOf() != null ? "1" : "0");
+
   /** The number of inserted lines in this change. */
   public static final FieldDef<ChangeData, Integer> ADDED =
       intRange(ChangeQueryBuilder.FIELD_ADDED)
@@ -919,6 +939,19 @@
   public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
       SubmitRuleOptions.builder().build();
 
+  /** All submit rules results in the form of "$ruleName,$status". */
+  public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RULE_RESULT =
+      exact("submit_rule_result")
+          .buildRepeatable(
+              cd -> {
+                List<String> result = new ArrayList<>();
+                List<SubmitRecord> submitRecords = cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT);
+                for (SubmitRecord record : submitRecords) {
+                  result.add(record.ruleName + "=" + record.status.name());
+                }
+                return result;
+              });
+
   /**
    * JSON type for storing SubmitRecords.
    *
@@ -938,12 +971,14 @@
       @Deprecated Map<String, String> data;
     }
 
+    String ruleName;
     SubmitRecord.Status status;
     List<StoredLabel> labels;
     List<StoredRequirement> requirements;
     String errorMessage;
 
     public StoredSubmitRecord(SubmitRecord rec) {
+      this.ruleName = rec.ruleName;
       this.status = rec.status;
       this.errorMessage = rec.errorMessage;
       if (rec.labels != null) {
@@ -975,6 +1010,7 @@
 
     public SubmitRecord toSubmitRecord() {
       SubmitRecord rec = new SubmitRecord();
+      rec.ruleName = ruleName;
       rec.status = status;
       rec.errorMessage = errorMessage;
       if (labels != null) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index bee0c35..30ab6e6a 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -154,10 +154,34 @@
   @Deprecated static final Schema<ChangeData> V64 = schema(V63, false);
 
   /** Added new field for submit requirements. */
+  @Deprecated
   static final Schema<ChangeData> V65 =
       new Schema.Builder<ChangeData>().add(V64).add(ChangeField.STORED_SUBMIT_REQUIREMENTS).build();
 
   /**
+   * The computation of {@link ChangeField#LABEL} has changed: We added the non_uploader arg to the
+   * label field.
+   */
+  @Deprecated static final Schema<ChangeData> V66 = schema(V65, false);
+
+  /** Updated submit records: store the rule name that created the submit record. */
+  @Deprecated static final Schema<ChangeData> V67 = schema(V66, false);
+
+  /** Added new field {@link ChangeField#SUBMIT_RULE_RESULT}. */
+  @Deprecated
+  static final Schema<ChangeData> V68 =
+      new Schema.Builder<ChangeData>().add(V67).add(ChangeField.SUBMIT_RULE_RESULT).build();
+
+  /** Added new field {@link ChangeField#CHERRY_PICK}. */
+  @Deprecated
+  static final Schema<ChangeData> V69 =
+      new Schema.Builder<ChangeData>().add(V68).add(ChangeField.CHERRY_PICK).build();
+
+  /** Added new field {@link ChangeField#ATTENTION_SET_USERS_COUNT}. */
+  static final Schema<ChangeData> V70 =
+      new Schema.Builder<ChangeData>().add(V69).add(ChangeField.ATTENTION_SET_USERS_COUNT).build();
+
+  /**
    * Name of the change index to be used when contacting index backends or loading configurations.
    */
   public static final String NAME = "changes";
diff --git a/java/com/google/gerrit/server/ioutil/HostPlatform.java b/java/com/google/gerrit/server/ioutil/HostPlatform.java
index 39e9c07..e27d17c 100644
--- a/java/com/google/gerrit/server/ioutil/HostPlatform.java
+++ b/java/com/google/gerrit/server/ioutil/HostPlatform.java
@@ -21,7 +21,7 @@
   private static final boolean win32 = compute("windows");
   private static final boolean mac = compute("mac");
 
-  /** @return true if this JVM is running on a Windows platform. */
+  /** Returns true if this JVM is running on a Windows platform. */
   public static boolean isWin32() {
     return win32;
   }
diff --git a/java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java b/java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java
index 015887b..a58d9ae 100644
--- a/java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java
+++ b/java/com/google/gerrit/server/ioutil/LimitedByteArrayOutputStream.java
@@ -57,7 +57,7 @@
     buffer.write(b, off, len);
   }
 
-  /** @return a newly allocated byte array with contents of the buffer. */
+  /** Returns a newly allocated byte array with contents of the buffer. */
   public byte[] toByteArray() {
     return buffer.toByteArray();
   }
diff --git a/java/com/google/gerrit/server/logging/CallerFinder.java b/java/com/google/gerrit/server/logging/CallerFinder.java
index bd7e608..4cb4b7f 100644
--- a/java/com/google/gerrit/server/logging/CallerFinder.java
+++ b/java/com/google/gerrit/server/logging/CallerFinder.java
@@ -41,7 +41,7 @@
  *
  * <p>E.g. the stacktrace could look like this:
  *
- * <pre>
+ * <pre>{@code
  * GroupQueryProcessor(QueryProcessor<T>).query(List<String>, List<Predicate<T>>) line: 216
  * GroupQueryProcessor(QueryProcessor<T>).query(List<Predicate<T>>) line: 188
  * GroupQueryProcessor(QueryProcessor<T>).query(Predicate<T>) line: 171
@@ -52,7 +52,7 @@
  * GroupCacheImpl$ByNameLoader.load(Object) line: 1
  * LocalCache$LoadingValueReference<K,V>.loadFuture(K, CacheLoader<? super K,V>) line: 3527
  * ...
- * </pre>
+ * }</pre>
  *
  * <p>The first interesting caller is {@code GroupCacheImpl$ByNameLoader.load(String) line: 166}. To
  * find this caller from the stacktrace we could specify {@link
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
index 3c4c563..1bba018 100644
--- a/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareRunnable.java
@@ -28,24 +28,24 @@
  *
  * <p>Example:
  *
- * <pre>
- *   try (TraceContext traceContext = TraceContext.newTrace(true, ...)) {
- *     executor
- *         .submit(new LoggingContextAwareRunnable(
- *             () -> {
- *               // Tracing is enabled since the runnable is created within the TraceContext.
- *               // Tracing is even enabled if the executor runs the runnable only after the
- *               // TraceContext was closed.
+ * <pre>{@code
+ * try (TraceContext traceContext = TraceContext.newTrace(true, ...)) {
+ *   executor
+ *       .submit(new LoggingContextAwareRunnable(
+ *           () -> {
+ *             // Tracing is enabled since the runnable is created within the TraceContext.
+ *             // Tracing is even enabled if the executor runs the runnable only after the
+ *             // TraceContext was closed.
  *
- *               // The tag "foo=bar" is not set, since it was added to the logging context only
- *               // after this runnable was created.
+ *             // The tag "foo=bar" is not set, since it was added to the logging context only
+ *             // after this runnable was created.
  *
- *               // do stuff
- *             }))
- *         .get();
- *     traceContext.addTag("foo", "bar");
- *   }
- * </pre>
+ *             // do stuff
+ *           }))
+ *       .get();
+ *   traceContext.addTag("foo", "bar");
+ * }
+ * }</pre>
  *
  * @see LoggingContextAwareCallable
  */
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 681dfbc..487e0af 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -268,15 +268,23 @@
     return this;
   }
 
-  public boolean isTracing() {
+  public static boolean isTracing() {
     return LoggingContext.getInstance().isLoggingForced();
   }
 
-  public Optional<String> getTraceId() {
+  public static Optional<String> getTraceId() {
     return LoggingContext.getInstance().getTagsAsMap().get(RequestId.Type.TRACE_ID.name()).stream()
         .findFirst();
   }
 
+  public static Optional<String> getPluginTag() {
+    return getTag(PLUGIN_TAG);
+  }
+
+  public static Optional<String> getTag(String tagName) {
+    return LoggingContext.getInstance().getTagsAsMap().get(tagName).stream().findFirst();
+  }
+
   public TraceContext enableAclLogging() {
     if (stopAclLoggingOnClose) {
       return this;
@@ -286,11 +294,7 @@
     return this;
   }
 
-  public boolean isAclLoggingEnabled() {
-    return LoggingContext.getInstance().isAclLogging();
-  }
-
-  public ImmutableList<String> getAclLogRecords() {
+  public static ImmutableList<String> getAclLogRecords() {
     return LoggingContext.getInstance().getAclLogRecords();
   }
 
diff --git a/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 2ff5fc3..ead4c06 100644
--- a/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -56,10 +56,13 @@
   class ParsedToken {
     private final Account.Id accountId;
     private final String emailAddress;
+    private final AuthRequest.Factory authRequestFactory;
 
-    public ParsedToken(Account.Id accountId, String emailAddress) {
+    public ParsedToken(
+        Account.Id accountId, String emailAddress, AuthRequest.Factory authRequestFactory) {
       this.accountId = accountId;
       this.emailAddress = emailAddress;
+      this.authRequestFactory = authRequestFactory;
     }
 
     public Account.Id getAccountId() {
@@ -71,7 +74,7 @@
     }
 
     public AuthRequest toAuthRequest() {
-      return AuthRequest.forEmail(getEmailAddress());
+      return authRequestFactory.createForEmail(getEmailAddress());
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 77be665..bdfaf6d 100644
--- a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -19,6 +19,7 @@
 
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
 import com.google.inject.AbstractModule;
@@ -31,6 +32,7 @@
 @Singleton
 public class SignedTokenEmailTokenVerifier implements EmailTokenVerifier {
   private final SignedToken emailRegistrationToken;
+  private final AuthRequest.Factory authRequestFactory;
 
   public static class Module extends AbstractModule {
     @Override
@@ -40,8 +42,9 @@
   }
 
   @Inject
-  SignedTokenEmailTokenVerifier(AuthConfig config) {
+  SignedTokenEmailTokenVerifier(AuthConfig config, AuthRequest.Factory authRequestFactory) {
     emailRegistrationToken = config.getEmailRegistrationToken();
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -77,7 +80,7 @@
     }
     Account.Id id = Account.Id.tryParse(matcher.group(1)).orElseThrow(InvalidTokenException::new);
     String newEmail = matcher.group(2);
-    return new ParsedToken(id, newEmail);
+    return new ParsedToken(id, newEmail, authRequestFactory);
   }
 
   private void checkEmailRegistrationToken() {
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index d805e39..e98647d 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.receive;
 
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
@@ -91,7 +92,7 @@
   private static final ImmutableMap<MailComment.CommentType, CommentForValidation.CommentType>
       MAIL_COMMENT_TYPE_TO_VALIDATION_TYPE =
           ImmutableMap.of(
-              MailComment.CommentType.CHANGE_MESSAGE,
+              MailComment.CommentType.PATCHSET_LEVEL,
                   CommentForValidation.CommentType.CHANGE_MESSAGE,
               MailComment.CommentType.FILE_COMMENT, CommentForValidation.CommentType.FILE_COMMENT,
               MailComment.CommentType.INLINE_COMMENT,
@@ -342,9 +343,6 @@
           changeMessagesUtil.setChangeMessage(ctx.getUpdate(psId), generateChangeMessage(), tag);
       comments = new ArrayList<>();
       for (MailComment c : parsedComments) {
-        if (c.getType() == MailComment.CommentType.CHANGE_MESSAGE) {
-          continue;
-        }
         comments.add(
             persistentCommentFromMailComment(ctx, c, targetPatchSetForComment(ctx, c, patchSet)));
       }
@@ -359,7 +357,7 @@
     @Override
     public void postUpdate(PostUpdateContext ctx) throws Exception {
       String patchSetComment = null;
-      if (parsedComments.get(0).getType() == MailComment.CommentType.CHANGE_MESSAGE) {
+      if (parsedComments.get(0).getType() == MailComment.CommentType.PATCHSET_LEVEL) {
         patchSetComment = parsedComments.get(0).getMessage();
       }
       // Send email notifications
@@ -396,15 +394,7 @@
 
     private String generateChangeMessage() {
       String changeMsg = "Patch Set " + psId.get() + ":";
-      if (parsedComments.get(0).getType() == MailComment.CommentType.CHANGE_MESSAGE) {
-        // Add a blank line after Patch Set to follow the default format
-        if (parsedComments.size() > 1) {
-          changeMsg += "\n\n" + numComments(parsedComments.size() - 1);
-        }
-        changeMsg += "\n\n" + parsedComments.get(0).getMessage();
-      } else {
-        changeMsg += "\n\n" + numComments(parsedComments.size());
-      }
+      changeMsg += "\n\n" + numComments(parsedComments.size());
       return changeMsg;
     }
 
@@ -424,7 +414,11 @@
       // The patch set that this comment is based on is different if this
       // comment was sent in reply to a comment on a previous patch set.
       Side side;
-      if (mailComment.getInReplyTo() != null) {
+      if (mailComment.getType() == MailComment.CommentType.PATCHSET_LEVEL) {
+        fileName = PATCHSET_LEVEL;
+        // Patchset comments do not have side.
+        side = Side.REVISION;
+      } else if (mailComment.getInReplyTo() != null) {
         fileName = mailComment.getInReplyTo().key.filename;
         side = Side.fromShort(mailComment.getInReplyTo().side);
       } else {
diff --git a/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
index dc99b46..e383207 100644
--- a/java/com/google/gerrit/server/mail/receive/MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -110,8 +110,6 @@
    * requestDeletion will enqueue an email for deletion and delete it the next time we connect to
    * the email server. This does not guarantee deletion as the Gerrit instance might fail before we
    * connect to the email server.
-   *
-   * @param messageId
    */
   public void requestDeletion(String messageId) {
     pendingDeletion.add(messageId);
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 48ab397..1a2e150 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -41,11 +41,10 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.FilePathAdapter;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -66,6 +65,7 @@
 import org.apache.james.mime4j.dom.field.FieldName;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
@@ -269,17 +269,23 @@
 
       if (patchSet != null) {
         detail.append("---\n");
-        PatchList patchList = getPatchList();
-        for (PatchListEntry p : patchList.getPatches()) {
-          if (Patch.isMagic(p.getNewName())) {
+        Map<String, FileDiffOutput> modifiedFiles = listModifiedFiles();
+        for (FileDiffOutput fileDiff : modifiedFiles.values()) {
+          if (fileDiff.newPath().isPresent() && Patch.isMagic(fileDiff.newPath().get())) {
             continue;
           }
           detail
-              .append(p.getChangeType().getCode())
+              .append(fileDiff.changeType().getCode())
               .append(" ")
-              .append(p.getNewName())
+              .append(
+                  FilePathAdapter.getNewPath(
+                      fileDiff.oldPath(), fileDiff.newPath(), fileDiff.changeType()))
               .append("\n");
         }
+        Integer insertions =
+            modifiedFiles.values().stream().map(FileDiffOutput::insertions).reduce(0, Integer::sum);
+        Integer deletions =
+            modifiedFiles.values().stream().map(FileDiffOutput::deletions).reduce(0, Integer::sum);
         detail.append(
             MessageFormat.format(
                 "" //
@@ -287,9 +293,9 @@
                     + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
                     + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
                     + "\n",
-                patchList.getPatches().size() - 1, //
-                patchList.getInsertions(), //
-                patchList.getDeletions()));
+                modifiedFiles.size() - 1, //
+                insertions, //
+                deletions));
         detail.append("\n");
       }
       return detail.toString();
@@ -300,7 +306,8 @@
   }
 
   /** Get the patch list corresponding to patch set patchSetId of this change. */
-  protected PatchList getPatchList(int patchSetId) throws PatchListNotAvailableException {
+  protected Map<String, FileDiffOutput> listModifiedFiles(int patchSetId)
+      throws DiffNotAvailableException {
     PatchSet ps;
     if (patchSetId == patchSet.number()) {
       ps = patchSet;
@@ -308,18 +315,20 @@
       try {
         ps = args.patchSetUtil.get(changeData.notes(), PatchSet.id(change.getId(), patchSetId));
       } catch (StorageException e) {
-        throw new PatchListNotAvailableException("Failed to get patchSet", e);
+        throw new DiffNotAvailableException("Failed to get patchSet", e);
       }
     }
-    return args.patchListCache.get(change, ps);
+    return args.diffOperations.listModifiedFilesAgainstParent(
+        change.getProject(), ps.commitId(), /* parentNum= */ 0);
   }
 
   /** Get the patch list corresponding to this patch set. */
-  protected PatchList getPatchList() throws PatchListNotAvailableException {
+  protected Map<String, FileDiffOutput> listModifiedFiles() throws DiffNotAvailableException {
     if (patchSet != null) {
-      return args.patchListCache.get(change, patchSet);
+      return args.diffOperations.listModifiedFilesAgainstParent(
+          change.getProject(), patchSet.commitId(), /* parentNum= */ 0);
     }
-    throw new PatchListNotAvailableException("no patchSet specified");
+    throw new DiffNotAvailableException("no patchSet specified");
   }
 
   /** Get the project entity the change is in; null if its been deleted. */
@@ -566,18 +575,15 @@
 
   /** Show patch set as unified difference. */
   public String getUnifiedDiff() {
-    PatchList patchList;
+    Map<String, FileDiffOutput> modifiedFiles;
     try {
-      patchList = getPatchList();
-      if (patchList.getOldId() == null) {
+      modifiedFiles = listModifiedFiles();
+      if (modifiedFiles.isEmpty()) {
         // Octopus merges are not well supported for diff output by Gerrit.
         // Currently these always have a null oldId in the PatchList.
         return "[Octopus merge; cannot be formatted as a diff.]\n";
       }
-    } catch (PatchListObjectTooLargeException e) {
-      logger.atWarning().log("Cannot format patch %s", e.getMessage());
-      return "";
-    } catch (PatchListNotAvailableException e) {
+    } catch (DiffNotAvailableException e) {
       logger.atSevere().withCause(e).log("Cannot format patch");
       return "";
     }
@@ -587,9 +593,11 @@
     try (DiffFormatter fmt = new DiffFormatter(buf)) {
       try (Repository git = args.server.openRepository(change.getProject())) {
         try {
+          ObjectId oldId = modifiedFiles.values().iterator().next().oldCommitId();
+          ObjectId newId = modifiedFiles.values().iterator().next().newCommitId();
           fmt.setRepository(git);
           fmt.setDetectRenames(true);
-          fmt.format(patchList.getOldId(), patchList.getNewId());
+          fmt.format(oldId, newId);
           return RawParseUtils.decode(buf.toByteArray());
         } catch (IOException e) {
           if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index ac6c2f3..5a7352a 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail.send;
 
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
@@ -36,10 +37,9 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.receive.Protocol;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.patch.PatchFile;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -72,23 +72,23 @@
     public PatchFile fileData;
     public List<Comment> comments = new ArrayList<>();
 
-    /** @return a web link to a comment for a change. */
+    /** Returns a web link to a comment for a change. */
     public String getCommentLink(String uuid) {
       return args.urlFormatter.get().getInlineCommentView(change, uuid).orElse(null);
     }
 
-    /** @return a web link to the comment tab view of a change. */
+    /** Returns a web link to the comment tab view of a change. */
     public String getCommentsTabLink() {
       return args.urlFormatter.get().getCommentsTabView(change).orElse(null);
     }
 
-    /** @return a web link to the findings tab view of a change. */
+    /** Returns a web link to the findings tab view of a change. */
     public String getFindingsTabLink() {
       return args.urlFormatter.get().getFindingsTabView(change).orElse(null);
     }
 
     /**
-     * @return A title for the group, i.e. "Commit Message", "Merge List", or "File [[filename]]".
+     * Returns a title for the group, i.e. "Commit Message", "Merge List", or "File [[filename]]".
      */
     public String getTitle() {
       if (Patch.COMMIT_MSG.equals(filename)) {
@@ -181,8 +181,8 @@
   }
 
   /**
-   * @return a list of FileCommentGroup objects representing the inline comments grouped by the
-   *     file.
+   * Returns a list of FileCommentGroup objects representing the inline comments grouped by the
+   * file.
    */
   private List<CommentSender.FileCommentGroup> getGroupedInlineComments(Repository repo) {
     List<CommentSender.FileCommentGroup> groups = new ArrayList<>();
@@ -198,30 +198,30 @@
         currentGroup = new FileCommentGroup();
         currentGroup.filename = c.key.filename;
         currentGroup.patchSetId = c.key.patchSetId;
-        // Get the patch list:
-        PatchList patchList = null;
+        // Get the modified files:
+        Map<String, FileDiffOutput> modifiedFiles = null;
         try {
-          patchList = getPatchList(c.key.patchSetId);
-        } catch (PatchListObjectTooLargeException e) {
-          logger.atWarning().log("Failed to get patch list: %s", e.getMessage());
-        } catch (PatchListNotAvailableException e) {
-          logger.atSevere().withCause(e).log("Failed to get patch list");
+          modifiedFiles = listModifiedFiles(c.key.patchSetId);
+        } catch (DiffNotAvailableException e) {
+          logger.atSevere().withCause(e).log("Failed to get modified files");
         }
 
         groups.add(currentGroup);
-        if (patchList != null) {
+        if (modifiedFiles != null && !modifiedFiles.isEmpty()) {
           try {
-            currentGroup.fileData = new PatchFile(repo, patchList, c.key.filename);
+            currentGroup.fileData = new PatchFile(repo, modifiedFiles, c.key.filename);
           } catch (IOException e) {
             logger.atWarning().withCause(e).log(
                 "Cannot load %s from %s in %s",
-                c.key.filename, patchList.getNewId().name(), projectState.getName());
+                c.key.filename,
+                modifiedFiles.values().iterator().next().newCommitId().name(),
+                projectState.getName());
             currentGroup.fileData = null;
           }
         }
       }
 
-      if (currentGroup.fileData != null) {
+      if (currentGroup.filename.equals(PATCHSET_LEVEL) || currentGroup.fileData != null) {
         currentGroup.comments.add(c);
       }
     }
@@ -268,7 +268,7 @@
   }
 
   /**
-   * @return the lines of file content in fileData that are encompassed by range on the given side.
+   * Returns the lines of file content in fileData that are encompassed by range on the given side.
    */
   private List<String> getLinesByRange(Comment.Range range, PatchFile fileData, short side) {
     List<String> lines = new ArrayList<>();
@@ -331,9 +331,9 @@
   }
 
   /**
-   * @return a shortened version of the given comment's message. Will be shortened to 100 characters
-   *     or the first line, or following the last period within the first 100 characters, whichever
-   *     is shorter. If the message is shortened, an ellipsis is appended.
+   * Returns a shortened version of the given comment's message. Will be shortened to 100 characters
+   * or the first line, or following the last period within the first 100 characters, whichever is
+   * shorter. If the message is shortened, an ellipsis is appended.
    */
   protected static String getShortenedCommentMessage(String message) {
     int threshold = 100;
@@ -369,8 +369,8 @@
   }
 
   /**
-   * @return grouped inline comment data mapped to data structures that are suitable for passing
-   *     into Soy.
+   * Returns grouped inline comment data mapped to data structures that are suitable for passing
+   * into Soy.
    */
   private List<Map<String, Object>> getCommentGroupsTemplateData(Repository repo) {
     List<Map<String, Object>> commentGroups = new ArrayList<>();
@@ -383,7 +383,9 @@
       List<Map<String, Object>> commentsList = new ArrayList<>();
       for (Comment comment : group.comments) {
         Map<String, Object> commentData = new HashMap<>();
-        commentData.put("lines", getLinesOfComment(comment, group.fileData));
+        if (group.fileData != null) {
+          commentData.put("lines", getLinesOfComment(comment, group.fileData));
+        }
         commentData.put("message", comment.message.trim());
         List<CommentFormatter.Block> blocks = CommentFormatter.parse(comment.message);
         commentData.put("messageBlocks", commentBlocksToSoyData(blocks));
diff --git a/java/com/google/gerrit/server/mail/send/EmailArguments.java b/java/com/google/gerrit/server/mail/send/EmailArguments.java
index 258c9af..96effc1 100644
--- a/java/com/google/gerrit/server/mail/send/EmailArguments.java
+++ b/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -35,7 +35,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.DiffOperations;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
+import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -71,7 +72,7 @@
   final PermissionBackend permissionBackend;
   final GroupBackend groupBackend;
   final AccountCache accountCache;
-  final PatchListCache patchListCache;
+  final DiffOperations diffOperations;
   final PatchSetUtil patchSetUtil;
   final ApprovalsUtil approvalsUtil;
   final Provider<FromAddressGenerator> fromAddressGenerator;
@@ -86,7 +87,6 @@
   final AllProjectsName allProjectsName;
   final List<String> sshAddresses;
   final SitePaths site;
-
   final Provider<ChangeQueryBuilder> queryBuilder;
   final ChangeData.Factory changeDataFactory;
   final Provider<SoySauce> soySauce;
@@ -97,6 +97,7 @@
   final boolean addInstanceNameInSubject;
   final Provider<String> instanceNameProvider;
   final Provider<CurrentUser> currentUserProvider;
+  final RetryHelper retryHelper;
 
   @Inject
   EmailArguments(
@@ -105,7 +106,7 @@
       PermissionBackend permissionBackend,
       GroupBackend groupBackend,
       AccountCache accountCache,
-      PatchListCache patchListCache,
+      DiffOperations diffOperations,
       PatchSetUtil patchSetUtil,
       ApprovalsUtil approvalsUtil,
       Provider<FromAddressGenerator> fromAddressGenerator,
@@ -129,13 +130,14 @@
       OutgoingEmailValidator validator,
       @GerritInstanceName Provider<String> instanceNameProvider,
       @GerritServerConfig Config cfg,
-      Provider<CurrentUser> currentUserProvider) {
+      Provider<CurrentUser> currentUserProvider,
+      RetryHelper retryHelper) {
     this.server = server;
     this.projectCache = projectCache;
     this.permissionBackend = permissionBackend;
     this.groupBackend = groupBackend;
     this.accountCache = accountCache;
-    this.patchListCache = patchListCache;
+    this.diffOperations = diffOperations;
     this.patchSetUtil = patchSetUtil;
     this.approvalsUtil = approvalsUtil;
     this.fromAddressGenerator = fromAddressGenerator;
@@ -158,8 +160,8 @@
     this.accountQueryProvider = accountQueryProvider;
     this.validator = validator;
     this.instanceNameProvider = instanceNameProvider;
-
     this.addInstanceNameInSubject = cfg.getBoolean("sendemail", "addInstanceNameInSubject", false);
     this.currentUserProvider = currentUserProvider;
+    this.retryHelper = retryHelper;
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
index aa683f6..b32c43a 100644
--- a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
@@ -58,8 +58,6 @@
   /**
    * Create a {@link MessageId} as a result of a change update.
    *
-   * @param repoView
-   * @param patchsetId
    * @return MessageId that depends on the patchset.
    */
   public MessageId fromChangeUpdate(RepoView repoView, PatchSet.Id patchsetId) {
@@ -89,8 +87,9 @@
   }
 
   /**
-   * @param accountId Create a {@link MessageId} as a result of an account update.
-   * @return MessageId that depends on the account id.
+   * Create a {@link MessageId} as a result of an account update
+   *
+   * @return {@link MessageId} that depends on the account id.
    */
   public MessageId fromAccountUpdate(Account.Id accountId) {
     String userRef = RefNames.refsUsers(accountId);
@@ -113,8 +112,6 @@
    * Create a {@link MessageId} from a reason, Account.Id, and timestamp.
    *
    * @param reason for performing this account update
-   * @param accountId
-   * @param timestamp
    * @return MessageId that depends on the reason, accountId, and timestamp.
    */
   public MessageId fromReasonAccountIdAndTimestamp(
diff --git a/java/com/google/gerrit/server/mail/send/ModifyReviewerSender.java b/java/com/google/gerrit/server/mail/send/ModifyReviewerSender.java
index dcf3b6c..b187f9c 100644
--- a/java/com/google/gerrit/server/mail/send/ModifyReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/ModifyReviewerSender.java
@@ -37,5 +37,6 @@
     super.init();
 
     ccExistingReviewers();
+    removeUsersThatIgnoredTheChange();
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index ddcc0cf..8824cbd 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -34,6 +35,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.RetryableAction.ActionType;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.template.soy.jbcsrc.api.SoySauce;
@@ -93,12 +95,27 @@
     this.messageId = messageId;
   }
 
-  /**
-   * Format and enqueue the message for delivery.
-   *
-   * @throws EmailException
-   */
+  /** Format and enqueue the message for delivery. */
   public void send() throws EmailException {
+    try {
+      args.retryHelper
+          .action(
+              ActionType.SEND_EMAIL,
+              "sendEmail",
+              () -> {
+                sendImpl();
+                return null;
+              })
+          .retryWithTrace(Exception.class::isInstance)
+          .call();
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      Throwables.throwIfInstanceOf(e, EmailException.class);
+      throw new EmailException("sending email failed", e);
+    }
+  }
+
+  private void sendImpl() throws EmailException {
     if (!args.emailSender.isEnabled()) {
       // Server has explicitly disabled email sending.
       //
@@ -164,7 +181,8 @@
             // drop them from the recipient lists, but only if the user is not being impersonated.
             //
             logger.atFine().log(
-                "Not CCing email sender %s because the email strategy of this user is not %s but %s",
+                "Not CCing email sender %s because the email strategy of this user is not %s but"
+                    + " %s",
                 fromUser.get().account().id(),
                 CC_ON_OWN_COMMENTS,
                 senderPrefs != null ? senderPrefs.getEmailStrategy() : null);
@@ -522,9 +540,9 @@
   }
 
   /**
+   * Returns whether this email is visible to the given account
+   *
    * @param to account.
-   * @throws PermissionBackendException
-   * @return whether this email is visible to the given account.
    */
   protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
     return true;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index a7c7757..d71f9ff 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -134,7 +134,7 @@
     return changeId;
   }
 
-  /** @return revision of the metadata that was loaded. */
+  /** Returns revision of the metadata that was loaded. */
   public ObjectId getRevision() {
     return revision;
   }
@@ -210,12 +210,12 @@
   protected abstract void loadDefaults();
 
   /**
-   * @return the NameKey for the project where the notes should be stored, which is not necessarily
-   *     the same as the change's project.
+   * Returns the NameKey for the project where the notes should be stored, which is not necessarily
+   * the same as the change's project.
    */
   public abstract Project.NameKey getProjectName();
 
-  /** @return name of the reference storing this configuration. */
+  /** Returns name of the reference storing this configuration. */
   protected abstract String getRefName();
 
   /** Set up the metadata, parsing any state from the loaded revision. */
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 8e6606e..6677490 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -122,12 +122,11 @@
   }
 
   /**
-   * @return notes for the state of this change prior to this update. If this update is part of a
-   *     series managed by a {@link NoteDbUpdateManager}, then this reflects the state prior to the
-   *     first update in the series. A null return value can only happen when the change is being
-   *     rebuilt from NoteDb. A change that is in the process of being created will result in a
-   *     non-null return value from this method, but a null return value from {@link
-   *     ChangeNotes#getRevision()}.
+   * Returns notes for the state of this change prior to this update. If this update is part of a
+   * series managed by a {@link NoteDbUpdateManager}, then this reflects the state prior to the
+   * first update in the series. A null return value can only happen when the change is being
+   * rebuilt from NoteDb. A change that is in the process of being created will result in a non-null
+   * return value from this method, but a null return value from {@link ChangeNotes#getRevision()}.
    */
   @Nullable
   public ChangeNotes getNotes() {
@@ -173,8 +172,8 @@
   }
 
   /**
-   * @return the NameKey for the project where the update will be stored, which is not necessarily
-   *     the same as the change's project.
+   * Returns the NameKey for the project where the update will be stored, which is not necessarily
+   * the same as the change's project.
    */
   protected abstract Project.NameKey getProjectName();
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index afd8316..28ab711 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -40,6 +40,7 @@
   static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
   static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
   static final FooterKey FOOTER_LABEL = new FooterKey("Label");
+  static final FooterKey FOOTER_COPIED_LABEL = new FooterKey("Copied-Label");
   static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
   static final FooterKey FOOTER_PATCH_SET_DESCRIPTION = new FooterKey("Patch-set-description");
   static final FooterKey FOOTER_PRIVATE = new FooterKey("Private");
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 6684493..2d9b014 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.entities.RefNames.changeMetaRef;
 import static java.util.Comparator.comparing;
@@ -36,6 +37,7 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import com.google.common.flogger.FluentLogger;
+import com.google.errorprone.annotations.FormatMethod;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AttentionSetUpdate;
@@ -90,6 +92,7 @@
   public static final Ordering<ChangeMessage> MESSAGE_BY_TIME =
       Ordering.from(comparing(ChangeMessage::getWrittenOn));
 
+  @FormatMethod
   public static ConfigInvalidException parseException(
       Change.Id changeId, String fmt, Object... args) {
     return new ConfigInvalidException("Change " + changeId + ": " + String.format(fmt, args));
@@ -336,6 +339,7 @@
   // ChangeNotesCache from handlers.
   private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
+  private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvalsWithCopied;
   private ImmutableSet<Comment.Key> commentKeys;
 
   public ChangeNotes(
@@ -373,28 +377,49 @@
     return patchSets;
   }
 
+  /**
+   * Gets the approvals, not including the copied approvals. To get copied approvals as well, use
+   * {@link #getApprovalsWithCopied}, or use {@code ApprovalInference}.
+   */
   public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() {
     if (approvals == null) {
-      approvals = ImmutableListMultimap.copyOf(state.approvals());
+      approvals =
+          state.approvals().stream()
+              .filter(e -> !e.getValue().copied())
+              .collect(toImmutableListMultimap(e -> e.getKey(), e -> e.getValue()));
     }
     return approvals;
   }
 
+  /**
+   * This method is currently used only in tests. TODO(paiking): Use this method to fetch approvals
+   * (including copied approvals) instead of computing copied approvals on demand. This will be used
+   * by {@code ApprovalCache}.
+   *
+   * @return all approvals, including copied approvals.
+   */
+  public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovalsWithCopied() {
+    if (approvalsWithCopied == null) {
+      approvalsWithCopied = ImmutableListMultimap.copyOf(state.approvals());
+    }
+    return approvalsWithCopied;
+  }
+
   public ReviewerSet getReviewers() {
     return state.reviewers();
   }
 
-  /** @return reviewers that do not currently have a Gerrit account and were added by email. */
+  /** Returns reviewers that do not currently have a Gerrit account and were added by email. */
   public ReviewerByEmailSet getReviewersByEmail() {
     return state.reviewersByEmail();
   }
 
-  /** @return reviewers that were modified during this change's current WIP phase. */
+  /** Returns reviewers that were modified during this change's current WIP phase. */
   public ReviewerSet getPendingReviewers() {
     return state.pendingReviewers();
   }
 
-  /** @return reviewers by email that were modified during this change's current WIP phase. */
+  /** Returns reviewers by email that were modified during this change's current WIP phase. */
   public ReviewerByEmailSet getPendingReviewersByEmail() {
     return state.pendingReviewersByEmail();
   }
@@ -424,8 +449,8 @@
   }
 
   /**
-   * @return an ImmutableSet of Account.Ids of all users that have been assigned to this change. The
-   *     order of the set is the order in which they were assigned.
+   * Returns an ImmutableSet of Account.Ids of all users that have been assigned to this change. The
+   * order of the set is the order in which they were assigned.
    */
   public ImmutableSet<Account.Id> getPastAssignees() {
     return Lists.reverse(state.assigneeUpdates()).stream()
@@ -436,37 +461,37 @@
   }
 
   /**
-   * @return an ImmutableList of AssigneeStatusUpdate of all the updates to the assignee field to
-   *     this change. The order of the list is from most recent updates to least recent.
+   * Returns an ImmutableList of AssigneeStatusUpdate of all the updates to the assignee field to
+   * this change. The order of the list is from most recent updates to least recent.
    */
   public ImmutableList<AssigneeStatusUpdate> getAssigneeUpdates() {
     return state.assigneeUpdates();
   }
 
-  /** @return a ImmutableSet of all hashtags for this change sorted in alphabetical order. */
+  /** Returns an ImmutableSet of all hashtags for this change sorted in alphabetical order. */
   public ImmutableSet<String> getHashtags() {
     return ImmutableSortedSet.copyOf(state.hashtags());
   }
 
-  /** @return a list of all users who have ever been a reviewer on this change. */
+  /** Returns a list of all users who have ever been a reviewer on this change. */
   public ImmutableList<Account.Id> getAllPastReviewers() {
     return state.allPastReviewers();
   }
 
   /**
-   * @return submit records stored during the most recent submit; only for changes that were
-   *     actually submitted.
+   * Returns submit records stored during the most recent submit; only for changes that were
+   * actually submitted.
    */
   public ImmutableList<SubmitRecord> getSubmitRecords() {
     return state.submitRecords();
   }
 
-  /** @return all change messages, in chronological order, oldest first. */
+  /** Returns all change messages, in chronological order, oldest first. */
   public ImmutableList<ChangeMessage> getChangeMessages() {
     return state.changeMessages();
   }
 
-  /** @return inline comments on each revision. */
+  /** Returns inline comments on each revision. */
   public ImmutableListMultimap<ObjectId, HumanComment> getHumanComments() {
     return state.publishedComments();
   }
@@ -486,7 +511,7 @@
     return state.updateCount();
   }
 
-  /** @return {@link Optional} value of time when the change was merged. */
+  /** Returns {@link Optional} value of time when the change was merged. */
   public Optional<Timestamp> getMergedOn() {
     return Optional.ofNullable(state.mergedOn());
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 87ad646..5cf3a64 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COPIED_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
@@ -55,6 +56,7 @@
 import com.google.common.collect.Tables;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
+import com.google.errorprone.annotations.FormatMethod;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
@@ -413,6 +415,9 @@
     for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
       parseApproval(psId, accountId, realAccountId, commitTimestamp, line);
     }
+    for (String line : commit.getFooterLineValues(FOOTER_COPIED_LABEL)) {
+      parseCopiedApproval(psId, commitTimestamp, line);
+    }
 
     for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
       for (String line : commit.getFooterLineValues(state.getFooterKey())) {
@@ -796,6 +801,69 @@
     }
   }
 
+  // Footer example: Copied-Label: <LABEL>=VOTE <Gerrit Account>,<Gerrit Real Account> :"<TAG>"
+  // ":<"TAG>"" is optional. <Gerrit Real Account> is also optional, if it was not set.
+  // The label, vote, and the Gerrit account are mandatory (unlike FOOTER_LABEL where Gerrit
+  // Account is also optional since by default it's the committer).
+  private void parseCopiedApproval(PatchSet.Id psId, Timestamp ts, String line)
+      throws ConfigInvalidException {
+    // Copied approvals can't be explicitly removed. They are removed the same way as non-copied
+    // approvals.
+    checkFooter(!line.startsWith("-"), FOOTER_COPIED_LABEL, line);
+
+    Account.Id accountId, realAccountId = null;
+    String labelVoteStr;
+    String tag = null;
+    int s = line.indexOf(' ');
+    int tagStart = line.indexOf(":\"");
+
+    // The first account is the accountId, and second (if applicable) is the realAccountId.
+    try {
+      labelVoteStr = line.substring(0, s);
+    } catch (StringIndexOutOfBoundsException ex) {
+      throw new ConfigInvalidException(ex.getMessage(), ex);
+    }
+    String[] identities =
+        line.substring(s + 1, tagStart == -1 ? line.length() : tagStart).split(",");
+    PersonIdent ident = RawParseUtils.parsePersonIdent(identities[0]);
+    checkFooter(ident != null, FOOTER_COPIED_LABEL, line);
+    accountId = parseIdent(ident);
+
+    if (identities.length > 1) {
+      PersonIdent realIdent = RawParseUtils.parsePersonIdent(identities[1]);
+      checkFooter(realIdent != null, FOOTER_COPIED_LABEL, line);
+      realAccountId = parseIdent(realIdent);
+    }
+
+    LabelVote l;
+    try {
+      l = LabelVote.parseWithEquals(labelVoteStr);
+    } catch (IllegalArgumentException e) {
+      ConfigInvalidException pe = parseException("invalid %s: %s", FOOTER_COPIED_LABEL, line);
+      pe.initCause(e);
+      throw pe;
+    }
+
+    if (tagStart != -1) {
+      // tagStart+2 skips ":\"" to parse the actual tag. Tags are in brackets.
+      // line.length()-1 skips the last ".
+      tag = line.substring(tagStart + 2, line.length() - 1);
+    }
+
+    PatchSetApproval.Builder psa =
+        PatchSetApproval.builder()
+            .key(PatchSetApproval.key(psId, accountId, LabelId.create(l.label())))
+            .value(l.value())
+            .granted(ts)
+            .tag(Optional.ofNullable(tag))
+            .copied(true);
+    if (realAccountId != null) {
+      psa.realAccountId(realAccountId);
+    }
+    approvals.putIfAbsent(psa.key(), psa);
+    bufferedApprovals.add(psa);
+  }
+
   private void parseApproval(
       PatchSet.Id psId, Account.Id accountId, Account.Id realAccountId, Timestamp ts, String line)
       throws ConfigInvalidException {
@@ -917,6 +985,11 @@
         }
       } else {
         checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
+        if (line.startsWith("Rule-Name: ")) {
+          String ruleName = line.split(": ")[1];
+          rec.ruleName = ruleName;
+          continue;
+        }
         SubmitRecord.Label label = new SubmitRecord.Label();
         if (rec.labels == null) {
           rec.labels = new ArrayList<>();
@@ -1167,6 +1240,7 @@
     }
     if (!missing.isEmpty()) {
       throw parseException(
+          "%s",
           "Missing footers: " + missing.stream().map(FooterKey::getName).collect(joining(", ")));
     }
   }
@@ -1200,6 +1274,7 @@
     return pending != null && pending.commitId().isPresent();
   }
 
+  @FormatMethod
   private ConfigInvalidException parseException(String fmt, Object... args) {
     return ChangeNotes.parseException(id, fmt, args);
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 971e0a8..5acea1b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COPIED_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
@@ -52,6 +53,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Table;
+import com.google.common.collect.Table.Cell;
 import com.google.common.collect.TreeBasedTable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Address;
@@ -61,6 +63,7 @@
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmissionId;
@@ -128,6 +131,7 @@
   private final ServiceUserClassifier serviceUserClassifier;
 
   private final Table<String, Account.Id, Optional<Short>> approvals;
+  private final List<PatchSetApproval> copiedApprovals = new ArrayList<>();
   private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
   private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>();
   private final List<HumanComment> comments = new ArrayList<>();
@@ -273,6 +277,15 @@
     approvals.put(label, reviewer, Optional.empty());
   }
 
+  /**
+   * We expect the {@code copied} flag of {@code copiedPatchSetApproval} to be set, since this
+   * method is only meant for copied approvals.
+   */
+  public void putCopiedApproval(PatchSetApproval copiedPatchSetApproval) {
+    checkArgument(copiedPatchSetApproval.copied(), "Approval that should be copied is not copied.");
+    copiedApprovals.add(copiedPatchSetApproval);
+  }
+
   public void merge(SubmissionId submissionId, Iterable<SubmitRecord> submitRecords) {
     this.status = Change.Status.MERGED;
     this.submissionId = submissionId.toString();
@@ -492,7 +505,7 @@
     this.cherryPickOf = Optional.empty();
   }
 
-  /** @return the tree id for the updated tree */
+  /** Returns the tree id for the updated tree */
   private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
       throws ConfigInvalidException, IOException {
     if (submitRequirementResults.isEmpty() && comments.isEmpty() && pushCert == null) {
@@ -705,18 +718,10 @@
     }
 
     for (Table.Cell<String, Account.Id, Optional<Short>> c : approvals.cellSet()) {
-      addFooter(msg, FOOTER_LABEL);
-      // Label names/values are safe to append without sanitizing.
-      if (!c.getValue().isPresent()) {
-        msg.append('-').append(c.getRowKey());
-      } else {
-        msg.append(LabelVote.create(c.getRowKey(), c.getValue().get()).formatWithEquals());
-      }
-      Account.Id id = c.getColumnKey();
-      if (!id.equals(getAccountId())) {
-        noteUtil.appendAccountIdIdentString(msg.append(' '), id);
-      }
-      msg.append('\n');
+      addLabelFooter(msg, c);
+    }
+    for (PatchSetApproval patchSetApproval : copiedApprovals) {
+      addCopiedLabelFooter(msg, patchSetApproval);
     }
 
     if (submissionId != null) {
@@ -730,7 +735,10 @@
           msg.append(' ').append(sanitizeFooter(rec.errorMessage));
         }
         msg.append('\n');
-
+        if (rec.ruleName != null) {
+          addFooter(msg, FOOTER_SUBMITTED_WITH).append("Rule-Name: ").append(rec.ruleName);
+          msg.append('\n');
+        }
         if (rec.labels != null) {
           for (SubmitRecord.Label label : rec.labels) {
             // Label names/values are safe to append without sanitizing.
@@ -745,7 +753,6 @@
             msg.append('\n');
           }
         }
-        // TODO(maximeg) We might want to list plugins that validated this submission.
       }
     }
 
@@ -795,6 +802,47 @@
     return cb;
   }
 
+  private void addLabelFooter(StringBuilder msg, Cell<String, Account.Id, Optional<Short>> c) {
+    addFooter(msg, FOOTER_LABEL);
+    // Label names/values are safe to append without sanitizing.
+    if (!c.getValue().isPresent()) {
+      msg.append('-').append(c.getRowKey());
+    } else {
+      msg.append(LabelVote.create(c.getRowKey(), c.getValue().get()).formatWithEquals());
+    }
+    Account.Id id = c.getColumnKey();
+    if (!id.equals(getAccountId())) {
+      noteUtil.appendAccountIdIdentString(msg.append(' '), id);
+    }
+    msg.append('\n');
+  }
+
+  private void addCopiedLabelFooter(StringBuilder msg, PatchSetApproval patchSetApproval) {
+    if (patchSetApproval.value() == 0) {
+      // Can only happen if we removed a vote. There is no need to persist removed votes.
+      return;
+    }
+    addFooter(msg, FOOTER_COPIED_LABEL);
+    // Label names/values are safe to append without sanitizing.
+    msg.append(
+        LabelVote.create(patchSetApproval.label(), patchSetApproval.value()).formatWithEquals());
+    Account.Id id = patchSetApproval.accountId();
+    noteUtil.appendAccountIdIdentString(msg.append(' '), id);
+
+    // In the non-copied labels, we don't need to pass the real account id since it's already
+    // in FOOTER_REAL_USER. Here, we want to retain the original real account id.
+    if (patchSetApproval.realAccountId() != null) {
+      noteUtil.appendAccountIdIdentString(msg.append(","), patchSetApproval.realAccountId());
+    }
+
+    // In the non-copied labels, we don't need to pass the tag since it's already in
+    // FOOTER_TAG, but in this chase we want to retain the original tag, and not the current tag.
+    if (patchSetApproval.tag().isPresent()) {
+      msg.append(":\"" + sanitizeFooter(patchSetApproval.tag().get()) + "\"");
+    }
+    msg.append('\n');
+  }
+
   private void clearAttentionSet(String reason) {
     if (getNotes().getAttentionSet() == null) {
       return;
@@ -990,6 +1038,7 @@
   public boolean isEmpty() {
     return commitSubject == null
         && approvals.isEmpty()
+        && copiedApprovals.isEmpty()
         && changeMessage == null
         && comments.isEmpty()
         && reviewers.isEmpty()
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 549bd0f..7d743dc 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -13,16 +13,17 @@
 // limitations under the License.
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ATTENTION;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
 import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_PATTERN;
 import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_REGEX;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -38,7 +39,6 @@
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -128,7 +128,7 @@
      * Refs that were fixed by the run/ would be fixed if in --dry-run, together with their commit
      * history diff. Diff is empty if --output-diff is false.
      */
-    public Map<String, List<String>> fixedRefDiff = new HashMap<>();
+    public Map<String, List<CommitDiff>> fixedRefDiff = new HashMap<>();
 
     /**
      * Refs that still contain user data after the backfill run. Only filled if --verify-commits,
@@ -140,6 +140,20 @@
     public List<String> refsFailedToFix = new ArrayList<>();
   }
 
+  /** Diff result of a single commit rewrite */
+  @AutoValue
+  public abstract static class CommitDiff {
+    public static CommitDiff create(ObjectId oldSha1, String commitDiff) {
+      return new AutoValue_CommitRewriter_CommitDiff(oldSha1, commitDiff);
+    }
+
+    /** SHA1 of the overwritten commit */
+    public abstract ObjectId oldSha1();
+
+    /** Diff applied to the commit with {@link #oldSha1} */
+    public abstract String diff();
+  }
+
   public static final String DEFAULT_ACCOUNT_REPLACEMENT = "Gerrit Account";
 
   private static final Pattern NON_REPLACE_ACCOUNT_PATTERN =
@@ -156,10 +170,14 @@
       Pattern.compile("Assignee changed from: (.*) to: (.*)");
 
   private static final Pattern REMOVED_REVIEWER_PATTERN =
-      Pattern.compile("Removed (cc|reviewer) (.*) .*");
+      Pattern.compile("Removed (cc|reviewer) (.*)(\\.| with the following votes)");
 
   private static final Pattern REMOVED_VOTE_PATTERN = Pattern.compile("Removed (.*) by (.*)");
 
+  private static final String REMOVED_VOTES_CHANGE_MESSAGE_START = "Removed the following votes:";
+  private static final Pattern REMOVED_VOTES_CHANGE_MESSAGE_PATTERN =
+      Pattern.compile("\\* (.*) by (.*)");
+
   private static final Pattern REMOVED_CHANGE_MESSAGE_PATTERN =
       Pattern.compile("Change message removed by: (.*)(\nReason: .*)?");
 
@@ -168,10 +186,12 @@
 
   private static final Pattern ON_CODE_OWNER_ADD_REVIEWER_PATTERN =
       Pattern.compile("(.*) who was added as reviewer owns the following files");
-  private static final Pattern ON_CODE_OWNER_APPROVAL_PATTERN =
-      Pattern.compile("code-owner approved by (.*):");
-  private static final Pattern ON_CODE_OWNER_OVERRIDE_PATTERN =
-      Pattern.compile("code-owners submit requirement .* overridden by (.*):");
+  private static final String ON_CODE_OWNER_APPROVAL_REGEX = "code-owner approved by (.*):";
+  private static final String ON_CODE_OWNER_OVERRIDE_REGEX =
+      "code-owners submit requirement .* overridden by (.*)";
+
+  private static final Pattern ON_CODE_OWNER_REVIEW_PATTERN =
+      Pattern.compile(ON_CODE_OWNER_APPROVAL_REGEX + "|" + ON_CODE_OWNER_OVERRIDE_REGEX);
 
   private static final Pattern REPLY_BY_REASON_PATTERN =
       Pattern.compile("(.*) replied on the change");
@@ -179,6 +199,11 @@
       Pattern.compile("Added by (.*) using the hovercard menu");
   private static final Pattern REMOVED_BY_REASON_PATTERN =
       Pattern.compile("Removed by (.*) using the hovercard menu");
+  private static final Pattern REMOVED_BY_ICON_CLICK_REASON_PATTERN =
+      Pattern.compile("Removed by (.*) by clicking the attention icon");
+
+  /** Matches {@link Account#getNameEmail} */
+  private static final Pattern NAME_EMAIL_PATTERN = Pattern.compile("(.*) (\\<.*\\>|\\(.*\\))");
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
@@ -211,6 +236,8 @@
     try (RevWalk revWalk = new RevWalk(repo);
         ObjectInserter ins = newPackInserter(repo)) {
       BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+      bru.setForceRefLog(true);
+      bru.setRefLogMessage(CommitRewriter.class.getName(), false);
       bru.setAllowNonFastForwards(true);
       for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
         Change.Id changeId = Change.Id.fromRef(ref.getName());
@@ -362,7 +389,8 @@
     ChangeFixProgress changeFixProgress = new ChangeFixProgress();
     while ((originalCommit = revWalk.next()) != null) {
 
-      changeFixProgress.updateAuthorId = parseIdent(originalCommit.getAuthorIdent());
+      changeFixProgress.updateAuthorId =
+          parseIdent(changeFixProgress, originalCommit.getAuthorIdent());
       PersonIdent fixedAuthorIdent;
       if (changeFixProgress.updateAuthorId.isPresent()) {
         fixedAuthorIdent =
@@ -378,8 +406,23 @@
               ? fixedCommitMessage.get()
               : originalCommit.getFullMessage();
       if (options.verifyCommits) {
-        changeFixProgress.isValidAfterFix &=
-            verifyCommit(commitMessage, fixedAuthorIdent, accountsInChange);
+        boolean isCommitValid = verifyCommit(commitMessage, fixedAuthorIdent, accountsInChange);
+        changeFixProgress.isValidAfterFix &= isCommitValid;
+        if (!isCommitValid) {
+          StringBuilder detailedVerificationStatus =
+              new StringBuilder(
+                  String.format(
+                      "Commit %s of ref %s failed verification after fix",
+                      originalCommit.getId(), ref));
+          detailedVerificationStatus.append("\nCommit body:\n");
+          detailedVerificationStatus.append(commitMessage);
+          if (fixedCommitMessage.isPresent()) {
+            detailedVerificationStatus.append("\n was fixed.\n");
+          }
+          detailedVerificationStatus.append("Commit author:\n");
+          detailedVerificationStatus.append(fixedAuthorIdent.toString());
+          logger.atWarning().log(detailedVerificationStatus.toString());
+        }
       }
       boolean needsFix =
           !fixedAuthorIdent.equals(originalCommit.getAuthorIdent())
@@ -411,7 +454,10 @@
             "Expected diff for commit %s of ref %s",
             originalCommit.getId(),
             ref.getName());
-        changeFixProgress.commitDiffs.add(diff);
+        changeFixProgress.commitDiffs.add(CommitDiff.create(originalCommit.getId(), diff));
+      } else if (needsFix) {
+        // Always output old commits SHA1
+        changeFixProgress.commitDiffs.add(CommitDiff.create(originalCommit.getId(), ""));
       }
     }
     return changeFixProgress;
@@ -478,37 +524,46 @@
   }
 
   private Optional<String> fixAssigneeChangeMessage(
-      Account.Id oldAssignee, Account.Id newAssignee, String originalChangeMessage) {
+      ChangeFixProgress changeFixProgress,
+      Optional<Account.Id> oldAssignee,
+      Optional<Account.Id> newAssignee,
+      String originalChangeMessage) {
     if (Strings.isNullOrEmpty(originalChangeMessage)) {
       return Optional.empty();
     }
 
     Matcher assigneeDeletedMatcher = ASSIGNEE_DELETED_PATTERN.matcher(originalChangeMessage);
     if (assigneeDeletedMatcher.matches()) {
-      if (!ACCOUNT_TEMPLATE_PATTERN.matcher(assigneeDeletedMatcher.group(1)).matches()) {
+      if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeDeletedMatcher.group(1)).matches()) {
         return Optional.of(
-            "Assignee deleted: " + AccountTemplateUtil.getAccountTemplate(oldAssignee));
+            "Assignee deleted: "
+                + getPossibleAccountReplacement(
+                    changeFixProgress, oldAssignee, assigneeDeletedMatcher.group(1)));
       }
       return Optional.empty();
     }
 
     Matcher assigneeAddedMatcher = ASSIGNEE_ADDED_PATTERN.matcher(originalChangeMessage);
     if (assigneeAddedMatcher.matches()) {
-      if (!ACCOUNT_TEMPLATE_PATTERN.matcher(assigneeAddedMatcher.group(1)).matches()) {
+      if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeAddedMatcher.group(1)).matches()) {
         return Optional.of(
-            "Assignee added: " + AccountTemplateUtil.getAccountTemplate(newAssignee));
+            "Assignee added: "
+                + getPossibleAccountReplacement(
+                    changeFixProgress, newAssignee, assigneeAddedMatcher.group(1)));
       }
       return Optional.empty();
     }
 
     Matcher assigneeChangedMatcher = ASSIGNEE_CHANGED_PATTERN.matcher(originalChangeMessage);
     if (assigneeChangedMatcher.matches()) {
-      if (!ACCOUNT_TEMPLATE_PATTERN.matcher(assigneeChangedMatcher.group(1)).matches()) {
+      if (!NON_REPLACE_ACCOUNT_PATTERN.matcher(assigneeChangedMatcher.group(1)).matches()) {
         return Optional.of(
             String.format(
                 "Assignee changed from: %s to: %s",
-                AccountTemplateUtil.getAccountTemplate(oldAssignee),
-                AccountTemplateUtil.getAccountTemplate(newAssignee)));
+                getPossibleAccountReplacement(
+                    changeFixProgress, oldAssignee, assigneeChangedMatcher.group(1)),
+                getPossibleAccountReplacement(
+                    changeFixProgress, newAssignee, assigneeChangedMatcher.group(2))));
       }
       return Optional.empty();
     }
@@ -520,7 +575,8 @@
       return Optional.empty();
     }
     Matcher matcher = REMOVED_REVIEWER_PATTERN.matcher(originalChangeMessage);
-    if (matcher.matches() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
+
+    if (matcher.find() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
       // Since we do not use change messages for reviewer updates on UI, it does not matter what we
       // rewrite it to.
       return Optional.of(originalChangeMessage.substring(0, matcher.end(1)));
@@ -529,24 +585,53 @@
   }
 
   private Optional<String> fixRemoveVoteChangeMessage(
-      Optional<Account.Id> reviewer, String originalChangeMessage) {
+      ChangeFixProgress changeFixProgress,
+      Optional<Account.Id> reviewer,
+      String originalChangeMessage) {
     if (Strings.isNullOrEmpty(originalChangeMessage)) {
       return Optional.empty();
     }
 
     Matcher matcher = REMOVED_VOTE_PATTERN.matcher(originalChangeMessage);
-    if (matcher.matches() && !ACCOUNT_TEMPLATE_PATTERN.matcher(matcher.group(2)).matches()) {
+    if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
       return Optional.of(
           String.format(
               "Removed %s by %s",
               matcher.group(1),
-              reviewer
-                  .map(AccountTemplateUtil::getAccountTemplate)
-                  .orElse(DEFAULT_ACCOUNT_REPLACEMENT)));
+              getPossibleAccountReplacement(
+                  changeFixProgress, reviewer, getNameFromNameEmail(matcher.group(2)))));
     }
     return Optional.empty();
   }
 
+  private Optional<String> fixRemoveVotesChangeMessage(
+      ChangeFixProgress changeFixProgress, String originalChangeMessage) {
+    if (Strings.isNullOrEmpty(originalChangeMessage)
+        || !originalChangeMessage.startsWith(REMOVED_VOTES_CHANGE_MESSAGE_START)) {
+      return Optional.empty();
+    }
+    String[] lines = originalChangeMessage.split("\\r?\\n");
+    StringBuilder fixedLines = new StringBuilder();
+    for (int i = 1; i < lines.length; i++) {
+      if (lines[i].isEmpty()) {
+        continue;
+      }
+      Matcher matcher = REMOVED_VOTES_CHANGE_MESSAGE_PATTERN.matcher(lines[i]);
+      if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
+        fixedLines.append(
+            String.format(
+                "* %s by %s\n",
+                matcher.group(1),
+                getPossibleAccountReplacement(
+                    changeFixProgress, Optional.empty(), getNameFromNameEmail(matcher.group(2)))));
+      }
+    }
+    if (fixedLines.length() == 0) {
+      return Optional.empty();
+    }
+    return Optional.of(REMOVED_VOTES_CHANGE_MESSAGE_START + "\n" + fixedLines);
+  }
+
   private Optional<String> fixDeleteChangeMessageCommitMessage(String originalChangeMessage) {
     if (Strings.isNullOrEmpty(originalChangeMessage)) {
       return Optional.empty();
@@ -589,43 +674,20 @@
 
     Matcher onAddReviewerMatcher = ON_CODE_OWNER_ADD_REVIEWER_PATTERN.matcher(originalMessage);
     if (!onAddReviewerMatcher.find()
-        || NON_REPLACE_ACCOUNT_PATTERN.matcher(onAddReviewerMatcher.group(1)).matches()) {
+        || NON_REPLACE_ACCOUNT_PATTERN
+            .matcher(normalizeOnCodeOwnerAddReviewerMatch(onAddReviewerMatcher.group(1)))
+            .matches()) {
       return Optional.empty();
     }
 
     // Pre fix, try to replace with something meaningful.
     // Retrieve reviewer accounts from cache and try to match by their name.
-    Map<Account.Id, AccountState> missingUserNameReviewers =
-        accountCache.get(
-            changeFixProgress.parsedReviewers.entrySet().stream()
-                .filter(entry -> entry.getValue().isEmpty())
-                .map(Map.Entry::getKey)
-                .collect(ImmutableSet.toImmutableSet()));
-    // TODO(mariasavtchouk): Adjust based on the dry run.
-    // We could just reset parsedReviewers here, because next message should only include the delta
-    changeFixProgress.parsedReviewers.putAll(
-        missingUserNameReviewers.entrySet().stream()
-            .collect(
-                ImmutableMap.toImmutableMap(
-                    Map.Entry::getKey, e -> e.getValue().account().getName())));
     onAddReviewerMatcher.reset();
     StringBuffer sb = new StringBuffer();
     while (onAddReviewerMatcher.find()) {
-      String reviewerName = onAddReviewerMatcher.group(1);
-      Set<Account.Id> possibleReplacements =
-          changeFixProgress.parsedReviewers.entrySet().stream()
-              .filter(reviewer -> reviewer.getValue().equals(reviewerName))
-              .map(Entry::getKey)
-              .collect(ImmutableSet.toImmutableSet());
-      String replacementName = DEFAULT_ACCOUNT_REPLACEMENT;
-      if (possibleReplacements.isEmpty()) {
-        logger.atWarning().log("Could not find reviewer account matching name %s", reviewerName);
-      } else if (possibleReplacements.size() > 1) {
-        logger.atWarning().log("Found multiple reviewer account matching name %s", reviewerName);
-      } else {
-        replacementName =
-            AccountTemplateUtil.getAccountTemplate(Iterables.getOnlyElement(possibleReplacements));
-      }
+      String reviewerName = normalizeOnCodeOwnerAddReviewerMatch(onAddReviewerMatcher.group(1));
+      String replacementName =
+          getPossibleAccountReplacement(changeFixProgress, Optional.empty(), reviewerName);
       onAddReviewerMatcher.appendReplacement(
           sb, replacementName + ", who was added as reviewer owns the following files");
     }
@@ -634,36 +696,42 @@
     return Optional.of(sb.toString());
   }
 
+  /**
+   * See {@link #ON_CODE_OWNER_ADD_REVIEWER_PATTERN}.
+   *
+   * <p>Some of the messages have format '{@link AccountTemplateUtil#ACCOUNT_TEMPLATE}, who...',
+   * while others '{@link AccountTemplateUtil#ACCOUNT_TEMPLATE} who...'.
+   *
+   * <p>Cut the trailing ',' from the match, so that valid patterns are not replaced.
+   */
+  private static String normalizeOnCodeOwnerAddReviewerMatch(String reviewerMatch) {
+    String reviewerName = reviewerMatch;
+    if (reviewerName.charAt(reviewerName.length() - 1) == ',') {
+      reviewerName = reviewerName.substring(0, reviewerName.length() - 1);
+    }
+    return reviewerName;
+  }
+
   private Optional<String> fixCodeOwnersOnReviewChangeMessage(
       Optional<Account.Id> reviewer, String originalMessage) {
     if (Strings.isNullOrEmpty(originalMessage)) {
       return Optional.empty();
     }
 
-    Matcher onCodeOwnerApprovalMatcher = ON_CODE_OWNER_APPROVAL_PATTERN.matcher(originalMessage);
-    if (onCodeOwnerApprovalMatcher.find()
-        && !ACCOUNT_TEMPLATE_PATTERN.matcher(onCodeOwnerApprovalMatcher.group(1)).matches()) {
-      return Optional.of(
-          originalMessage.replace(
-                  "approved by " + onCodeOwnerApprovalMatcher.group(1),
-                  "approved by "
-                      + reviewer
-                          .map(AccountTemplateUtil::getAccountTemplate)
-                          .orElse(DEFAULT_ACCOUNT_REPLACEMENT))
-              + "\n");
-    }
-
-    Matcher onCodeOwnerOverrideMatcher = ON_CODE_OWNER_OVERRIDE_PATTERN.matcher(originalMessage);
-    if (onCodeOwnerOverrideMatcher.find()
-        && !ACCOUNT_TEMPLATE_PATTERN.matcher(onCodeOwnerOverrideMatcher.group(1)).matches()) {
-      return Optional.of(
-          originalMessage.replace(
-                  "overridden by " + onCodeOwnerOverrideMatcher.group(1),
-                  "overridden by "
-                      + reviewer
-                          .map(AccountTemplateUtil::getAccountTemplate)
-                          .orElse(DEFAULT_ACCOUNT_REPLACEMENT))
-              + "\n");
+    Matcher onCodeOwnerReviewMatcher = ON_CODE_OWNER_REVIEW_PATTERN.matcher(originalMessage);
+    while (onCodeOwnerReviewMatcher.find()) {
+      String accountName =
+          firstNonNull(onCodeOwnerReviewMatcher.group(1), onCodeOwnerReviewMatcher.group(2));
+      if (!ACCOUNT_TEMPLATE_PATTERN.matcher(accountName).matches()) {
+        return Optional.of(
+            originalMessage.replace(
+                    "by " + accountName,
+                    "by "
+                        + reviewer
+                            .map(AccountTemplateUtil::getAccountTemplate)
+                            .orElse(DEFAULT_ACCOUNT_REPLACEMENT))
+                + "\n");
+      }
     }
 
     return Optional.empty();
@@ -694,6 +762,14 @@
 
       return Optional.of("Removed by someone using the hovercard menu");
     }
+
+    Matcher removedByIconClickReasonMatcher =
+        REMOVED_BY_ICON_CLICK_REASON_PATTERN.matcher(originalReason);
+    if (removedByIconClickReasonMatcher.matches()
+        && !OK_ACCOUNT_NAME_PATTERN.matcher(removedByIconClickReasonMatcher.group(1)).matches()) {
+
+      return Optional.of("Removed by someone by clicking the attention icon");
+    }
     return Optional.empty();
   }
 
@@ -733,22 +809,22 @@
     for (FooterLine fl : footerLines) {
       String footerKey = fl.getKey();
       String footerValue = fl.getValue();
-      if (footerKey.equalsIgnoreCase(FOOTER_TAG.getName())) {
-        if (footerValue.equals(ChangeMessagesUtil.TAG_MERGED) && !fixedChangeMessage.isPresent()) {
-          fixedChangeMessage = fixSubmitChangeMessage(originalChangeMessage);
-        }
-      } else if (footerKey.equalsIgnoreCase(FOOTER_ASSIGNEE.getName())) {
+      if (footerKey.equalsIgnoreCase(FOOTER_ASSIGNEE.getName())) {
         Account.Id oldAssignee = fixProgress.assigneeId;
         FixIdentResult fixedAssignee = null;
         if (footerValue.equals("")) {
           fixProgress.assigneeId = null;
         } else {
-          fixedAssignee = getFixedIdentString(footerValue);
+          fixedAssignee = getFixedIdentString(fixProgress, footerValue);
           fixProgress.assigneeId = fixedAssignee.accountId;
         }
         if (!fixedChangeMessage.isPresent()) {
           fixedChangeMessage =
-              fixAssigneeChangeMessage(oldAssignee, fixProgress.assigneeId, originalChangeMessage);
+              fixAssigneeChangeMessage(
+                  fixProgress,
+                  Optional.ofNullable(oldAssignee),
+                  Optional.ofNullable(fixProgress.assigneeId),
+                  originalChangeMessage);
         }
         if (fixedAssignee != null && fixedAssignee.fixedIdentString.isPresent()) {
           addFooter(footerLinesBuilder, footerKey, fixedAssignee.fixedIdentString.get());
@@ -760,15 +836,14 @@
         if (!fixedChangeMessage.isPresent()) {
           fixedChangeMessage = fixReviewerChangeMessage(originalChangeMessage);
         }
-        FixIdentResult fixedReviewer = getFixedIdentString(footerValue);
-        fixProgress.parsedReviewers.putIfAbsent(fixedReviewer.accountId, "");
+        FixIdentResult fixedReviewer = getFixedIdentString(fixProgress, footerValue);
         if (fixedReviewer.fixedIdentString.isPresent()) {
           addFooter(footerLinesBuilder, footerKey, fixedReviewer.fixedIdentString.get());
           anyFootersFixed = true;
           continue;
         }
       } else if (footerKey.equalsIgnoreCase(FOOTER_REAL_USER.getName())) {
-        FixIdentResult fixedRealUser = getFixedIdentString(footerValue);
+        FixIdentResult fixedRealUser = getFixedIdentString(fixProgress, footerValue);
         if (fixedRealUser.fixedIdentString.isPresent()) {
           addFooter(footerLinesBuilder, footerKey, fixedRealUser.fixedIdentString.get());
           anyFootersFixed = true;
@@ -779,11 +854,12 @@
         FixIdentResult fixedVoter = null;
         if (voterIdentStart > 0) {
           String originalIdentString = footerValue.substring(voterIdentStart + 1);
-          fixedVoter = getFixedIdentString(originalIdentString);
+          fixedVoter = getFixedIdentString(fixProgress, originalIdentString);
         }
         if (!fixedChangeMessage.isPresent()) {
           fixedChangeMessage =
               fixRemoveVoteChangeMessage(
+                  fixProgress,
                   fixedVoter == null
                       ? fixProgress.updateAuthorId
                       : Optional.of(fixedVoter.accountId),
@@ -803,7 +879,7 @@
         int voterIdentStart = StringUtils.ordinalIndexOf(footerValue, ": ", 2);
         if (voterIdentStart >= 0) {
           String originalIdentString = footerValue.substring(voterIdentStart + 2);
-          FixIdentResult fixedVoter = getFixedIdentString(originalIdentString);
+          FixIdentResult fixedVoter = getFixedIdentString(fixProgress, originalIdentString);
           if (fixedVoter.fixedIdentString.isPresent()) {
             String fixedLabelVote =
                 footerValue.substring(0, voterIdentStart)
@@ -819,7 +895,7 @@
         AttentionStatusInNoteDb originalAttentionSetUpdate =
             gson.fromJson(footerValue, AttentionStatusInNoteDb.class);
         FixIdentResult fixedAttentionAccount =
-            getFixedIdentString(originalAttentionSetUpdate.personIdent);
+            getFixedIdentString(fixProgress, originalAttentionSetUpdate.personIdent);
         Optional<String> fixedReason = fixAttentionSetReason(originalAttentionSetUpdate.reason);
         if (fixedAttentionAccount.fixedIdentString.isPresent() || fixedReason.isPresent()) {
           AttentionStatusInNoteDb fixedAttentionSetUpdate =
@@ -836,7 +912,27 @@
       }
       addFooter(footerLinesBuilder, footerKey, footerValue);
     }
-
+    // Some of the old commits are missing corresponding footers but still have change messages that
+    // need the fix. For such cases, try to guess or replace with the default string (see
+    // getPossibleAccountReplacement)
+    if (!fixedChangeMessage.isPresent()) {
+      fixedChangeMessage = fixReviewerChangeMessage(originalChangeMessage);
+    }
+    if (!fixedChangeMessage.isPresent()) {
+      fixedChangeMessage = fixRemoveVotesChangeMessage(fixProgress, originalChangeMessage);
+    }
+    if (!fixedChangeMessage.isPresent()) {
+      fixedChangeMessage =
+          fixRemoveVoteChangeMessage(fixProgress, Optional.empty(), originalChangeMessage);
+    }
+    if (!fixedChangeMessage.isPresent()) {
+      fixedChangeMessage =
+          fixAssigneeChangeMessage(
+              fixProgress, Optional.empty(), Optional.empty(), originalChangeMessage);
+    }
+    if (!fixedChangeMessage.isPresent()) {
+      fixedChangeMessage = fixSubmitChangeMessage(originalChangeMessage);
+    }
     if (!fixedChangeMessage.isPresent()) {
       fixedChangeMessage = fixDeleteChangeMessageCommitMessage(originalChangeMessage);
     }
@@ -872,9 +968,11 @@
     return sb;
   }
 
-  private Optional<Account.Id> parseIdent(PersonIdent ident) {
+  private Optional<Account.Id> parseIdent(ChangeFixProgress changeFixProgress, PersonIdent ident) {
     Optional<Account.Id> account = NoteDbUtil.parseIdent(ident);
-    if (!account.isPresent()) {
+    if (account.isPresent()) {
+      changeFixProgress.parsedAccounts.putIfAbsent(account.get(), "");
+    } else {
       logger.atWarning().log("Failed to parse id %s", ident);
     }
     return account;
@@ -896,20 +994,22 @@
    * Parses {@code originalIdentString} and applies the fix, so it does not contain user data, see
    * {@link ChangeNoteUtil#appendAccountIdIdentString}.
    *
+   * @param changeFixProgress see {@link ChangeFixProgress}
    * @param originalIdentString ident to apply the fix to.
    * @return {@link FixIdentResult}, with {@link FixIdentResult#accountId} parsed from {@code
    *     originalIdentString} and {@link FixIdentResult#fixedIdentString} if the fix was applied.
    * @throws ConfigInvalidException if could not parse {@link FixIdentResult#accountId} from {@code
    *     originalIdentString}
    */
-  private FixIdentResult getFixedIdentString(String originalIdentString)
+  private FixIdentResult getFixedIdentString(
+      ChangeFixProgress changeFixProgress, String originalIdentString)
       throws ConfigInvalidException {
     FixIdentResult fixIdentResult = new FixIdentResult();
     PersonIdent originalIdent = RawParseUtils.parsePersonIdent(originalIdentString);
     // Ident as String is saved in NoteDB footers, if this fails to parse, something is
     // wrong with the change and we better not touch it.
     fixIdentResult.accountId =
-        parseIdent(originalIdent)
+        parseIdent(changeFixProgress, originalIdent)
             .orElseThrow(
                 () -> new ConfigInvalidException("field to parse id: " + originalIdentString));
     String fixedIdentString =
@@ -922,6 +1022,59 @@
     return fixIdentResult;
   }
 
+  /** Extracts {@link Account#getName} from {@link Account#getNameEmail} */
+  private String getNameFromNameEmail(String nameEmail) {
+    Matcher nameEmailMatcher = NAME_EMAIL_PATTERN.matcher(nameEmail);
+    return nameEmailMatcher.matches() ? nameEmailMatcher.group(1) : nameEmail;
+  }
+
+  /**
+   * Returns replacement for {@code accountName}.
+   *
+   * <p>If {@code account} is known, replace with {@link AccountTemplateUtil#getAccountTemplate}.
+   * Otherwise, try to guess the correct replacement account for {@code accountName} among {@link
+   * ChangeFixProgress#parsedAccounts} that appeared in the change. If this fails {@link
+   * #DEFAULT_ACCOUNT_REPLACEMENT} is applied.
+   *
+   * @param changeFixProgress see {@link ChangeFixProgress}
+   * @param account account that should be used for replacement, if known
+   * @param accountName {@link Account#getName} to replace.
+   * @return replacement for {@code accountName}
+   */
+  private String getPossibleAccountReplacement(
+      ChangeFixProgress changeFixProgress, Optional<Account.Id> account, String accountName) {
+    if (account.isPresent()) {
+      return AccountTemplateUtil.getAccountTemplate(account.get());
+    }
+    // Retrieve reviewer accounts from cache and try to match by their name.
+    Map<Account.Id, AccountState> missingUserNameReviewers =
+        accountCache.get(
+            changeFixProgress.parsedAccounts.entrySet().stream()
+                .filter(entry -> entry.getValue().isEmpty())
+                .map(Map.Entry::getKey)
+                .collect(ImmutableSet.toImmutableSet()));
+    changeFixProgress.parsedAccounts.putAll(
+        missingUserNameReviewers.entrySet().stream()
+            .collect(
+                ImmutableMap.toImmutableMap(
+                    Map.Entry::getKey, e -> e.getValue().account().getName())));
+    Set<Account.Id> possibleReplacements =
+        changeFixProgress.parsedAccounts.entrySet().stream()
+            .filter(e -> e.getValue().equals(accountName))
+            .map(Entry::getKey)
+            .collect(ImmutableSet.toImmutableSet());
+    String replacementName = DEFAULT_ACCOUNT_REPLACEMENT;
+    if (possibleReplacements.isEmpty()) {
+      logger.atWarning().log("Could not find reviewer account matching name %s", accountName);
+    } else if (possibleReplacements.size() > 1) {
+      logger.atWarning().log("Found multiple reviewer account matching name %s", accountName);
+    } else {
+      replacementName =
+          AccountTemplateUtil.getAccountTemplate(Iterables.getOnlyElement(possibleReplacements));
+    }
+    return replacementName;
+  }
+
   /**
    * Cuts tree and parent lines from raw unparsed commit body, so they are not included in diff
    * comparison.
@@ -989,11 +1142,11 @@
     Optional<Account.Id> updateAuthorId = null;
 
     /**
-     * Reviewer accounts parsed so far together with their {@link Account#getName} extracted from
-     * {@link #accountCache} if needed by rewrite. Maps to empty string if was not requested from
-     * cache yet.
+     * Accounts parsed so far together with their {@link Account#getName} extracted from {@link
+     * #accountCache} if needed by rewrite. Maps to empty string if was not requested from cache
+     * yet.
      */
-    Map<Account.Id, String> parsedReviewers = new HashMap<>();
+    Map<Account.Id, String> parsedAccounts = new HashMap<>();
 
     /** Id of the current commit in rewriter walk. */
     ObjectId newTipId = null;
@@ -1006,6 +1159,6 @@
      */
     boolean isValidAfterFix = true;
 
-    List<String> commitDiffs = new ArrayList<>();
+    List<CommitDiff> commitDiffs = new ArrayList<>();
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
index e07c793..6d6d53d 100644
--- a/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteChangeMessageRewriter.java
@@ -110,7 +110,6 @@
    * @param commitMessage the full commit message of the new commit.
    * @param inserter the {@code ObjectInserter} for the rewrite process.
    * @return the {@code objectId} of the new commit.
-   * @throws IOException
    */
   private ObjectId rewriteOneCommit(
       RevCommit originalCommit,
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index d0b6247..e8c0fda 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -191,8 +191,6 @@
    * @param putInComments the comments put in by this commit.
    * @param deletedComments the comments deleted by this commit.
    * @return the {@code objectId} of the new commit.
-   * @throws IOException
-   * @throws ConfigInvalidException
    */
   private ObjectId rewriteCommit(
       RevCommit originalCommit,
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
index 7a8e28f..7ae98778 100644
--- a/java/com/google/gerrit/server/notedb/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -112,8 +112,11 @@
                 .setCumulative()
                 .setUnit(Units.MILLISECONDS),
             Field.ofEnum(SequenceType.class, "sequence", Metadata.Builder::noteDbSequenceType)
+                .description("The sequence from which IDs were retrieved.")
                 .build(),
-            Field.ofBoolean("multiple", Metadata.Builder::multiple).build());
+            Field.ofBoolean("multiple", Metadata.Builder::multiple)
+                .description("Whether more than one ID was retrieved.")
+                .build());
   }
 
   public int nextAccountId() {
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
index 4d99f93..57a3cd7 100644
--- a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.gerrit.entities.Change;
 import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -39,10 +38,19 @@
 
   @Override
   public boolean updateChange(ChangeContext ctx) throws Exception {
-    Change change = ctx.getChange();
-    ChangeData changeData = changeDataFactory.create(change);
-    ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
-    update.putSubmitRequirementResults(evaluator.getResults(changeData).values());
+    // Create ChangeData using the project/change IDs instead of ctx.getChange(). We do that because
+    // for changes requiring a rebase before submission (e.g. if submit type = RebaseAlways), the
+    // RebaseOp inserts a new patchset that is visible here (via Change#getCurrentPatchset). If we
+    // then try to get ChangeData#currentPatchset it will return null, since it loads patchsets from
+    // NoteDb but tries to find the patchset with the ID of the one just inserted by the rebase op.
+    // Note that this implementation means that, in this case, submit requirement results will be
+    // stored in change notes of the pre last patchset commit. This is fine since submit requirement
+    // results should evaluate to the exact same results for both commits. Additionally, the
+    // pre-last commit is the one for which we displayed the submit requirement results of the last
+    // patchset to the user before it was merged.
+    ChangeData changeData = changeDataFactory.create(ctx.getProject(), ctx.getChange().getId());
+    ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+    update.putSubmitRequirementResults(evaluator.evaluateAllRequirements(changeData).values());
     return !changeData.submitRequirements().isEmpty();
   }
 }
diff --git a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
index 416b850..9bf56d8 100644
--- a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
+++ b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
@@ -40,6 +40,7 @@
     SubmitRequirementResultProto.Builder builder = SubmitRequirementResultProto.newBuilder();
     builder
         .setSubmitRequirement(SubmitRequirementSerializer.serialize(r.submitRequirement()))
+        .setLegacy(r.legacy())
         .setCommit(ObjectIdConverter.create().toByteString(r.patchSetCommitId()));
     if (r.applicabilityExpressionResult().isPresent()) {
       builder.setApplicabilityExpressionResult(
@@ -60,6 +61,7 @@
   public SubmitRequirementResult fromProto(SubmitRequirementResultProto proto) {
     SubmitRequirementResult.Builder builder =
         SubmitRequirementResult.builder()
+            .legacy(proto.getLegacy())
             .patchSetCommitId(ObjectIdConverter.create().fromByteString(proto.getCommit()))
             .submitRequirement(
                 SubmitRequirementSerializer.deserialize(proto.getSubmitRequirement()));
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 97910400..2529c04 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -100,18 +100,22 @@
       MetricMaker metricMaker,
       @GerritServerConfig Config cfg,
       @GerritPersonIdent Provider<PersonIdent> gerritIdentProvider) {
+    Field<OperationType> operationTypeField =
+        Field.ofEnum(OperationType.class, "type", Metadata.Builder::operationName)
+            .description("The type of the operation (CACHE_LOAD, IN_MEMORY_WRITE, ON_DISK_WRITE).")
+            .build();
     this.counter =
         metricMaker.newCounter(
             "git/auto-merge/num_operations",
             new Description("AutoMerge computations").setRate().setUnit("auto merge computations"),
-            Field.ofEnum(OperationType.class, "type", Metadata.Builder::operationName).build());
+            operationTypeField);
     this.latency =
         metricMaker.newTimer(
             "git/auto-merge/latency",
             new Description("AutoMerge computation latency")
                 .setCumulative()
                 .setUnit("milliseconds"),
-            Field.ofEnum(OperationType.class, "type", Metadata.Builder::operationName).build());
+            operationTypeField);
     this.save = cacheAutomerge(cfg);
     this.gerritIdentProvider = gerritIdentProvider;
     this.configuredMergeStrategy = MergeUtil.getMergeStrategy(cfg);
diff --git a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
index b61e0c7..fcce672 100644
--- a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
+++ b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -16,58 +16,73 @@
 
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Callable;
+import org.eclipse.jgit.lib.ObjectId;
 
 public class DiffSummaryLoader implements Callable<DiffSummary> {
   public interface Factory {
     DiffSummaryLoader create(DiffSummaryKey key, Project.NameKey project);
   }
 
-  private final PatchListCache patchListCache;
+  private final DiffOperations diffOperations;
   private final DiffSummaryKey key;
   private final Project.NameKey project;
 
   @Inject
-  DiffSummaryLoader(PatchListCache plc, @Assisted DiffSummaryKey k, @Assisted Project.NameKey p) {
-    patchListCache = plc;
+  DiffSummaryLoader(
+      DiffOperations diffOps, @Assisted DiffSummaryKey k, @Assisted Project.NameKey p) {
+    diffOperations = diffOps;
     key = k;
     project = p;
   }
 
   @Override
   public DiffSummary call() throws Exception {
-    PatchList patchList = patchListCache.get(key.toPatchListKey(), project);
-    return toDiffSummary(patchList);
+    ObjectId oldId = key.toPatchListKey().getOldId();
+    ObjectId newId = key.toPatchListKey().getNewId();
+    Map<String, FileDiffOutput> diffList =
+        oldId == null
+            ? diffOperations.listModifiedFilesAgainstParent(project, newId, /* parentNum= */ 0)
+            : diffOperations.listModifiedFiles(project, oldId, newId);
+    return toDiffSummary(diffList);
   }
 
-  private DiffSummary toDiffSummary(PatchList patchList) {
-    List<String> r = new ArrayList<>(patchList.getPatches().size());
-    for (PatchListEntry e : patchList.getPatches()) {
-      if (Patch.isMagic(e.getNewName())) {
+  private DiffSummary toDiffSummary(Map<String, FileDiffOutput> fileDiffs) {
+    List<String> r = new ArrayList<>(fileDiffs.size());
+    int linesInserted = 0;
+    int linesDeleted = 0;
+    for (String path : fileDiffs.keySet()) {
+      if (Patch.isMagic(path)) {
         continue;
       }
-      switch (e.getChangeType()) {
+      FileDiffOutput fileDiff = fileDiffs.get(path);
+      linesInserted += fileDiff.insertions();
+      linesDeleted += fileDiff.deletions();
+      switch (fileDiff.changeType()) {
         case ADDED:
         case MODIFIED:
         case DELETED:
         case COPIED:
         case REWRITE:
-          r.add(e.getNewName());
+          r.add(
+              FilePathAdapter.getNewPath(
+                  fileDiff.oldPath(), fileDiff.newPath(), fileDiff.changeType()));
           break;
 
         case RENAMED:
-          r.add(e.getOldName());
-          r.add(e.getNewName());
+          r.add(FilePathAdapter.getOldPath(fileDiff.oldPath(), fileDiff.changeType()));
+          r.add(
+              FilePathAdapter.getNewPath(
+                  fileDiff.oldPath(), fileDiff.newPath(), fileDiff.changeType()));
           break;
       }
     }
-    return new DiffSummary(
-        r.stream().sorted().toArray(String[]::new),
-        patchList.getInsertions(),
-        patchList.getDeletions());
+    return new DiffSummary(r.stream().sorted().toArray(String[]::new), linesInserted, linesDeleted);
   }
 }
diff --git a/java/com/google/gerrit/server/patch/FilePathAdapter.java b/java/com/google/gerrit/server/patch/FilePathAdapter.java
index ccd1466..2c98f1a 100644
--- a/java/com/google/gerrit/server/patch/FilePathAdapter.java
+++ b/java/com/google/gerrit/server/patch/FilePathAdapter.java
@@ -35,11 +35,12 @@
       case DELETED:
       case ADDED:
       case MODIFIED:
-      case REWRITE:
         return null;
       case COPIED:
       case RENAMED:
         return oldName.get();
+      case REWRITE:
+        return oldName.isPresent() ? oldName.get() : null;
       default:
         throw new IllegalArgumentException("Unsupported type " + changeType);
     }
diff --git a/java/com/google/gerrit/server/patch/IntraLineLoader.java b/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 29f5c2c..d6afa88 100644
--- a/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -15,7 +15,6 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.common.base.Supplier;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -28,6 +27,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -35,6 +35,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.MyersDiff;
 import org.eclipse.jgit.lib.Config;
@@ -273,39 +274,71 @@
    *     otherwise.
    */
   private static boolean isValidTransformation(CharText lText, CharText rText, List<Edit> edits) {
-    Supplier<String> applyDeleteAndReplaceEditsToLeft =
-        () -> {
-          StringBuilder reconstructed = toStringBuilder(lText);
-          String right = toStringBuilder(rText).toString();
-          // Process edits right to left to avoid re-computation of indices when characters are
-          // removed.
-          for (int i = edits.size() - 1; i >= 0; i--) {
-            Edit edit = edits.get(i);
-            if (edit.getType() == Edit.Type.REPLACE) {
-              reconstructed.replace(
-                  edit.getBeginA(),
-                  edit.getEndA(),
-                  right.substring(edit.getBeginB(), edit.getEndB()));
-            } else if (edit.getType() == Edit.Type.DELETE) {
-              reconstructed.delete(edit.getBeginA(), edit.getEndA());
-            }
-          }
-          return reconstructed.toString();
-        };
-    Supplier<StringBuilder> removeInsertEditsFromRight =
-        () -> {
-          StringBuilder reconstructed = toStringBuilder(rText);
-          // Process edits right to left to avoid re-computation of indices when characters are
-          // removed.
-          for (int i = edits.size() - 1; i >= 0; i--) {
-            Edit edit = edits.get(i);
-            if (edit.getType() == Edit.Type.INSERT) {
-              reconstructed.delete(edit.getBeginB(), edit.getEndB());
-            }
-          }
-          return reconstructed;
-        };
-    return applyDeleteAndReplaceEditsToLeft.get().contentEquals(removeInsertEditsFromRight.get());
+    // Apply replace and delete edits to the left text
+    Optional<String> left =
+        applyEditsToString(
+            toStringBuilder(lText),
+            toStringBuilder(rText).toString(),
+            edits.stream()
+                .filter(e -> e.getType() == Edit.Type.REPLACE || e.getType() == Edit.Type.DELETE)
+                .collect(Collectors.toList()));
+    // Remove insert edits from the right text
+    Optional<String> right =
+        applyEditsToString(
+            toStringBuilder(rText),
+            null,
+            edits.stream()
+                .filter(e -> e.getType() == Edit.Type.INSERT)
+                .collect(Collectors.toList()));
+
+    return left.isPresent() && right.isPresent() && left.get().contentEquals(right.get());
+  }
+
+  /**
+   * Apply edits to the {@code target} string. Replace edits are applied to target and replaced with
+   * a substring from {@code from}. Delete edits are applied to target. Insert edits are removed
+   * from target.
+   *
+   * @return Optional containing the transformed string, or empty if the transformation fails (due
+   *     to index out of bounds).
+   */
+  private static Optional<String> applyEditsToString(
+      StringBuilder target, String from, List<Edit> edits) {
+    // Process edits right to left to avoid re-computation of indices when characters are removed.
+    try {
+      for (int i = edits.size() - 1; i >= 0; i--) {
+        Edit edit = edits.get(i);
+        if (edit.getType() == Edit.Type.REPLACE) {
+          boundaryCheck(target, edit.getBeginA(), edit.getEndA() - 1);
+          boundaryCheck(from, edit.getBeginB(), edit.getEndB() - 1);
+          target.replace(
+              edit.getBeginA(), edit.getEndA(), from.substring(edit.getBeginB(), edit.getEndB()));
+        } else if (edit.getType() == Edit.Type.DELETE) {
+          boundaryCheck(target, edit.getBeginA(), edit.getEndA() - 1);
+          target.delete(edit.getBeginA(), edit.getEndA());
+        } else if (edit.getType() == Edit.Type.INSERT) {
+          boundaryCheck(target, edit.getBeginB(), edit.getEndB() - 1);
+          target.delete(edit.getBeginB(), edit.getEndB());
+        }
+      }
+      return Optional.of(target.toString());
+    } catch (StringIndexOutOfBoundsException unused) {
+      return Optional.empty();
+    }
+  }
+
+  private static void boundaryCheck(StringBuilder s, int i1, int i2) {
+    if (i1 >= 0 && i2 >= 0 && i1 < s.length() && i2 < s.length()) {
+      return;
+    }
+    throw new StringIndexOutOfBoundsException();
+  }
+
+  private static void boundaryCheck(String s, int i1, int i2) {
+    if (i1 >= 0 && i2 >= 0 && i1 < s.length() && i2 < s.length()) {
+      return;
+    }
+    throw new StringIndexOutOfBoundsException();
   }
 
   private static StringBuilder toStringBuilder(CharText text) {
diff --git a/java/com/google/gerrit/server/patch/PatchFile.java b/java/com/google/gerrit/server/patch/PatchFile.java
index ca5223d..81355cc 100644
--- a/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/java/com/google/gerrit/server/patch/PatchFile.java
@@ -18,7 +18,9 @@
 
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.exceptions.NoSuchEntityException;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import java.io.IOException;
+import java.util.Map;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -35,7 +37,7 @@
 /** State supporting processing of a single {@link Patch} instance. */
 public class PatchFile {
   private final Repository repo;
-  private final PatchListEntry entry;
+  private final FileDiffOutput diff;
   private final RevTree aTree;
   private final RevTree bTree;
 
@@ -51,21 +53,30 @@
   private Text a;
   private Text b;
 
-  public PatchFile(Repository repo, PatchList patchList, String fileName)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+  public PatchFile(Repository repo, Map<String, FileDiffOutput> modifiedFiles, String fileName)
+      throws IOException {
     this.repo = repo;
-    this.entry = patchList.get(fileName);
+    this.diff =
+        modifiedFiles.values().stream()
+            .filter(f -> f.newPath().isPresent() && f.newPath().get().equals(fileName))
+            .findFirst()
+            .orElse(FileDiffOutput.empty(fileName, ObjectId.zeroId(), ObjectId.zeroId()));
 
+    if (Patch.PATCHSET_LEVEL.equals(fileName)) {
+      aTree = null;
+      bTree = null;
+      return;
+    }
     try (ObjectReader reader = repo.newObjectReader();
         RevWalk rw = new RevWalk(reader)) {
-      final RevCommit bCommit = rw.parseCommit(patchList.getNewId());
+      final RevCommit bCommit = rw.parseCommit(diff.newCommitId());
 
       if (Patch.COMMIT_MSG.equals(fileName)) {
-        if (patchList.getComparisonType().isAgainstParentOrAutoMerge()) {
+        if (diff.comparisonType().isAgainstParentOrAutoMerge()) {
           a = Text.EMPTY;
         } else {
           // For the initial commit, we have an empty tree on Side A
-          RevObject object = rw.parseAny(patchList.getOldId());
+          RevObject object = rw.parseAny(diff.oldCommitId());
           a = object instanceof RevCommit ? Text.forCommit(reader, object) : Text.EMPTY;
         }
         b = Text.forCommit(reader, bCommit);
@@ -74,18 +85,18 @@
         bTree = null;
       } else if (Patch.MERGE_LIST.equals(fileName)) {
         // For the initial commit, we have an empty tree on Side A
-        RevObject object = rw.parseAny(patchList.getOldId());
+        RevObject object = rw.parseAny(diff.oldCommitId());
         a =
             object instanceof RevCommit
-                ? Text.forMergeList(patchList.getComparisonType(), reader, object)
+                ? Text.forMergeList(diff.comparisonType(), reader, object)
                 : Text.EMPTY;
-        b = Text.forMergeList(patchList.getComparisonType(), reader, bCommit);
+        b = Text.forMergeList(diff.comparisonType(), reader, bCommit);
 
         aTree = null;
         bTree = null;
       } else {
-        if (patchList.getOldId() != null) {
-          aTree = rw.parseTree(patchList.getOldId());
+        if (diff.oldCommitId() != null) {
+          aTree = rw.parseTree(diff.oldCommitId());
         } else {
           final RevCommit p = bCommit.getParent(0);
           rw.parseHeaders(p);
@@ -97,11 +108,11 @@
   }
 
   private String getOldName() {
-    String name = entry.getOldName();
+    String name = FilePathAdapter.getOldPath(diff.oldPath(), diff.changeType());
     if (name != null) {
       return name;
     }
-    return entry.getNewName();
+    return FilePathAdapter.getNewPath(diff.oldPath(), diff.newPath(), diff.changeType());
   }
 
   /**
@@ -111,7 +122,6 @@
    * @param line the line number to extract (1 based; 1 is the first line).
    * @return the string version of the file line.
    * @throws IOException the patch or complete file content cannot be read.
-   * @throws NoSuchEntityException
    */
   public String getLine(int file, int line) throws IOException, NoSuchEntityException {
     switch (file) {
@@ -123,7 +133,10 @@
 
       case 1:
         if (b == null) {
-          b = load(bTree, entry.getNewName());
+          b =
+              load(
+                  bTree,
+                  FilePathAdapter.getNewPath(diff.oldPath(), diff.newPath(), diff.changeType()));
         }
         return b.getString(line - 1);
 
@@ -135,7 +148,7 @@
   private Text load(ObjectId tree, String path)
       throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
           IOException {
-    if (path == null) {
+    if (path == null || Patch.PATCHSET_LEVEL.equals(path)) {
       return Text.EMPTY;
     }
     final TreeWalk tw = TreeWalk.forPath(repo, path, tree);
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index cb95553..b983fb8 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -140,17 +140,17 @@
     return Collections.unmodifiableList(Arrays.asList(patches));
   }
 
-  /** @return the comparison type */
+  /** Returns the comparison type */
   public ComparisonType getComparisonType() {
     return comparisonType;
   }
 
-  /** @return total number of new lines added. */
+  /** Returns total number of new lines added. */
   public int getInsertions() {
     return insertions;
   }
 
-  /** @return total number of lines removed. */
+  /** Returns total number of lines removed. */
   public int getDeletions() {
     return deletions;
   }
diff --git a/java/com/google/gerrit/server/patch/PatchListCache.java b/java/com/google/gerrit/server/patch/PatchListCache.java
index e60302a..6d42249 100644
--- a/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
-import org.eclipse.jgit.lib.ObjectId;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
@@ -32,30 +29,6 @@
   @Deprecated
   PatchList get(PatchListKey key, Project.NameKey project) throws PatchListNotAvailableException;
 
-  /**
-   * Returns the patch list - list of modified files - between two commits.
-   *
-   * @param change entity containing all change data.
-   * @param patchSet single revision of a {@link Change}.
-   * @return patch list containing the modified files between two commits.
-   * @deprecated use {@link DiffOperations} instead.
-   */
-  @Deprecated
-  PatchList get(Change change, PatchSet patchSet) throws PatchListNotAvailableException;
-
-  /**
-   * Returns the patch list - list of modified files - between two commits.
-   *
-   * @param change entity containing all change data.
-   * @param patchSet single revision of a {@link Change}.
-   * @param parentNum 1-based parent number when new commit used in comparison is a merge commit.
-   * @return patch list containing the modified files between two commits.
-   * @deprecated use {@link DiffOperations} instead.
-   */
-  @Deprecated
-  ObjectId getOldId(Change change, PatchSet patchSet, Integer parentNum)
-      throws PatchListNotAvailableException;
-
   IntraLineDiff getIntraLineDiff(IntraLineDiffKey key, IntraLineDiffArgs args);
 
   DiffSummary getDiffSummary(DiffSummaryKey key, Project.NameKey project)
diff --git a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index a3e9a54..dd2bb47 100644
--- a/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -18,10 +18,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.cache.Cache;
 import com.google.common.util.concurrent.UncheckedExecutionException;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.server.cache.CacheBackend;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -125,27 +122,6 @@
   }
 
   @Override
-  public PatchList get(Change change, PatchSet patchSet) throws PatchListNotAvailableException {
-    return get(change, patchSet, null);
-  }
-
-  @Override
-  public ObjectId getOldId(Change change, PatchSet patchSet, Integer parentNum)
-      throws PatchListNotAvailableException {
-    return get(change, patchSet, parentNum).getOldId();
-  }
-
-  private PatchList get(Change change, PatchSet patchSet, Integer parentNum)
-      throws PatchListNotAvailableException {
-    Project.NameKey project = change.getProject();
-    ObjectId b = patchSet.commitId();
-    if (parentNum != null) {
-      return get(PatchListKey.againstParentNum(parentNum, b, Whitespace.IGNORE_NONE), project);
-    }
-    return get(PatchListKey.againstDefaultBase(b, Whitespace.IGNORE_NONE), project);
-  }
-
-  @Override
   public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key, IntraLineDiffArgs args) {
     if (computeIntraline) {
       try {
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 5998bba..0b08c4f 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -235,10 +235,10 @@
         return null;
       case DELETED:
       case MODIFIED:
-      case REWRITE:
         return entry.getNewName();
       case COPIED:
       case RENAMED:
+      case REWRITE:
       default:
         return entry.getOldName();
     }
diff --git a/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index fbb6559..96b23c8 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -26,20 +26,13 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.metrics.Counter1;
-import com.google.gerrit.metrics.Description;
-import com.google.gerrit.metrics.Field;
-import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LargeObjectException;
-import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchScriptBuilder.IntraLineDiffCalculatorResult;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
@@ -50,23 +43,15 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import org.apache.commons.lang.exception.ExceptionUtils;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
@@ -93,33 +78,10 @@
         CurrentUser currentUser);
   }
 
-  /** These metrics are temporary for launching the new redesigned diff cache. */
-  @Singleton
-  static class Metrics {
-    final Counter1<String> diffs;
-    static final String MATCH = "match";
-    static final String MISMATCH = "mismatch";
-    static final String ERROR = "error";
-
-    @Inject
-    Metrics(MetricMaker metricMaker) {
-      diffs =
-          metricMaker.newCounter(
-              "diff/get_diff/dark_launch",
-              new Description(
-                      "Total number of matching, non-matching, or error in diffs in the old and new diff cache implementations.")
-                  .setRate()
-                  .setUnit("count"),
-              Field.ofString("type", Metadata.Builder::eventType).build());
-    }
-  }
-
   private final GitRepositoryManager repoManager;
   private final PatchSetUtil psUtil;
   private final Provider<PatchScriptBuilder> builderFactory;
   private final PatchListCache patchListCache;
-  private final Metrics metrics;
-  private final ExecutorService executor;
 
   private final String fileName;
   @Nullable private final PatchSet.Id psa;
@@ -137,8 +99,6 @@
 
   private ChangeNotes notes;
 
-  private final boolean runNewDiffCache;
-
   @AssistedInject
   PatchScriptFactory(
       GitRepositoryManager grm,
@@ -149,9 +109,6 @@
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       DiffOperations diffOperations,
-      Metrics metrics,
-      @DiffExecutor ExecutorService executor,
-      @GerritServerConfig Config cfg,
       @Assisted ChangeNotes notes,
       @Assisted String fileName,
       @Assisted("patchSetA") @Nullable PatchSet.Id patchSetA,
@@ -167,8 +124,6 @@
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.diffOperations = diffOperations;
-    this.metrics = metrics;
-    this.executor = executor;
 
     this.fileName = fileName;
     this.psa = patchSetA;
@@ -176,9 +131,6 @@
     this.psb = patchSetB;
     this.diffPrefs = diffPrefs;
     this.currentUser = currentUser;
-
-    this.runNewDiffCache = cfg.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
-
     changeId = patchSetB.changeId();
   }
 
@@ -192,9 +144,6 @@
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       DiffOperations diffOperations,
-      Metrics metrics,
-      @DiffExecutor ExecutorService executor,
-      @GerritServerConfig Config cfg,
       @Assisted ChangeNotes notes,
       @Assisted String fileName,
       @Assisted int parentNum,
@@ -210,8 +159,6 @@
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.diffOperations = diffOperations;
-    this.metrics = metrics;
-    this.executor = executor;
 
     this.fileName = fileName;
     this.psa = null;
@@ -219,9 +166,6 @@
     this.psb = patchSetB;
     this.diffPrefs = diffPrefs;
     this.currentUser = currentUser;
-
-    this.runNewDiffCache = cfg.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
-
     changeId = patchSetB.changeId();
     checkArgument(parentNum > 0, "parentNum must be > 0");
   }
@@ -259,16 +203,7 @@
           }
           bId = edit.get().getEditCommit();
         }
-        if (runNewDiffCache) {
-          PatchScript patchScript = getPatchScriptWithNewDiffCache(git, aId, bId);
-          // TODO(ghareeb): remove the async run. This is temporarily used to keep sanity checking
-          // the results while rolling out the new diff cache.
-          runOldDiffCacheAsyncAndExportMetrics(git, aId, bId, patchScript);
-          return patchScript;
-        }
-        return getPatchScriptWithOldDiffCache(git, aId, bId);
-      } catch (PatchListNotAvailableException e) {
-        throw new NoSuchChangeException(changeId, e);
+        return getPatchScript(git, aId, bId);
       } catch (DiffNotAvailableException e) {
         throw new StorageException(e);
       } catch (IOException e) {
@@ -286,42 +221,7 @@
     }
   }
 
-  private void runOldDiffCacheAsyncAndExportMetrics(
-      Repository git, ObjectId aId, ObjectId bId, PatchScript expected) {
-    @SuppressWarnings("unused")
-    Future<?> possiblyIgnoredError =
-        executor.submit(
-            () -> {
-              try {
-                PatchScript patchScript = getPatchScriptWithOldDiffCache(git, aId, bId);
-                if (areEqualPatchscripts(patchScript, expected)) {
-                  metrics.diffs.increment(Metrics.MATCH);
-                } else {
-                  metrics.diffs.increment(Metrics.MISMATCH);
-                  logger.atWarning().atMostEvery(10, TimeUnit.SECONDS).log(
-                      "Mismatching diff for change %s, old commit ID: %s, new commit ID: %s, file name: %s.",
-                      changeId.toString(), aId, bId, fileName);
-                }
-              } catch (PatchListNotAvailableException | IOException e) {
-                metrics.diffs.increment(Metrics.ERROR);
-                logger.atSevere().atMostEvery(10, TimeUnit.SECONDS).log(
-                    String.format(
-                            "Error computing new diff for change %s, old commit ID: %s, new commit ID: %s.\n",
-                            changeId.toString(), aId, bId)
-                        + ExceptionUtils.getStackTrace(e));
-              }
-            });
-  }
-
-  private PatchScript getPatchScriptWithOldDiffCache(Repository git, ObjectId aId, ObjectId bId)
-      throws IOException, PatchListNotAvailableException {
-    PatchScriptBuilder patchScriptBuilder = newBuilder();
-    PatchList list = listFor(keyFor(aId, bId, diffPrefs.ignoreWhitespace));
-    PatchListEntry content = list.get(fileName);
-    return patchScriptBuilder.toPatchScriptOld(git, list, content);
-  }
-
-  private PatchScript getPatchScriptWithNewDiffCache(Repository git, ObjectId aId, ObjectId bId)
+  private PatchScript getPatchScript(Repository git, ObjectId aId, ObjectId bId)
       throws IOException, DiffNotAvailableException {
     FileDiffOutput fileDiffOutput =
         aId == null
@@ -332,61 +232,6 @@
     return newBuilder().toPatchScriptNew(git, fileDiffOutput);
   }
 
-  /**
-   * The comparison is not exhaustive but is using the most important fields. Comparing all fields
-   * will require some work in {@link PatchScript} to, e.g., convert it to autovalue. This
-   * comparison method shall give a strong signal that both patchscripts are almost identical.
-   */
-  private static boolean areEqualPatchscripts(PatchScript ps1, PatchScript ps2) {
-    boolean equal = true;
-    if (!ps1.getChangeType().equals(ps2.getChangeType())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching change type: old = %s, new = %s.", ps1.getChangeType(), ps2.getChangeType());
-    }
-    if (!ps1.getPatchHeader().equals(ps2.getPatchHeader())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching patch header: old = %s, new = %s.",
-          ps1.getPatchHeader(), ps2.getPatchHeader());
-    }
-    if (!Objects.equals(ps1.getOldName(), ps2.getOldName())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching old name: old = %s, new = %s.", ps1.getOldName(), ps2.getOldName());
-    }
-    if (!Objects.equals(ps1.getNewName(), ps2.getNewName())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching new name: old = %s, new = %s.", ps1.getNewName(), ps2.getNewName());
-    }
-    if (!ps1.getEdits().containsAll(ps2.getEdits())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching edits: old = %s, new = %s.", ps1.getEdits(), ps2.getEdits());
-    }
-    if (!ps2.getEdits().containsAll(ps1.getEdits())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching edits: old = %s, new = %s.", ps1.getEdits(), ps2.getEdits());
-    }
-    if (!ps1.getEditsDueToRebase().equals(ps2.getEditsDueToRebase())) {
-      equal = false;
-      logger.atWarning().log(
-          "Mismatching edits due to rebase: old = %s, new = %s.",
-          ps1.getEditsDueToRebase(), ps2.getEditsDueToRebase());
-    }
-    if (!ps1.getA().equals(ps2.getA())) {
-      equal = false;
-      logger.atWarning().log("Mismatching sparse file content in old commit.");
-    }
-    if (!ps1.getB().equals(ps2.getB())) {
-      equal = false;
-      logger.atWarning().log("Mismatching sparse file content in new commit.");
-    }
-    return equal;
-  }
-
   private Optional<ObjectId> getAId() {
     if (psa == null) {
       return Optional.empty();
@@ -404,17 +249,6 @@
     return Optional.of(getCommitId(psb));
   }
 
-  private PatchListKey keyFor(ObjectId aId, ObjectId bId, Whitespace whitespace) {
-    if (parentNum == 0) {
-      return PatchListKey.againstCommit(aId, bId, whitespace);
-    }
-    return PatchListKey.againstParentNum(parentNum, bId, whitespace);
-  }
-
-  private PatchList listFor(PatchListKey key) throws PatchListNotAvailableException {
-    return patchListCache.get(key, notes.getProjectName());
-  }
-
   private PatchScriptBuilder newBuilder() {
     final PatchScriptBuilder b = builderFactory.get();
     b.setDiffPrefs(diffPrefs);
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index 62387ee..572d73d 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.LabelType;
@@ -51,7 +52,6 @@
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
@@ -128,11 +128,39 @@
     }
 
     diff.append("The change was submitted with unreviewed changes in the following files:\n\n");
-
-    for (FileDiffOutput fileDiff : modifiedFilesList) {
-      diff.append(
-          getDiffForFile(
-              notes, currentPatchset.id(), latestApprovedPatchsetId, fileDiff, currentUser));
+    TemporaryBuffer.Heap buffer =
+        new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxCumulativeSize), maxCumulativeSize);
+    try (Repository repository = repositoryManager.openRepository(notes.getProjectName());
+        DiffFormatter formatter = new DiffFormatter(buffer)) {
+      formatter.setRepository(repository);
+      formatter.setDetectRenames(true);
+      boolean isDiffTooLarge = false;
+      List<String> formatterResult = null;
+      try {
+        formatter.format(
+            modifiedFilesList.get(0).oldCommitId(), modifiedFilesList.get(0).newCommitId());
+        // This returns the diff for all the files.
+        formatterResult =
+            Arrays.stream(RawParseUtils.decode(buffer.toByteArray()).split("\n"))
+                .collect(Collectors.toList());
+      } catch (IOException e) {
+        if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+          isDiffTooLarge = true;
+        } else {
+          throw e;
+        }
+      }
+      for (FileDiffOutput fileDiff : modifiedFilesList) {
+        diff.append(
+            getDiffForFile(
+                notes,
+                currentPatchset.id(),
+                latestApprovedPatchsetId,
+                fileDiff,
+                currentUser,
+                formatterResult,
+                isDiffTooLarge));
+      }
     }
     return diff.toString();
   }
@@ -142,7 +170,9 @@
       PatchSet.Id currentPatchsetId,
       PatchSet.Id latestApprovedPatchsetId,
       FileDiffOutput fileDiffOutput,
-      CurrentUser currentUser)
+      CurrentUser currentUser,
+      @Nullable List<String> formatterResult,
+      boolean isDiffTooLarge)
       throws AuthException, InvalidChangeOperationException, IOException,
           PermissionBackendException {
     StringBuilder diff =
@@ -167,6 +197,7 @@
             currentUser);
     PatchScript patchScript = null;
     try {
+      // TODO(paiking): we can get rid of this call to optimize by checking the diff for renames.
       patchScript = patchScriptFactory.call();
     } catch (LargeObjectException exception) {
       diff.append("The file content is too large for showing the full diff. \n\n");
@@ -178,7 +209,15 @@
               "The file %s was renamed to %s\n",
               fileDiffOutput.oldPath().get(), fileDiffOutput.newPath().get()));
     }
-    diff.append(getUnifiedDiff(patchScript, notes));
+    if (isDiffTooLarge) {
+      diff.append("The diff is too large to show. Please review the diff.");
+      diff.append("\n```\n");
+      return diff.toString();
+    }
+    // This filters only the file we need.
+    // TODO(paiking): we can make this more efficient by mapping the files to their respective
+    //  diffs prior to this method, such that we need to go over the diff only once.
+    diff.append(getDiffForFile(patchScript, formatterResult));
     // This line (and the ``` above) are useful for formatting in the web UI.
     diff.append("\n```\n");
     return diff.toString();
@@ -188,63 +227,42 @@
    * Show patch set as unified difference for a specific file. We on purpose are not using {@link
    * DiffInfoCreator} since we'd like to get the original git/JGit style diff.
    */
-  public String getUnifiedDiff(PatchScript patchScript, ChangeNotes changeNotes)
-      throws IOException {
-    TemporaryBuffer.Heap buf =
-        new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxCumulativeSize), maxCumulativeSize);
-    try (DiffFormatter fmt = new DiffFormatter(buf);
-        // TODO(paiking): ensure we open the repository only once by opening it in the calling
-        //  method.
-        Repository git = repositoryManager.openRepository(changeNotes.getProjectName())) {
-      fmt.setRepository(git);
-      fmt.setDetectRenames(true);
-      fmt.format(
-          ObjectId.fromString(patchScript.getFileInfoA().commitId),
-          ObjectId.fromString(patchScript.getFileInfoB().commitId));
-      List<String> formatterResult =
-          Arrays.stream(RawParseUtils.decode(buf.toByteArray()).split("\n"))
-              .collect(Collectors.toList());
-      // only return information about the current file, and not about files that are not
-      // relevant. DiffFormatter returns other potential files because of rebases, which we can
-      // ignore.
-      List<String> modifiedFormatterResult = new ArrayList<>();
-      int indexOfFormatterResult = 0;
-      while (formatterResult.size() > indexOfFormatterResult
-          && !formatterResult
-              .get(indexOfFormatterResult)
-              .equals(
-                  String.format(
-                      "diff --git a/%s b/%s",
-                      patchScript.getOldName() != null
-                          ? patchScript.getOldName()
-                          : patchScript.getNewName(),
-                      patchScript.getNewName()))) {
-        indexOfFormatterResult++;
-      }
-      // remove non user friendly information.
-      while (formatterResult.size() > indexOfFormatterResult
-          && !formatterResult.get(indexOfFormatterResult).startsWith("@@")) {
-        indexOfFormatterResult++;
-      }
-      for (; indexOfFormatterResult < formatterResult.size(); indexOfFormatterResult++) {
-        if (formatterResult.get(indexOfFormatterResult).startsWith("diff --git")) {
-          break;
-        }
-        modifiedFormatterResult.add(formatterResult.get(indexOfFormatterResult));
-      }
-      if (modifiedFormatterResult.size() == 0) {
-        // This happens for diffs that are just renames, but we already account for renames.
-        return "";
-      }
-      return modifiedFormatterResult.stream()
-          .filter(s -> !s.equals("\\ No newline at end of file"))
-          .collect(Collectors.joining("\n"));
-    } catch (IOException e) {
-      if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
-        return "The diff is too large to show. Please review the diff.";
-      }
-      throw e;
+  public String getDiffForFile(PatchScript patchScript, List<String> formatterResult) {
+    // only return information about the current file, and not about files that are not
+    // relevant. DiffFormatter returns other potential files because of rebases, which we can
+    // ignore.
+    List<String> modifiedFormatterResult = new ArrayList<>();
+    int indexOfFormatterResult = 0;
+    while (formatterResult.size() > indexOfFormatterResult
+        && !formatterResult
+            .get(indexOfFormatterResult)
+            .equals(
+                String.format(
+                    "diff --git a/%s b/%s",
+                    patchScript.getOldName() != null
+                        ? patchScript.getOldName()
+                        : patchScript.getNewName(),
+                    patchScript.getNewName()))) {
+      indexOfFormatterResult++;
     }
+    // remove non user friendly information.
+    while (formatterResult.size() > indexOfFormatterResult
+        && !formatterResult.get(indexOfFormatterResult).startsWith("@@")) {
+      indexOfFormatterResult++;
+    }
+    for (; indexOfFormatterResult < formatterResult.size(); indexOfFormatterResult++) {
+      if (formatterResult.get(indexOfFormatterResult).startsWith("diff --git")) {
+        break;
+      }
+      modifiedFormatterResult.add(formatterResult.get(indexOfFormatterResult));
+    }
+    if (modifiedFormatterResult.size() == 0) {
+      // This happens for diffs that are just renames, but we already account for renames.
+      return "";
+    }
+    return modifiedFormatterResult.stream()
+        .filter(s -> !s.equals("\\ No newline at end of file"))
+        .collect(Collectors.joining("\n"));
   }
 
   private DiffPreferencesInfo createDefaultDiffPreferencesInfo() {
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java
index 56f49c9..76d1710 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCache.java
@@ -35,9 +35,10 @@
 public interface ModifiedFilesCache {
 
   /**
+   * Returns the list of {@link ModifiedFile}s between the 2 git commits identified by the key
+   *
    * @param key used to identify two git commits and contains other attributes to control the diff
    *     calculation.
-   * @return the list of {@link ModifiedFile}s between the 2 git commits identified by the key.
    * @throws DiffNotAvailableException the supplied commits IDs of the key do no exist, are not IDs
    *     of a commit, or an exception occurred while reading a pack file.
    */
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index e4fd728..460c2e2 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -86,7 +86,7 @@
             .valueSerializer(GitModifiedFilesCacheImpl.ValueSerializer.INSTANCE)
             .maximumWeight(10 << 20)
             .weigher(ModifiedFilesWeigher.class)
-            .version(2)
+            .version(3)
             .loader(ModifiedFilesLoader.class);
       }
     };
@@ -208,27 +208,22 @@
     }
 
     /**
-     * Return the {@code modifiedFiles} input list while merging {@link ChangeType#ADDED} and {@link
-     * ChangeType#DELETED} entries for the same file into a single {@link ChangeType#REWRITE} entry.
+     * Return the {@code modifiedFiles} input list while merging rewritten entries.
      *
-     * <p>Background: In some cases, JGit returns two diff entries (ADDED + DELETED) for the same
-     * file path. This happens e.g. when a file's mode is changed between patchsets, for example
-     * converting a symlink file to a regular file. We identify this case and return a single
-     * modified file with changeType = {@link ChangeType#REWRITE}.
+     * <p>Background: In some cases, JGit returns two diff entries (ADDED/DELETED, RENAMED/DELETED,
+     * etc...) for the same file path. This happens e.g. when a file's mode is changed between
+     * patchsets, for example converting a symlink file to a regular file. We identify this case and
+     * return a single modified file with changeType = {@link ChangeType#REWRITE}.
      */
     private static List<ModifiedFile> mergeRewrittenEntries(List<ModifiedFile> modifiedFiles) {
       List<ModifiedFile> result = new ArrayList<>();
-
-      // Handle ADDED and DELETED entries separately.
       ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
       modifiedFiles.stream()
-          .filter(ModifiedFilesLoader::isAddedOrDeleted)
           .forEach(
               f -> {
-                if (f.oldPath().isPresent()) {
+                if (f.changeType() == ChangeType.DELETED) {
                   byPath.get(f.oldPath().get()).add(f);
-                }
-                if (f.newPath().isPresent()) {
+                } else {
                   byPath.get(f.newPath().get()).add(f);
                 }
               });
@@ -236,31 +231,12 @@
         List<ModifiedFile> entries = byPath.get(path);
         if (entries.size() == 1) {
           result.add(entries.get(0));
-        } else if (entries.size() == 2) {
-          result.add(getAddedEntry(entries).toBuilder().changeType(ChangeType.REWRITE).build());
         } else {
-          // JGit error. Not expected to happen.
-          logger.atWarning().log(
-              "Found %d ADDED and DELETED entries for the same file path: %s."
-                  + " Adding the first entry only to the result.",
-              entries.size(), entries);
-          result.add(entries.get(0));
+          // More than one. Return a single REWRITE entry.
+          result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build());
         }
       }
-
-      // Add the remaining non ADDED/DELETED entries to the result
-      modifiedFiles.stream().filter(f -> !isAddedOrDeleted(f)).forEach(result::add);
       return result;
     }
-
-    private static boolean isAddedOrDeleted(ModifiedFile f) {
-      return f.changeType() == ChangeType.ADDED || f.changeType() == ChangeType.DELETED;
-    }
-
-    private static ModifiedFile getAddedEntry(List<ModifiedFile> modifiedFiles) {
-      return modifiedFiles.get(0).changeType() == ChangeType.ADDED
-          ? modifiedFiles.get(0)
-          : modifiedFiles.get(1);
-    }
   }
 }
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
index 2ac3f5e..4a406c8 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheKey.java
@@ -32,10 +32,10 @@
   /** A specific git project / repository. */
   public abstract Project.NameKey project();
 
-  /** @return the old commit ID used in the git tree diff */
+  /** Returns the old commit ID used in the git tree diff */
   public abstract ObjectId aCommit();
 
-  /** @return the new commit ID used in the git tree diff */
+  /** Returns the new commit ID used in the git tree diff */
   public abstract ObjectId bCommit();
 
   /**
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
index a67f221..92c3b39 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffCacheImpl.java
@@ -97,7 +97,7 @@
         persist(DIFF, FileDiffCacheKey.class, FileDiffOutput.class)
             .maximumWeight(10 << 20)
             .weigher(FileDiffWeigher.class)
-            .version(7)
+            .version(8)
             .keySerializer(FileDiffCacheKey.Serializer.INSTANCE)
             .valueSerializer(FileDiffOutput.Serializer.INSTANCE)
             .loader(FileDiffLoader.class);
diff --git a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
index 3c6d746..242c1a4 100644
--- a/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
+++ b/java/com/google/gerrit/server/patch/filediff/FileDiffOutput.java
@@ -37,7 +37,10 @@
 public abstract class FileDiffOutput implements Serializable {
   private static final long serialVersionUID = 1L;
 
-  /** The 20 bytes SHA-1 object ID of the old git commit used in the diff. */
+  /**
+   * The 20 bytes SHA-1 object ID of the old git commit used in the diff, or {@link
+   * ObjectId#zeroId()} if {@link #newCommitId()} was a root commit.
+   */
   public abstract ObjectId oldCommitId();
 
   /** The 20 bytes SHA-1 object ID of the new git commit used in the diff. */
@@ -130,9 +133,16 @@
         .build();
   }
 
+  /**
+   * Create a negative file diff. We use this to cache negative diffs for entries that result in
+   * timeouts.
+   */
   public static FileDiffOutput createNegative(
       String filePath, ObjectId oldCommitId, ObjectId newCommitId) {
-    return empty(filePath, oldCommitId, newCommitId).toBuilder().build();
+    return empty(filePath, oldCommitId, newCommitId)
+        .toBuilder()
+        .negative(Optional.of(true))
+        .build();
   }
 
   /** Returns true if this entity represents an unchanged file between two commits. */
diff --git a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java b/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
index 017e276..031f3db 100644
--- a/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
+++ b/java/com/google/gerrit/server/patch/filediff/PatchListLoader.java
@@ -35,6 +35,9 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -52,6 +55,7 @@
 import com.google.gerrit.server.patch.Text;
 import com.google.gerrit.server.patch.filediff.EditTransformer.ContextAwareEdit;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -99,12 +103,30 @@
     PatchListLoader create(PatchListKey key, Project.NameKey project);
   }
 
+  @Singleton
+  static class Metrics {
+    final Counter0 timeouts;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      // TODO(ghareeb): Remove this metric from documentation once this class is deprecated.
+      timeouts =
+          metricMaker.newCounter(
+              "caches/diff/legacy/timeouts",
+              new Description(
+                      "Total number of git file diff computations that resulted in timeouts.")
+                  .setRate()
+                  .setUnit("count"));
+    }
+  }
+
   private final GitRepositoryManager repoManager;
   private final PatchListCache patchListCache;
   private final ThreeWayMergeStrategy mergeStrategy;
   private final ExecutorService diffExecutor;
   private final AutoMerger autoMerger;
   private final PatchListKey key;
+  private final Metrics metrics;
   private final Project.NameKey project;
   private final long timeoutMillis;
 
@@ -115,16 +137,18 @@
       @GerritServerConfig Config cfg,
       @DiffExecutor ExecutorService de,
       AutoMerger am,
+      Metrics metrics,
       @Assisted PatchListKey k,
       @Assisted Project.NameKey p) {
-    repoManager = mgr;
-    patchListCache = plc;
-    mergeStrategy = MergeUtil.getMergeStrategy(cfg);
-    diffExecutor = de;
-    autoMerger = am;
-    key = k;
-    project = p;
-    timeoutMillis =
+    this.repoManager = mgr;
+    this.patchListCache = plc;
+    this.mergeStrategy = MergeUtil.getMergeStrategy(cfg);
+    this.diffExecutor = de;
+    this.autoMerger = am;
+    this.metrics = metrics;
+    this.key = k;
+    this.project = p;
+    this.timeoutMillis =
         ConfigUtil.getTimeUnit(
             cfg,
             "cache",
@@ -529,6 +553,7 @@
     try {
       return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | TimeoutException e) {
+      metrics.timeouts.increment();
       logger.atWarning().log(
           "%s ms timeout reached for Diff loader in project %s"
               + " on commit %s on path %s comparing %s..%s",
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
index a502a46..2f23c8c 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiff.java
@@ -34,6 +34,7 @@
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.patch.FileHeader;
 
 /**
@@ -120,7 +121,10 @@
   /** The file name at the new git tree identified by {@link #newId()} */
   public abstract Optional<String> newPath();
 
-  /** The 20 bytes SHA-1 object ID of the old git tree of the diff. */
+  /**
+   * The 20 bytes SHA-1 object ID of the old git tree of the diff, or {@link ObjectId#zeroId()} if
+   * {@link #newId()} was a root git tree (i.e. has no parents).
+   */
   public abstract AbbreviatedObjectId oldId();
 
   /** The 20 bytes SHA-1 object ID of the new git tree of the diff. */
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index 77b8938..f293a64 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -31,6 +31,9 @@
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -47,6 +50,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -91,6 +95,22 @@
     };
   }
 
+  @Singleton
+  static class Metrics {
+    final Counter0 timeouts;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      timeouts =
+          metricMaker.newCounter(
+              "caches/diff/timeouts",
+              new Description(
+                      "Total number of git file diff computations that resulted in timeouts.")
+                  .setRate()
+                  .setUnit("count"));
+    }
+  }
+
   /** Enum for the supported diff algorithms for the file diff computation. */
   public enum DiffAlgorithm {
     HISTOGRAM_WITH_FALLBACK_MYERS,
@@ -110,9 +130,6 @@
 
   private final LoadingCache<GitFileDiffCacheKey, GitFileDiff> cache;
 
-  private static final ImmutableSet<Patch.ChangeType> ADDED_AND_DELETED =
-      ImmutableSet.of(Patch.ChangeType.ADDED, Patch.ChangeType.DELETED);
-
   @Inject
   public GitFileDiffCacheImpl(
       @Named(GIT_DIFF) LoadingCache<GitFileDiffCacheKey, GitFileDiff> cache) {
@@ -152,12 +169,14 @@
     private final GitRepositoryManager repoManager;
     private final ExecutorService diffExecutor;
     private final long timeoutMillis;
+    private final Metrics metrics;
 
     @Inject
     public Loader(
         @GerritServerConfig Config cfg,
         GitRepositoryManager repoManager,
-        @DiffExecutor ExecutorService de) {
+        @DiffExecutor ExecutorService de,
+        Metrics metrics) {
       this.repoManager = repoManager;
       this.diffExecutor = de;
       this.timeoutMillis =
@@ -168,6 +187,7 @@
               "timeout",
               TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
               TimeUnit.MILLISECONDS);
+      this.metrics = metrics;
     }
 
     @Override
@@ -346,6 +366,7 @@
         return GitFileDiff.create(diffEntry, fileHeader);
       } catch (InterruptedException | TimeoutException e) {
         // If timeout happens, create a negative result
+        metrics.timeouts.increment();
         return GitFileDiff.createNegative(
             AbbreviatedObjectId.fromObjectId(key.oldTree()),
             AbbreviatedObjectId.fromObjectId(key.newTree()),
@@ -360,12 +381,13 @@
   }
 
   /**
-   * Create a single {@link GitFileDiff} with {@link Patch.ChangeType} equals {@link
-   * Patch.ChangeType#REWRITE}, assuming the input list contains two entries with types {@link
-   * Patch.ChangeType#ADDED} and {@link Patch.ChangeType#DELETED}.
+   * Create a single {@link GitFileDiff} with {@link com.google.gerrit.entities.Patch.ChangeType}
+   * equals {@link com.google.gerrit.entities.Patch.ChangeType#REWRITE}, assuming the input list
+   * contains two entries.
    *
    * @param gitDiffs input list of exactly two {@link GitFileDiff} for same file path.
-   * @return a single {@link GitFileDiff} with change type equals {@link Patch.ChangeType#REWRITE}.
+   * @return a single {@link GitFileDiff} with change type equals {@link
+   *     com.google.gerrit.entities.Patch.ChangeType#REWRITE}.
    * @throws DiffNotAvailableException if input list contains git diffs with change types other than
    *     {ADDED, DELETED}. This is a JGit error.
    */
@@ -377,19 +399,9 @@
               "JGit error: found %d dff entries for same file path %s",
               gitDiffs.size(), gitDiffs.get(0).getDefaultPath()));
     }
-    if (!ImmutableSet.of(gitDiffs.get(0).changeType(), gitDiffs.get(1).changeType())
-        .equals(ADDED_AND_DELETED)) {
-      // This is an illegal state. JGit is not supposed to return this, so we throw an exception.
-      throw new DiffNotAvailableException(
-          String.format(
-              "JGit error: unexpected change types %s and %s for same file path %s",
-              gitDiffs.get(0).changeType(),
-              gitDiffs.get(1).changeType(),
-              gitDiffs.get(0).getDefaultPath()));
-    }
-    GitFileDiff addedEntry =
-        gitDiffs.get(0).changeType() == Patch.ChangeType.ADDED ? gitDiffs.get(0) : gitDiffs.get(1);
-    return addedEntry.toBuilder().changeType(Patch.ChangeType.REWRITE).build();
+    // Convert the first entry (prioritized according to change type enum order) to REWRITE
+    gitDiffs.sort(Comparator.comparingInt(o -> o.changeType().ordinal()));
+    return gitDiffs.get(0).toBuilder().changeType(Patch.ChangeType.REWRITE).build();
   }
 
   /** An entity representing the options affecting the diff computation. */
diff --git a/java/com/google/gerrit/server/permissions/LabelPermission.java b/java/com/google/gerrit/server/permissions/LabelPermission.java
index 268570c..c266caa 100644
--- a/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -71,12 +71,12 @@
     this.name = LabelType.checkName(name);
   }
 
-  /** @return {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+  /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
   public ForUser forUser() {
     return forUser;
   }
 
-  /** @return name of the label, e.g. {@code "Code-Review"}. */
+  /** Returns name of the label, e.g. {@code "Code-Review"}. */
   public String label() {
     return name;
   }
@@ -199,17 +199,17 @@
       this.label = requireNonNull(label, "LabelVote");
     }
 
-    /** @return {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+    /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
     public ForUser forUser() {
       return forUser;
     }
 
-    /** @return name of the label, e.g. {@code "Code-Review"}. */
+    /** Returns name of the label, e.g. {@code "Code-Review"}. */
     public String label() {
       return label.label();
     }
 
-    /** @return specific value of the label, e.g. 1 or 2. */
+    /** Returns specific value of the label, e.g. 1 or 2. */
     public short value() {
       return label.value();
     }
diff --git a/java/com/google/gerrit/server/permissions/PermissionCollection.java b/java/com/google/gerrit/server/permissions/PermissionCollection.java
index ddba52b..4b8db1c 100644
--- a/java/com/google/gerrit/server/permissions/PermissionCollection.java
+++ b/java/com/google/gerrit/server/permissions/PermissionCollection.java
@@ -277,8 +277,8 @@
   }
 
   /**
-   * @return true if a "${username}" pattern might need to be expanded to build this collection,
-   *     making the results user specific.
+   * Returns true if a "${username}" pattern might need to be expanded to build this collection,
+   * making the results user specific.
    */
   public boolean isUserSpecific() {
     return perUser;
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index a92fde0..1203049 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -154,8 +154,8 @@
   }
 
   /**
-   * @return {@code Capable.OK} if the user can upload to at least one reference. Does not check
-   *     Contributor Agreements.
+   * Returns {@code Capable.OK} if the user can upload to at least one reference. Does not check
+   * Contributor Agreements.
    */
   boolean canPushToAtLeastOneRef() {
     return canPerformOnAnyRef(Permission.PUSH)
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index f800207..6b51335 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -135,19 +135,19 @@
     return hasReadPermissionOnRef;
   }
 
-  /** @return true if this user can add a new patch set to this ref */
+  /** Returns true if this user can add a new patch set to this ref */
   boolean canAddPatchSet() {
     return projectControl
         .controlForRef(MagicBranch.NEW_CHANGE + refName)
         .canPerform(Permission.ADD_PATCH_SET);
   }
 
-  /** @return true if this user can rebase changes on this ref */
+  /** Returns true if this user can rebase changes on this ref */
   boolean canRebase() {
     return canPerform(Permission.REBASE);
   }
 
-  /** @return true if this user can submit patch sets to this ref */
+  /** Returns true if this user can submit patch sets to this ref */
   boolean canSubmit(boolean isChangeOwner) {
     if (RefNames.REFS_CONFIG.equals(refName)) {
       // Always allow project owners to submit configuration changes.
@@ -160,12 +160,12 @@
     return canPerform(Permission.SUBMIT, isChangeOwner, false);
   }
 
-  /** @return true if this user can force edit topic names. */
+  /** Returns true if this user can force edit topic names. */
   boolean canForceEditTopicName() {
     return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
   }
 
-  /** @return true if this user can delete changes. */
+  /** Returns true if this user can delete changes. */
   boolean canDeleteChanges(boolean isChangeOwner) {
     return canPerform(Permission.DELETE_CHANGES)
         || (isChangeOwner && canPerform(Permission.DELETE_OWN_CHANGES, isChangeOwner, false));
@@ -201,12 +201,12 @@
     return canPerform(Permission.REVERT);
   }
 
-  /** @return true if this user can submit merge patch sets to this ref */
+  /** Returns true if this user can submit merge patch sets to this ref */
   private boolean canUploadMerges() {
     return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE);
   }
 
-  /** @return true if the user can update the reference as a fast-forward. */
+  /** Returns true if the user can update the reference as a fast-forward. */
   private boolean canUpdate() {
     if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
       // Pushing requires being at least project owner, in addition to push.
@@ -225,7 +225,7 @@
     return canPerform(Permission.PUSH);
   }
 
-  /** @return true if the user can rewind (force push) the reference. */
+  /** Returns true if the user can rewind (force push) the reference. */
   private boolean canForceUpdate() {
     if (canPushWithForce()) {
       return true;
@@ -281,7 +281,7 @@
     }
   }
 
-  /** @return true if this user can forge the author line in a commit. */
+  /** Returns true if this user can forge the author line in a commit. */
   private boolean canForgeAuthor() {
     if (canForgeAuthor == null) {
       canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
@@ -289,7 +289,7 @@
     return canForgeAuthor;
   }
 
-  /** @return true if this user can forge the committer line in a commit. */
+  /** Returns true if this user can forge the committer line in a commit. */
   private boolean canForgeCommitter() {
     if (canForgeCommitter == null) {
       canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
@@ -297,7 +297,7 @@
     return canForgeCommitter;
   }
 
-  /** @return true if this user can forge the server on the committer line. */
+  /** Returns true if this user can forge the server on the committer line. */
   private boolean canForgeGerritServerIdentity() {
     return canPerform(Permission.FORGE_SERVER);
   }
@@ -364,7 +364,9 @@
     }
 
     return new PermissionRange(
-        permissionName, Math.max(voteMin, blockAllowMin), Math.min(voteMax, blockAllowMax));
+        permissionName,
+        /* min= */ Math.max(voteMin, blockAllowMin),
+        /* max= */ Math.min(voteMax, blockAllowMax));
   }
 
   private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) {
@@ -560,7 +562,8 @@
             break;
           case FORGE_COMMITTER:
             pde.setAdvice(
-                "You need 'Forge Committer' rights to push commits with another user as committer.");
+                "You need 'Forge Committer' rights to push commits with another user as"
+                    + " committer.");
             break;
           case FORGE_SERVER:
             pde.setAdvice(
diff --git a/java/com/google/gerrit/server/permissions/SectionSortCache.java b/java/com/google/gerrit/server/permissions/SectionSortCache.java
index d800782..e64f8b6 100644
--- a/java/com/google/gerrit/server/permissions/SectionSortCache.java
+++ b/java/com/google/gerrit/server/permissions/SectionSortCache.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
@@ -126,22 +127,22 @@
 
     public abstract List<String> patterns();
 
-    public abstract int cachedHashCode();
-
     static EntryKey create(String refName, List<AccessSection> sections) {
-      int hc = refName.hashCode();
       List<String> patterns = new ArrayList<>(sections.size());
       for (AccessSection s : sections) {
-        String n = s.getName();
-        patterns.add(n);
-        hc = hc * 31 + n.hashCode();
+        patterns.add(s.getName());
       }
-      return new AutoValue_SectionSortCache_EntryKey(refName, ImmutableList.copyOf(patterns), hc);
+      return new AutoValue_SectionSortCache_EntryKey(refName, ImmutableList.copyOf(patterns));
     }
 
+    @Memoized
     @Override
-    public final int hashCode() {
-      return cachedHashCode();
+    public int hashCode() {
+      int hc = ref().hashCode();
+      for (String n : patterns()) {
+        hc = hc * 31 + n.hashCode();
+      }
+      return hc;
     }
   }
 
diff --git a/java/com/google/gerrit/server/plugincontext/PluginContext.java b/java/com/google/gerrit/server/plugincontext/PluginContext.java
index 3c30745..a5fad56 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginContext.java
@@ -54,7 +54,7 @@
  * <p>A plugin context can be manually opened by invoking the newTrace methods. This should only be
  * needed if an extension throws multiple exceptions that need to be handled:
  *
- * <pre>
+ * <pre>{@code
  * public interface Foo {
  *   void doFoo() throws Exception1, Exception2, Exception3;
  * }
@@ -66,7 +66,7 @@
  *     fooExtension.get().doFoo();
  *   }
  * }
- * </pre>
+ * }</pre>
  *
  * <p>This class hosts static methods with generic functionality to invoke plugin extensions with a
  * trace context that are commonly used by {@link PluginItemContext}, {@link PluginSetContext} and
@@ -121,11 +121,17 @@
     @Inject
     PluginMetrics(MetricMaker metricMaker) {
       Field<String> pluginNameField =
-          Field.ofString("plugin_name", Metadata.Builder::pluginName).build();
+          Field.ofString("plugin_name", Metadata.Builder::pluginName)
+              .description("The name of the plugin.")
+              .build();
       Field<String> classNameField =
-          Field.ofString("class_name", Metadata.Builder::className).build();
+          Field.ofString("class_name", Metadata.Builder::className)
+              .description("The class of the plugin that was invoked.")
+              .build();
       Field<String> exportValueField =
-          Field.ofString("export_value", Metadata.Builder::exportValue).build();
+          Field.ofString("export_value", Metadata.Builder::exportValue)
+              .description("The export name under which the invoked class is registered.")
+              .build();
 
       this.latency =
           metricMaker.newTimer(
diff --git a/java/com/google/gerrit/server/plugincontext/PluginItemContext.java b/java/com/google/gerrit/server/plugincontext/PluginItemContext.java
index 421b3ad..e88a6fe 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginItemContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginItemContext.java
@@ -40,46 +40,46 @@
  *
  * <p>Example if all exceptions should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * fooPluginItemContext.run(foo -> foo.doFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if all exceptions, but one, should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * try {
  *   fooPluginItemContext.run(foo -> foo.doFoo(), MyException.class);
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Object result = fooPluginItemContext.call(foo -> foo.getFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values and a single exception should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Object result;
  * try {
  *   result = fooPluginItemContext.call(foo -> foo.getFoo(), MyException.class);
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if several exceptions should be handled:
  *
- * <pre>
+ * <pre>{@code
  * try (TraceContext traceContext = PluginContext.newTrace(fooDynamicItem.getEntry())) {
  *   fooDynamicItem.get().doFoo();
  * } catch (MyException1 | MyException2 | MyException3 e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  */
 public class PluginItemContext<T> {
   @Nullable private final DynamicItem<T> dynamicItem;
diff --git a/java/com/google/gerrit/server/plugincontext/PluginMapContext.java b/java/com/google/gerrit/server/plugincontext/PluginMapContext.java
index b02ad27..fb50cd5 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginMapContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginMapContext.java
@@ -33,15 +33,15 @@
  *
  * <p>Example if all exceptions should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * Map<String, Object> results = new HashMap<>();
  * fooPluginMapContext.runEach(
  *     extension -> results.put(extension.getExportName(), extension.get().getFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if all exceptions, but one, should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * Map<String, Object> results = new HashMap<>();
  * try {
  *   fooPluginMapContext.runEach(
@@ -50,22 +50,22 @@
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Map<String, Object> results = new HashMap<>();
  * for (PluginMapEntryContext<Foo> c : fooPluginMapContext) {
  *   if (c.call(extension -> extension.get().handles(x))) {
  *     c.run(extension -> results.put(extension.getExportName(), extension.get().getFoo());
  *   }
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values and a single exception should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Map<String, Object> results = new HashMap<>();
  * try {
  *   for (PluginMapEntryContext<Foo> c : fooPluginMapContext) {
@@ -77,11 +77,11 @@
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if several exceptions should be handled:
  *
- * <pre>
+ * <pre>{@code
  * for (Extension<Foo> fooExtension : fooDynamicMap) {
  *   try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) {
  *     fooExtension.get().doFoo();
@@ -89,7 +89,7 @@
  *     // handle the exception
  *   }
  * }
- * </pre>
+ * }</pre>
  */
 public class PluginMapContext<T> implements Iterable<PluginMapEntryContext<T>> {
   private final DynamicMap<T> dynamicMap;
diff --git a/java/com/google/gerrit/server/plugincontext/PluginMapEntryContext.java b/java/com/google/gerrit/server/plugincontext/PluginMapEntryContext.java
index 68589cf..27181cb 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginMapEntryContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginMapEntryContext.java
@@ -35,15 +35,15 @@
  *
  * <p>The call* methods execute the extension and deliver a result back to the caller.
  *
- * <pre>
+ * <pre>{@code
  * Map<String, Object> results = new HashMap<>();
  * fooPluginMapEntryContext.run(
  *     extension -> results.put(extension.getExportName(), extension.get().getFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if all exceptions, but one, should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * Map<String, Object> results = new HashMap<>();
  * try {
  *   fooPluginMapEntryContext.run(
@@ -52,28 +52,28 @@
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Object result = fooPluginMapEntryContext.call(extension -> extension.get().getFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values and a single exception should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Object result;
  * try {
  *   result = fooPluginMapEntryContext.call(extension -> extension.get().getFoo(), MyException.class);
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if several exceptions should be handled:
  *
- * <pre>
+ * <pre>{@code
  * for (Extension<Foo> fooExtension : fooDynamicMap) {
  *   try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) {
  *     fooExtension.get().doFoo();
@@ -81,7 +81,7 @@
  *     // handle the exception
  *   }
  * }
- * </pre>
+ * }</pre>
  */
 public class PluginMapEntryContext<T> {
   private final Extension<T> extension;
diff --git a/java/com/google/gerrit/server/plugincontext/PluginSetContext.java b/java/com/google/gerrit/server/plugincontext/PluginSetContext.java
index b64cfeb..43c9552 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginSetContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginSetContext.java
@@ -34,33 +34,33 @@
  *
  * <p>Example if all exceptions should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * fooPluginSetContext.runEach(foo -> foo.doFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if all exceptions, but one, should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * try {
  *   fooPluginSetContext.runEach(foo -> foo.doFoo(), MyException.class);
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values should be handled:
  *
- * <pre>
+ * <pre>{@code
  * for (PluginSetEntryContext<Foo> c : fooPluginSetContext) {
  *   if (c.call(foo -> foo.handles(x))) {
  *     c.run(foo -> foo.doFoo());
  *   }
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values and a single exception should be handled:
  *
- * <pre>
+ * <pre>{@code
  * try {
  *   for (PluginSetEntryContext<Foo> c : fooPluginSetContext) {
  *     if (c.call(foo -> foo.handles(x), MyException.class)) {
@@ -70,11 +70,11 @@
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if several exceptions should be handled:
  *
- * <pre>
+ * <pre>{@code
  * for (Extension<Foo> fooExtension : fooDynamicSet.entries()) {
  *   try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) {
  *     fooExtension.get().doFoo();
@@ -82,7 +82,7 @@
  *     // handle the exception
  *   }
  * }
- * </pre>
+ * }</pre>
  */
 public class PluginSetContext<T> implements Iterable<PluginSetEntryContext<T>> {
   private final DynamicSet<T> dynamicSet;
diff --git a/java/com/google/gerrit/server/plugincontext/PluginSetEntryContext.java b/java/com/google/gerrit/server/plugincontext/PluginSetEntryContext.java
index 2268c07..be97b52 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginSetEntryContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginSetEntryContext.java
@@ -37,40 +37,40 @@
  *
  * <p>Example if all exceptions should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * fooPluginSetEntryContext.run(foo -> foo.doFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if all exceptions, but one, should be caught and logged:
  *
- * <pre>
+ * <pre>{@code
  * try {
  *   fooPluginSetEntryContext.run(foo -> foo.doFoo(), MyException.class);
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Object result = fooPluginSetEntryContext.call(foo -> foo.getFoo());
- * </pre>
+ * }</pre>
  *
  * <p>Example if return values and a single exception should be handled:
  *
- * <pre>
+ * <pre>{@code
  * Object result;
  * try {
  *   result = fooPluginSetEntryContext.call(foo -> foo.getFoo(), MyException.class);
  * } catch (MyException e) {
  *   // handle the exception
  * }
- * </pre>
+ * }</pre>
  *
  * <p>Example if several exceptions should be handled:
  *
- * <pre>
+ * <pre>{@code
  * for (Extension<Foo> fooExtension : fooDynamicSet.entries()) {
  *   try (TraceContext traceContext = PluginContext.newTrace(fooExtension)) {
  *     fooExtension.get().doFoo();
@@ -78,7 +78,7 @@
  *     // handle the exception
  *   }
  * }
- * </pre>
+ * }</pre>
  */
 public class PluginSetEntryContext<T> {
   private final Extension<T> extension;
diff --git a/java/com/google/gerrit/server/project/ProjectCache.java b/java/com/google/gerrit/server/project/ProjectCache.java
index cd41ce5..fee7105 100644
--- a/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/java/com/google/gerrit/server/project/ProjectCache.java
@@ -42,10 +42,10 @@
     return () -> new NoSuchProjectException(nameKey);
   }
 
-  /** @return the parent state for all projects on this server. */
+  /** Returns the parent state for all projects on this server. */
   ProjectState getAllProjects();
 
-  /** @return the project state of the project storing meta data for all users. */
+  /** Returns the project state of the project storing meta data for all users. */
   ProjectState getAllUsers();
 
   /**
@@ -84,12 +84,12 @@
    */
   void remove(Project.NameKey name);
 
-  /** @return sorted iteration of projects. */
+  /** Returns sorted iteration of projects. */
   ImmutableSortedSet<Project.NameKey> all();
 
   /**
-   * @return estimated set of relevant groups extracted from hot project access rules. If the cache
-   *     is cold or too small for the entire project set of the server, this set may be incomplete.
+   * Returns estimated set of relevant groups extracted from hot project access rules. If the cache
+   * is cold or too small for the entire project set of the server, this set may be incomplete.
    */
   Set<AccountGroup.UUID> guessRelevantGroupUUIDs();
 
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index e69967c..de27afa 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -51,9 +51,9 @@
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
 import com.google.gerrit.server.cache.serialize.entities.CachedProjectConfigSerializer;
+import com.google.gerrit.server.config.AllProjectsConfigProvider;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
@@ -78,8 +78,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.lib.StoredConfig;
 
 /**
  * Cache of project information, including access rights.
@@ -304,27 +303,22 @@
   /**
    * Returns a {@code MurMur128} hash of the contents of {@code etc/All-Projects-project.config}.
    */
-  public static byte[] allProjectsFileProjectConfigHash(
-      AllProjectsName allProjectsName, SitePaths sitePaths) {
+  public static byte[] allProjectsFileProjectConfigHash(Optional<StoredConfig> allProjectsConfig) {
     // Hash the contents of All-Projects-project.config
     // This is a way for administrators to orchestrate project.config changes across many Gerrit
     // instances.
     // When this file changes, we need to make sure we disregard persistently cached project
     // state.
-    FileBasedConfig fileBasedConfig =
-        new FileBasedConfig(
-            sitePaths
-                .etc_dir
-                .resolve(allProjectsName.get())
-                .resolve(ProjectConfig.PROJECT_CONFIG)
-                .toFile(),
-            FS.DETECTED);
+    if (!allProjectsConfig.isPresent()) {
+      // If the project.config file is not present, this is equal to an empty config file:
+      return Hashing.murmur3_128().hashString("", UTF_8).asBytes();
+    }
     try {
-      fileBasedConfig.load();
+      allProjectsConfig.get().load();
     } catch (IOException | ConfigInvalidException e) {
       throw new IllegalStateException(e);
     }
-    return Hashing.murmur3_128().hashString(fileBasedConfig.toText(), UTF_8).asBytes();
+    return Hashing.murmur3_128().hashString(allProjectsConfig.get().toText(), UTF_8).asBytes();
   }
 
   @Singleton
@@ -334,7 +328,7 @@
     private final ListeningExecutorService cacheRefreshExecutor;
     private final Counter2<String, Boolean> refreshCounter;
     private final AllProjectsName allProjectsName;
-    private final SitePaths sitePaths;
+    private final AllProjectsConfigProvider allProjectsConfigProvider;
 
     @Inject
     InMemoryLoader(
@@ -344,18 +338,25 @@
         @CacheRefreshExecutor ListeningExecutorService cacheRefreshExecutor,
         MetricMaker metricMaker,
         AllProjectsName allProjectsName,
-        SitePaths sitePaths) {
+        AllProjectsConfigProvider allProjectsConfigProvider) {
       this.persistedCache = persistedCache;
       this.repoManager = repoManager;
       this.cacheRefreshExecutor = cacheRefreshExecutor;
       refreshCounter =
           metricMaker.newCounter(
               "caches/refresh_count",
-              new Description("count").setRate(),
-              Field.ofString("cache", Metadata.Builder::className).build(),
-              Field.ofBoolean("outdated", Metadata.Builder::outdated).build());
+              new Description(
+                      "The number of refreshes per cache with an indicator if a reload was"
+                          + " necessary.")
+                  .setRate(),
+              Field.ofString("cache", Metadata.Builder::className)
+                  .description("The name of the cache.")
+                  .build(),
+              Field.ofBoolean("outdated", Metadata.Builder::outdated)
+                  .description("Whether the cache entry was outdated on reload.")
+                  .build());
       this.allProjectsName = allProjectsName;
-      this.sitePaths = sitePaths;
+      this.allProjectsConfigProvider = allProjectsConfigProvider;
     }
 
     @Override
@@ -369,7 +370,8 @@
             Cache.ProjectCacheKeyProto.newBuilder().setProject(key.get());
         Ref configRef = git.exactRef(RefNames.REFS_CONFIG);
         if (key.get().equals(allProjectsName.get())) {
-          byte[] fileHash = allProjectsFileProjectConfigHash(allProjectsName, sitePaths);
+          Optional<StoredConfig> allProjectsConfig = allProjectsConfigProvider.get(allProjectsName);
+          byte[] fileHash = allProjectsFileProjectConfigHash(allProjectsConfig);
           keyProto.setGlobalConfigRevision(ByteString.copyFrom(fileHash));
         }
         if (configRef != null) {
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 0d710b9..513aeed 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -64,10 +64,10 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.AllProjectsConfigProvider;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfig;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
@@ -98,8 +98,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 
 public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -203,28 +201,21 @@
   // ProjectCache, so this would retain lots more memory.
   @Singleton
   public static class Factory {
-    @Nullable
-    public static StoredConfig getBaseConfig(
-        SitePaths sitePaths, AllProjectsName allProjects, Project.NameKey projectName) {
-      return projectName.equals(allProjects)
-          // Delay loading till onLoad method.
-          ? new FileBasedConfig(
-              sitePaths.etc_dir.resolve(allProjects.get()).resolve(PROJECT_CONFIG).toFile(),
-              FS.DETECTED)
-          : null;
-    }
-
-    private final SitePaths sitePaths;
-    private final AllProjectsName allProjects;
+    private final AllProjectsName allProjectsName;
+    private final AllProjectsConfigProvider allProjectsConfigProvider;
 
     @Inject
-    Factory(SitePaths sitePaths, AllProjectsName allProjects) {
-      this.sitePaths = sitePaths;
-      this.allProjects = allProjects;
+    Factory(AllProjectsName allProjectsName, AllProjectsConfigProvider allProjectsConfigProvider) {
+      this.allProjectsName = allProjectsName;
+      this.allProjectsConfigProvider = allProjectsConfigProvider;
     }
 
     public ProjectConfig create(Project.NameKey projectName) {
-      return new ProjectConfig(projectName, getBaseConfig(sitePaths, allProjects, projectName));
+      return new ProjectConfig(
+          projectName,
+          projectName.equals(allProjectsName)
+              ? allProjectsConfigProvider.get(allProjectsName)
+              : Optional.empty());
     }
 
     public ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException {
@@ -249,7 +240,7 @@
     }
   }
 
-  private final StoredConfig baseConfig;
+  private final Optional<StoredConfig> baseConfig;
 
   private Project project;
   private AccountsSection accountsSection;
@@ -355,7 +346,7 @@
     requireNonNull(commentLinkSections.remove(name));
   }
 
-  private ProjectConfig(Project.NameKey projectName, @Nullable StoredConfig baseConfig) {
+  private ProjectConfig(Project.NameKey projectName, Optional<StoredConfig> baseConfig) {
     this.projectName = projectName;
     this.baseConfig = baseConfig;
   }
@@ -569,32 +560,32 @@
     groupList.renameGroup(uuid, newName);
   }
 
-  /** @return the group reference, if the group is used by at least one rule. */
+  /** Returns the group reference, if the group is used by at least one rule. */
   public GroupReference getGroup(AccountGroup.UUID uuid) {
     return groupList.byUUID(uuid);
   }
 
   /**
-   * @return the group reference corresponding to the specified group name if the group is used by
-   *     at least one rule or plugin value.
+   * Returns the group reference corresponding to the specified group name if the group is used by
+   * at least one rule or plugin value.
    */
   public GroupReference getGroup(String groupName) {
     return groupList.byName(groupName);
   }
 
   /**
-   * @return the project's rules.pl ObjectId, if present in the branch. Null if it doesn't exist.
+   * Returns the project's rules.pl ObjectId, if present in the branch. Null if it doesn't exist.
    */
   public ObjectId getRulesId() {
     return rulesId;
   }
 
-  /** @return the maxObjectSizeLimit configured on this project, or zero if not configured. */
+  /** Returns the maxObjectSizeLimit configured on this project, or zero if not configured. */
   public long getMaxObjectSizeLimit() {
     return maxObjectSizeLimit;
   }
 
-  /** @return the checkReceivedObjects for this project, default is true. */
+  /** Returns the checkReceivedObjects for this project, default is true. */
   public boolean getCheckReceivedObjects() {
     return checkReceivedObjects;
   }
@@ -636,8 +627,8 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    if (baseConfig != null) {
-      baseConfig.load();
+    if (baseConfig.isPresent()) {
+      baseConfig.get().load();
     }
     readGroupList();
 
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index c382f04..4e778a4 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -39,7 +39,8 @@
 import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.gerrit.server.git.GitRepositoryManager.Status;
+import com.google.gerrit.server.git.RepositoryExistsException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
@@ -107,12 +108,9 @@
     final Project.NameKey nameKey = args.getProject();
     try {
       final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
-      try (Repository repo = repoManager.openRepository(nameKey)) {
-        if (repo.getObjectDatabase().exists()) {
-          throw new ResourceConflictException("project \"" + nameKey + "\" exists");
-        }
-      } catch (RepositoryNotFoundException e) {
-        // It does not exist, safe to ignore.
+      Status status = repoManager.getRepositoryStatus(nameKey);
+      if (!status.equals(Status.NON_EXISTENT)) {
+        throw new RepositoryExistsException(nameKey, "Repository status: " + status);
       }
       try (Repository repo = repoManager.createRepository(nameKey)) {
         RefUpdate u = repo.updateRef(Constants.HEAD);
@@ -129,13 +127,11 @@
 
         return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
       }
-    } catch (RepositoryCaseMismatchException e) {
+    } catch (RepositoryExistsException e) {
       throw new ResourceConflictException(
           "Cannot create "
               + nameKey.get()
-              + " because the name is already occupied by another project."
-              + " The other project has the same name, only spelled in a"
-              + " different case.",
+              + " because the name is already occupied by another project.",
           e);
     } catch (RepositoryNotFoundException badName) {
       throw new BadRequestException("invalid project name: " + nameKey, badName);
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 4569027..69e6036 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -138,8 +138,8 @@
   }
 
   /**
-   * @return cached computation of all global capabilities. This should only be invoked on the state
-   *     from {@link ProjectCache#getAllProjects()}. Null on any other project.
+   * Returns cached computation of all global capabilities. This should only be invoked on the state
+   * from {@link ProjectCache#getAllProjects()}. Null on any other project.
    */
   public CapabilityCollection getCapabilityCollection() {
     return capabilities;
@@ -316,9 +316,9 @@
   }
 
   /**
-   * @return all {@link AccountGroup}'s to which the owner privilege for 'refs/*' is assigned for
-   *     this project (the local owners), if there are no local owners the local owners of the
-   *     nearest parent project that has local owners are returned
+   * Returns all {@link AccountGroup}'s to which the owner privilege for 'refs/*' is assigned for
+   * this project (the local owners), if there are no local owners the local owners of the nearest
+   * parent project that has local owners are returned
    */
   public Set<AccountGroup.UUID> getOwners() {
     for (ProjectState p : tree()) {
@@ -330,10 +330,10 @@
   }
 
   /**
-   * @return all {@link AccountGroup}'s that are allowed to administrate the complete project. This
-   *     includes all groups to which the owner privilege for 'refs/*' is assigned for this project
-   *     (the local owners) and all groups to which the owner privilege for 'refs/*' is assigned for
-   *     one of the parent projects (the inherited owners).
+   * Returns all {@link AccountGroup}'s that are allowed to administrate the complete project. This
+   * includes all groups to which the owner privilege for 'refs/*' is assigned for this project (the
+   * local owners) and all groups to which the owner privilege for 'refs/*' is assigned for one of
+   * the parent projects (the inherited owners).
    */
   public Set<AccountGroup.UUID> getAllOwners() {
     Set<AccountGroup.UUID> result = new HashSet<>();
@@ -346,16 +346,16 @@
   }
 
   /**
-   * @return an iterable that walks through this project and then the parents of this project.
-   *     Starts from this project and progresses up the hierarchy to All-Projects.
+   * Returns an iterable that walks through this project and then the parents of this project.
+   * Starts from this project and progresses up the hierarchy to All-Projects.
    */
   public Iterable<ProjectState> tree() {
     return () -> new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this);
   }
 
   /**
-   * @return an iterable that walks in-order from All-Projects through the project hierarchy to this
-   *     project.
+   * Returns an iterable that walks in-order from All-Projects through the project hierarchy to this
+   * project.
    */
   public Iterable<ProjectState> treeInOrder() {
     List<ProjectState> projects = Lists.newArrayList(tree());
@@ -364,8 +364,8 @@
   }
 
   /**
-   * @return an iterable that walks through the parents of this project. Starts from the immediate
-   *     parent of this project and progresses up the hierarchy to All-Projects.
+   * Returns an iterable that walks through the parents of this project. Starts from the immediate
+   * parent of this project and progresses up the hierarchy to All-Projects.
    */
   public FluentIterable<ProjectState> parents() {
     return FluentIterable.from(tree()).skip(1);
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 2adebe7..342c2bc 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -55,9 +55,9 @@
   }
 
   /**
-   * @return true if a commit is reachable from a given set of refs. This method enforces
-   *     permissions on the given set of refs and performs a reachability check. Tags are not
-   *     filtered separately and will only be returned if reachable by a provided ref.
+   * Returns true if a commit is reachable from a given set of refs. This method enforces
+   * permissions on the given set of refs and performs a reachability check. Tags are not filtered
+   * separately and will only be returned if reachable by a provided ref.
    */
   public boolean fromRefs(
       Project.NameKey project, Repository repo, RevCommit commit, List<Ref> refs) {
diff --git a/java/com/google/gerrit/server/project/RefResource.java b/java/com/google/gerrit/server/project/RefResource.java
index ac2735d..fcf6048 100644
--- a/java/com/google/gerrit/server/project/RefResource.java
+++ b/java/com/google/gerrit/server/project/RefResource.java
@@ -22,9 +22,9 @@
     super(projectState, user);
   }
 
-  /** @return the ref's name */
+  /** Returns the ref's name */
   public abstract String getRef();
 
-  /** @return the ref's revision */
+  /** Returns the ref's revision */
   public abstract String getRevision();
 }
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 652c49f..0336e8e 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -62,7 +62,7 @@
     checkRemoveReviewer(notes, currentUser, reviewer, 0);
   }
 
-  /** @return true if the user is allowed to remove this reviewer. */
+  /** Returns true if the user is allowed to remove this reviewer. */
   public boolean testRemoveReviewer(
       ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
       throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
new file mode 100644
index 0000000..f028def
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.MoreCollectors;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRecord.Label;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Convert {@link com.google.gerrit.entities.SubmitRecord} entities to {@link
+ * com.google.gerrit.entities.SubmitRequirementResult}s.
+ */
+public class SubmitRequirementsAdapter {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private SubmitRequirementsAdapter() {}
+
+  public static List<SubmitRequirementResult> createResult(
+      SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId) {
+    List<SubmitRequirementResult> results;
+    if (record.ruleName.equals("gerrit~DefaultSubmitRule")) {
+      results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId);
+    } else {
+      results = createFromCustomSubmitRecord(record, psCommitId);
+    }
+    logger.atFine().log("Converted submit record %s to submit requirements %s", record, results);
+    return results;
+  }
+
+  private static List<SubmitRequirementResult> createFromDefaultSubmitRecord(
+      List<Label> labels, List<LabelType> labelTypes, ObjectId psCommitId) {
+    ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
+    for (Label label : labels) {
+      LabelType labelType = getLabelType(labelTypes, label.label);
+      if (!isBlocking(labelType)) {
+        continue;
+      }
+      ImmutableList<String> atoms = toExpressionAtomList(labelType);
+      SubmitRequirement.Builder req =
+          SubmitRequirement.builder()
+              .setName(label.label)
+              .setSubmittabilityExpression(toExpression(atoms))
+              .setAllowOverrideInChildProjects(labelType.isCanOverride());
+      result.add(
+          SubmitRequirementResult.builder()
+              .legacy(true)
+              .submitRequirement(req.build())
+              .submittabilityExpressionResult(
+                  createExpressionResult(toExpression(atoms), mapStatus(label), atoms))
+              .patchSetCommitId(psCommitId)
+              .build());
+    }
+    return result.build();
+  }
+
+  private static List<SubmitRequirementResult> createFromCustomSubmitRecord(
+      SubmitRecord record, ObjectId psCommitId) {
+    if (record.labels == null || record.labels.isEmpty()) {
+      SubmitRequirement sr =
+          SubmitRequirement.builder()
+              .setName(record.ruleName)
+              .setSubmittabilityExpression(
+                  SubmitRequirementExpression.create(String.format("rule:%s", record.ruleName)))
+              .setAllowOverrideInChildProjects(false)
+              .build();
+      return ImmutableList.of(
+          SubmitRequirementResult.builder()
+              .legacy(true)
+              .submitRequirement(sr)
+              .submittabilityExpressionResult(
+                  createExpressionResult(
+                      sr.submittabilityExpression(),
+                      mapStatus(record),
+                      ImmutableList.of(record.ruleName)))
+              .patchSetCommitId(psCommitId)
+              .build());
+    }
+    ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
+    for (Label label : record.labels) {
+      String expressionString = String.format("label:%s=%s", label.label, record.ruleName);
+      SubmitRequirement sr =
+          SubmitRequirement.builder()
+              .setName(label.label)
+              .setSubmittabilityExpression(SubmitRequirementExpression.create(expressionString))
+              .setAllowOverrideInChildProjects(false)
+              .build();
+      result.add(
+          SubmitRequirementResult.builder()
+              .legacy(true)
+              .submitRequirement(sr)
+              .submittabilityExpressionResult(
+                  createExpressionResult(
+                      sr.submittabilityExpression(),
+                      mapStatus(label),
+                      ImmutableList.of(expressionString)))
+              .patchSetCommitId(psCommitId)
+              .build());
+    }
+    return result.build();
+  }
+
+  private static boolean isBlocking(LabelType labelType) {
+    return labelType.getFunction().isBlock() || labelType.getFunction().isRequired();
+  }
+
+  private static SubmitRequirementExpression toExpression(List<String> atoms) {
+    return SubmitRequirementExpression.create(String.join(" ", atoms));
+  }
+
+  private static ImmutableList<String> toExpressionAtomList(LabelType lt) {
+    String ignoreSelfApproval =
+        lt.isIgnoreSelfApproval() ? ",user=" + ChangeQueryBuilder.ARG_ID_NON_UPLOADER : "";
+    switch (lt.getFunction()) {
+      case MAX_WITH_BLOCK:
+        return ImmutableList.of(
+            String.format("label:%s=MAX", lt.getName()) + ignoreSelfApproval,
+            String.format("-label:%s=MIN", lt.getName()));
+      case ANY_WITH_BLOCK:
+        return ImmutableList.of(String.format(String.format("-label:%s=MIN", lt.getName())));
+      case MAX_NO_BLOCK:
+        return ImmutableList.of(
+            String.format(String.format("label:%s=MAX", lt.getName())) + ignoreSelfApproval);
+      case NO_BLOCK:
+      case NO_OP:
+      case PATCH_SET_LOCK:
+      default:
+        return ImmutableList.of();
+    }
+  }
+
+  private static Status mapStatus(Label label) {
+    SubmitRequirementExpressionResult.Status status = Status.PASS;
+    switch (label.status) {
+      case OK:
+      case MAY:
+        status = Status.PASS;
+        break;
+      case REJECT:
+      case NEED:
+      case IMPOSSIBLE:
+        status = Status.FAIL;
+        break;
+    }
+    return status;
+  }
+
+  private static Status mapStatus(SubmitRecord submitRecord) {
+    switch (submitRecord.status) {
+      case OK:
+      case CLOSED:
+      case FORCED:
+        return Status.PASS;
+      case NOT_READY:
+        return Status.FAIL;
+      case RULE_ERROR:
+      default:
+        return Status.ERROR;
+    }
+  }
+
+  private static SubmitRequirementExpressionResult createExpressionResult(
+      SubmitRequirementExpression expression, Status status, ImmutableList<String> atoms) {
+    return SubmitRequirementExpressionResult.create(
+        expression,
+        status,
+        status == Status.PASS ? atoms : ImmutableList.of(),
+        status == Status.FAIL ? atoms : ImmutableList.of());
+  }
+
+  private static LabelType getLabelType(List<LabelType> labelTypes, String labelName) {
+    return labelTypes.stream()
+        .filter(lt -> lt.getName().equals(labelName))
+        .collect(MoreCollectors.onlyElement());
+  }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
index fad90df..b3ac380 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluator.java
@@ -14,36 +14,27 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
-
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
-import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
 import com.google.gerrit.entities.SubmitRequirementResult;
-import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
 import java.util.Map;
-import java.util.Optional;
 
-/** Evaluates submit requirements for different change data. */
-@Singleton
-public class SubmitRequirementsEvaluator {
-  private final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
-  private final ProjectCache projectCache;
+public interface SubmitRequirementsEvaluator {
+  /**
+   * Evaluate and return all submit requirement results for a change. Submit requirements are read
+   * from the project config of the project containing the change as well as parent projects.
+   */
+  Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(ChangeData cd);
 
-  @Inject
-  private SubmitRequirementsEvaluator(
-      Provider<ChangeQueryBuilder> changeQueryBuilderProvider, ProjectCache projectCache) {
-    this.changeQueryBuilderProvider = changeQueryBuilderProvider;
-    this.projectCache = projectCache;
-  }
+  /** Evaluate a single {@link SubmitRequirement} using change data. */
+  SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd);
+
+  /** Evaluate a {@link SubmitRequirementExpression} using change data. */
+  SubmitRequirementExpressionResult evaluateExpression(
+      SubmitRequirementExpression expression, ChangeData changeData);
 
   /**
    * Validate a {@link SubmitRequirementExpression}. Callers who wish to validate submit
@@ -52,74 +43,5 @@
    * @param expression entity containing the expression string.
    * @throws QueryParseException the expression string contains invalid syntax and can't be parsed.
    */
-  public void validateExpression(SubmitRequirementExpression expression)
-      throws QueryParseException {
-    changeQueryBuilderProvider.get().parse(expression.expressionString());
-  }
-
-  /**
-   * Evaluate and return all submit requirements for a change. Submit requirements are retrieved for
-   * the project containing the change and parent projects as well.
-   */
-  public Map<SubmitRequirement, SubmitRequirementResult> getResults(ChangeData cd) {
-    ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
-    Map<String, SubmitRequirement> requirements = state.getSubmitRequirements();
-    ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result =
-        ImmutableMap.builderWithExpectedSize(requirements.size());
-    for (SubmitRequirement requirement : requirements.values()) {
-      result.put(requirement, evaluate(requirement, cd));
-    }
-    return result.build();
-  }
-
-  /** Evaluate a {@link SubmitRequirement} on a given {@link ChangeData}. */
-  public SubmitRequirementResult evaluate(SubmitRequirement sr, ChangeData cd) {
-    SubmitRequirementExpressionResult blockingResult =
-        evaluateExpression(sr.submittabilityExpression(), cd);
-
-    Optional<SubmitRequirementExpressionResult> applicabilityResult =
-        sr.applicabilityExpression().isPresent()
-            ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
-            : Optional.empty();
-
-    Optional<SubmitRequirementExpressionResult> overrideResult =
-        sr.overrideExpression().isPresent()
-            ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
-            : Optional.empty();
-
-    return SubmitRequirementResult.builder()
-        .submitRequirement(sr)
-        .patchSetCommitId(cd.currentPatchSet().commitId())
-        .submittabilityExpressionResult(blockingResult)
-        .applicabilityExpressionResult(applicabilityResult)
-        .overrideExpressionResult(overrideResult)
-        .build();
-  }
-
-  /** Evaluate a {@link SubmitRequirementExpression} using change data. */
-  public SubmitRequirementExpressionResult evaluateExpression(
-      SubmitRequirementExpression expression, ChangeData changeData) {
-    try {
-      Predicate<ChangeData> predicate =
-          changeQueryBuilderProvider.get().parse(expression.expressionString());
-      PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData);
-      return SubmitRequirementExpressionResult.create(expression, predicateResult);
-    } catch (QueryParseException e) {
-      return SubmitRequirementExpressionResult.error(expression, e.getMessage());
-    }
-  }
-
-  /** Evaluate the predicate recursively using change data. */
-  private PredicateResult evaluatePredicateTree(
-      Predicate<ChangeData> predicate, ChangeData changeData) {
-    PredicateResult.Builder predicateResult =
-        PredicateResult.builder()
-            .predicateString(predicate.isLeaf() ? predicate.getPredicateString() : "")
-            .status(predicate.asMatchable().match(changeData));
-    predicate
-        .getChildren()
-        .forEach(
-            c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c, changeData)));
-    return predicateResult.build();
-  }
+  void validateExpression(SubmitRequirementExpression expression) throws QueryParseException;
 }
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
new file mode 100644
index 0000000..9be50c7
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -0,0 +1,173 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.PredicateResult;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Scopes;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Evaluates submit requirements for different change data. */
+public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvaluator {
+
+  private final Provider<ChangeQueryBuilder> changeQueryBuilderProvider;
+  private final ProjectCache projectCache;
+  private final SubmitRuleEvaluator.Factory legacyEvaluator;
+  private final ExperimentFeatures experimentFeatures;
+
+  public static Module module() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(SubmitRequirementsEvaluator.class)
+            .to(SubmitRequirementsEvaluatorImpl.class)
+            .in(Scopes.SINGLETON);
+      }
+    };
+  }
+
+  @Inject
+  private SubmitRequirementsEvaluatorImpl(
+      Provider<ChangeQueryBuilder> changeQueryBuilderProvider,
+      ProjectCache projectCache,
+      SubmitRuleEvaluator.Factory legacyEvaluator,
+      ExperimentFeatures experimentFeatures) {
+    this.changeQueryBuilderProvider = changeQueryBuilderProvider;
+    this.projectCache = projectCache;
+    this.legacyEvaluator = legacyEvaluator;
+    this.experimentFeatures = experimentFeatures;
+  }
+
+  @Override
+  public void validateExpression(SubmitRequirementExpression expression)
+      throws QueryParseException {
+    changeQueryBuilderProvider.get().parse(expression.expressionString());
+  }
+
+  @Override
+  public Map<SubmitRequirement, SubmitRequirementResult> evaluateAllRequirements(ChangeData cd) {
+    Map<SubmitRequirement, SubmitRequirementResult> result = getRequirements(cd);
+    if (experimentFeatures.isFeatureEnabled(
+        ExperimentFeaturesConstants
+            .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)) {
+      result.putAll(getLegacyRequirements(cd));
+    }
+    return ImmutableMap.copyOf(result);
+  }
+
+  @Override
+  public SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd) {
+    SubmitRequirementExpressionResult blockingResult =
+        evaluateExpression(sr.submittabilityExpression(), cd);
+
+    Optional<SubmitRequirementExpressionResult> applicabilityResult =
+        sr.applicabilityExpression().isPresent()
+            ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
+            : Optional.empty();
+
+    Optional<SubmitRequirementExpressionResult> overrideResult =
+        sr.overrideExpression().isPresent()
+            ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
+            : Optional.empty();
+
+    return SubmitRequirementResult.builder()
+        .legacy(false)
+        .submitRequirement(sr)
+        .patchSetCommitId(cd.currentPatchSet().commitId())
+        .submittabilityExpressionResult(blockingResult)
+        .applicabilityExpressionResult(applicabilityResult)
+        .overrideExpressionResult(overrideResult)
+        .build();
+  }
+
+  @Override
+  public SubmitRequirementExpressionResult evaluateExpression(
+      SubmitRequirementExpression expression, ChangeData changeData) {
+    try {
+      Predicate<ChangeData> predicate =
+          changeQueryBuilderProvider.get().parse(expression.expressionString());
+      PredicateResult predicateResult = evaluatePredicateTree(predicate, changeData);
+      return SubmitRequirementExpressionResult.create(expression, predicateResult);
+    } catch (QueryParseException e) {
+      return SubmitRequirementExpressionResult.error(expression, e.getMessage());
+    }
+  }
+
+  /** Evaluate and return submit requirements stored in this project's config and its parents. */
+  private Map<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) {
+    ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
+    Map<String, SubmitRequirement> requirements = state.getSubmitRequirements();
+    Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
+    for (SubmitRequirement requirement : requirements.values()) {
+      result.put(requirement, evaluateRequirement(requirement, cd));
+    }
+    return result;
+  }
+
+  /**
+   * Convert and return legacy submit records (created by label functions and other {@link
+   * com.google.gerrit.server.rules.SubmitRule}s to submit requirement results.
+   */
+  private Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(ChangeData cd) {
+    // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
+    // This doesn't have an effect since we never call this class (i.e. to evaluate submit
+    // requirements) for closed changes.
+    List<SubmitRecord> records = legacyEvaluator.create(SubmitRuleOptions.defaults()).evaluate(cd);
+    List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
+    ObjectId commitId = cd.currentPatchSet().commitId();
+    return records.stream()
+        .map(r -> SubmitRequirementsAdapter.createResult(r, labelTypes, commitId))
+        .flatMap(List::stream)
+        .collect(Collectors.toMap(sr -> sr.submitRequirement(), Function.identity()));
+  }
+
+  /** Evaluate the predicate recursively using change data. */
+  private PredicateResult evaluatePredicateTree(
+      Predicate<ChangeData> predicate, ChangeData changeData) {
+    PredicateResult.Builder predicateResult =
+        PredicateResult.builder()
+            .predicateString(predicate.isLeaf() ? predicate.getPredicateString() : "")
+            .status(predicate.asMatchable().match(changeData));
+    predicate
+        .getChildren()
+        .forEach(
+            c -> predicateResult.addChildPredicateResult(evaluatePredicateTree(c, changeData)));
+    return predicateResult.build();
+  }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 6345cdb..6c5559c 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -18,15 +18,19 @@
 import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
 
 import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitTypeRecord;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.index.OnlineReindexMode;
+import com.google.gerrit.server.logging.CallerFinder;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.rules.DefaultSubmitRule;
@@ -42,12 +46,15 @@
  * the results through rules found in the parent projects, all the way up to All-Projects.
  */
 public class SubmitRuleEvaluator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final ProjectCache projectCache;
   private final PrologRule prologRule;
   private final PluginSetContext<SubmitRule> submitRules;
   private final Timer0 submitRuleEvaluationLatency;
   private final Timer0 submitTypeEvaluationLatency;
   private final SubmitRuleOptions opts;
+  private final CallerFinder callerFinder;
 
   public interface Factory {
     /** Returns a new {@link SubmitRuleEvaluator} with the specified options */
@@ -78,6 +85,14 @@
                 .setUnit(Units.MILLISECONDS));
 
     this.opts = options;
+
+    this.callerFinder =
+        CallerFinder.builder()
+            .addTarget(ChangeApi.class)
+            .addTarget(ChangeJson.class)
+            .addTarget(ChangeData.class)
+            .addTarget(SubmitRequirementsEvaluatorImpl.class)
+            .build();
   }
 
   /**
@@ -88,6 +103,9 @@
    * @param cd ChangeData to evaluate
    */
   public List<SubmitRecord> evaluate(ChangeData cd) {
+    logger.atFine().log(
+        "Evaluate submit rules for change %d (caller: %s)",
+        cd.change().getId().get(), callerFinder.findCallerLazy());
     try (Timer0.Context ignored = submitRuleEvaluationLatency.start()) {
       Change change;
       ProjectState projectState;
@@ -125,7 +143,17 @@
               projectState.hasPrologRules()
                   ? rule -> !(rule.get() instanceof DefaultSubmitRule)
                   : rule -> true)
-          .map(c -> c.call(s -> s.evaluate(cd)))
+          .map(
+              c ->
+                  c.call(
+                      s -> {
+                        Optional<SubmitRecord> evaluate = s.evaluate(cd);
+                        if (evaluate.isPresent()) {
+                          evaluate.get().ruleName =
+                              c.getPluginName() + "~" + s.getClass().getSimpleName();
+                        }
+                        return evaluate;
+                      }))
           .filter(Optional::isPresent)
           .map(Optional::get)
           .collect(toImmutableList());
@@ -136,7 +164,6 @@
    * Evaluate the submit type rules to get the submit type.
    *
    * @return record from the evaluated rules.
-   * @param cd
    */
   public SubmitTypeRecord getSubmitType(ChangeData cd) {
     try (Timer0.Context ignored = submitTypeEvaluationLatency.start()) {
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index b7f8b45..1d67009 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.index.query.InternalQuery;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.inject.Inject;
@@ -46,12 +47,16 @@
 public class InternalAccountQuery extends InternalQuery<AccountState, InternalAccountQuery> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+
   @Inject
   InternalAccountQuery(
       AccountQueryProcessor queryProcessor,
       AccountIndexCollection indexes,
-      IndexConfig indexConfig) {
+      IndexConfig indexConfig,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     super(queryProcessor, indexes, indexConfig);
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   public List<AccountState> byDefault(String query, boolean canSeeSecondaryEmails) {
@@ -59,7 +64,7 @@
   }
 
   public List<AccountState> byExternalId(String scheme, String id) {
-    return byExternalId(ExternalId.Key.create(scheme, id));
+    return byExternalId(externalIdKeyFactory.create(scheme, id));
   }
 
   public List<AccountState> byExternalId(ExternalId.Key externalId) {
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
index 3bf072a..4dedbb5 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalContext.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -28,8 +28,12 @@
   /** Approval on the source patch set to be copied. */
   public abstract PatchSetApproval patchSetApproval();
 
-  /** Target change and patch set for the approval. */
-  public abstract PatchSet.Id target();
+  /**
+   * Target change and patch set for the approval. This must be used instead of getting the PatchSet
+   * from {@link #changeNotes()} because it is possible we are now creating the patch-set, so it
+   * doesn't exist in changeNotes yet.
+   */
+  public abstract PatchSet target();
 
   /** {@link ChangeNotes} of the change in question. */
   public abstract ChangeNotes changeNotes();
@@ -38,18 +42,18 @@
   public abstract ChangeKind changeKind();
 
   public static ApprovalContext create(
-      ChangeNotes changeNotes, PatchSetApproval psa, PatchSet.Id id, ChangeKind changeKind) {
+      ChangeNotes changeNotes, PatchSetApproval psa, PatchSet patchSet, ChangeKind changeKind) {
     checkState(
-        psa.patchSetId().changeId().equals(id.changeId()),
+        psa.patchSetId().changeId().equals(patchSet.id().changeId()),
         "approval and target must be the same change. got: %s, %s",
         psa.patchSetId(),
-        id);
+        patchSet.id());
     // TODO(ekempin): Use checkState to verify that psa.patchSetId().get() + 1 == id.get() so that
     // it's ensured that approvals are only copied to the next consecutive patch set. To add back
     // this verification https://gerrit-review.googlesource.com/c/gerrit/+/312633 can be reverted.
     // As explained in the commit message of this change doing this check is only possible if there
     // are no changes with gaps in patch set numbers. Since it's planned to fix-up old changes with
     // gaps in patch set numbers, this todo is a reminder to add back the check once this is done.
-    return new AutoValue_ApprovalContext(psa, id, changeNotes, changeKind);
+    return new AutoValue_ApprovalContext(psa, patchSet, changeNotes, changeKind);
   }
 }
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index 459a8b0..55c27be 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -14,40 +14,57 @@
 
 package com.google.gerrit.server.query.approval;
 
+import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Patch.ChangeType;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.DiffNotAvailableException;
 import com.google.gerrit.server.patch.DiffOperations;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 /** Predicate that matches when the new patch-set includes the same files as the old patch-set. */
 @Singleton
 public class ListOfFilesUnchangedPredicate extends ApprovalPredicate {
   private final DiffOperations diffOperations;
+  private final GitRepositoryManager repositoryManager;
 
   @Inject
-  public ListOfFilesUnchangedPredicate(DiffOperations diffOperations) {
+  public ListOfFilesUnchangedPredicate(
+      DiffOperations diffOperations, GitRepositoryManager repositoryManager) {
     this.diffOperations = diffOperations;
+    this.repositoryManager = repositoryManager;
   }
 
   @Override
   public boolean match(ApprovalContext ctx) {
-    PatchSet currentPatchset = ctx.changeNotes().getCurrentPatchSet();
-    Map.Entry<PatchSet.Id, PatchSet> priorPatchSet =
-        ctx.changeNotes().getPatchSets().lowerEntry(currentPatchset.id());
+    PatchSet targetPatchSet = ctx.target();
+    PatchSet sourcePatchSet =
+        ctx.changeNotes().getPatchSets().get(ctx.patchSetApproval().patchSetId());
+
+    Integer parentNum =
+        isInitialCommit(ctx.changeNotes().getProjectName(), targetPatchSet.commitId()) ? 0 : 1;
     try {
-      return match(
-          diffOperations.listModifiedFiles(
-              ctx.changeNotes().getProjectName(),
-              priorPatchSet.getValue().commitId(),
-              currentPatchset.commitId()));
+      Map<String, FileDiffOutput> modifiedTargetPatchSet =
+          diffOperations.listModifiedFilesAgainstParent(
+              ctx.changeNotes().getProjectName(), targetPatchSet.commitId(), parentNum);
+      Map<String, FileDiffOutput> modifiedSourcePatchSet =
+          diffOperations.listModifiedFilesAgainstParent(
+              ctx.changeNotes().getProjectName(), sourcePatchSet.commitId(), parentNum);
+      return match(modifiedTargetPatchSet, modifiedSourcePatchSet);
     } catch (DiffNotAvailableException ex) {
       throw new StorageException(
           "failed to compute difference in files, so won't copy"
@@ -57,24 +74,49 @@
     }
   }
 
-  public boolean match(Map<String, FileDiffOutput> modifiedFiles) {
-    return modifiedFiles.values().stream()
-        .noneMatch(
-            p ->
-                p.changeType() == ChangeType.ADDED
-                    || p.changeType() == ChangeType.DELETED
-                    || p.changeType() == ChangeType.RENAMED);
+  /**
+   * returns {@code true} if the files that were modified are the same in both inputs, and the
+   * {@link ChangeType} matches for each modified file.
+   */
+  public boolean match(
+      Map<String, FileDiffOutput> modifiedFiles1, Map<String, FileDiffOutput> modifiedFiles2) {
+    Set<String> allFiles = new HashSet<>();
+    allFiles.addAll(modifiedFiles1.keySet());
+    allFiles.addAll(modifiedFiles2.keySet());
+    for (String file : allFiles) {
+      if (Patch.isMagic(file)) {
+        continue;
+      }
+      FileDiffOutput fileDiffOutput1 = modifiedFiles1.get(file);
+      FileDiffOutput fileDiffOutput2 = modifiedFiles2.get(file);
+      if (fileDiffOutput1 == null || fileDiffOutput2 == null) {
+        return false;
+      }
+      if (!fileDiffOutput2.changeType().equals(fileDiffOutput1.changeType())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public boolean isInitialCommit(Project.NameKey project, ObjectId objectId) {
+    try (Repository repo = repositoryManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(repo)) {
+      return revWalk.parseCommit(objectId).getParentCount() == 0;
+    } catch (IOException ex) {
+      throw new StorageException(ex);
+    }
   }
 
   @Override
   public Predicate<ApprovalContext> copy(
       Collection<? extends Predicate<ApprovalContext>> children) {
-    return new ListOfFilesUnchangedPredicate(diffOperations);
+    return new ListOfFilesUnchangedPredicate(diffOperations, repositoryManager);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(diffOperations);
+    return Objects.hash(diffOperations, repositoryManager);
   }
 
   @Override
@@ -83,6 +125,7 @@
       return false;
     }
     ListOfFilesUnchangedPredicate o = (ListOfFilesUnchangedPredicate) other;
-    return Objects.equals(o.diffOperations, diffOperations);
+    return Objects.equals(o.diffOperations, diffOperations)
+        && Objects.equals(o.repositoryManager, repositoryManager);
   }
 }
diff --git a/java/com/google/gerrit/server/query/approval/UserInPredicate.java b/java/com/google/gerrit/server/query/approval/UserInPredicate.java
index 7e16fcb..ac6720d 100644
--- a/java/com/google/gerrit/server/query/approval/UserInPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/UserInPredicate.java
@@ -39,7 +39,7 @@
   public boolean match(ApprovalContext ctx) {
     Account.Id accountId;
     if (field == Field.UPLOADER) {
-      PatchSet patchSet = ctx.changeNotes().getPatchSets().get(ctx.target());
+      PatchSet patchSet = ctx.target();
       accountId = patchSet.uploader();
     } else if (field == Field.APPROVER) {
       accountId = ctx.patchSetApproval().accountId();
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 8b4e4c7..6c74301 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -728,7 +728,7 @@
     this.attentionSet = attentionSet;
   }
 
-  /** @return patches for the change, in patch set ID order. */
+  /** Returns patches for the change, in patch set ID order. */
   public Collection<PatchSet> patchSets() {
     if (patchSets == null) {
       patchSets = psUtil.byChange(notes());
@@ -741,7 +741,7 @@
     this.patchSets = patchSets;
   }
 
-  /** @return patch with the given ID, or null if it does not exist. */
+  /** Returns patch with the given ID, or null if it does not exist. */
   public PatchSet patchSet(PatchSet.Id psId) {
     if (currentPatchSet != null && currentPatchSet.id().equals(psId)) {
       return currentPatchSet;
@@ -755,8 +755,8 @@
   }
 
   /**
-   * @return all patch set approvals for the change, keyed by ID, ordered by timestamp within each
-   *     patch set.
+   * Returns all patch set approvals for the change, keyed by ID, ordered by timestamp within each
+   * patch set.
    */
   public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() {
     if (allApprovals == null) {
@@ -951,7 +951,7 @@
             notes().getSubmitRequirementsResult().stream()
                 .collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
       } else {
-        submitRequirements = submitRequirementsEvaluator.getResults(this);
+        submitRequirements = submitRequirementsEvaluator.evaluateAllRequirements(this);
       }
     }
     return submitRequirements;
@@ -1102,19 +1102,6 @@
   }
 
   public boolean isReviewedBy(Account.Id accountId) {
-    Collection<String> stars = stars(accountId);
-
-    PatchSet ps = currentPatchSet();
-    if (ps != null) {
-      if (stars.contains(StarredChangesUtil.REVIEWED_LABEL + "/" + ps.number())) {
-        return true;
-      }
-
-      if (stars.contains(StarredChangesUtil.UNREVIEWED_LABEL + "/" + ps.number())) {
-        return false;
-      }
-    }
-
     return reviewedBy().contains(accountId);
   }
 
@@ -1212,8 +1199,8 @@
   }
 
   /**
-   * @return {@code null} if {@code revertOf} is {@code null}; true if the change is a pure revert;
-   *     false otherwise.
+   * Returns {@code null} if {@code revertOf} is {@code null}; true if the change is a pure revert;
+   * false otherwise.
    */
   @Nullable
   public Boolean isPureRevert() {
diff --git a/java/com/google/gerrit/server/query/change/ChangeDataSource.java b/java/com/google/gerrit/server/query/change/ChangeDataSource.java
index 34579a9..26ce46c 100644
--- a/java/com/google/gerrit/server/query/change/ChangeDataSource.java
+++ b/java/com/google/gerrit/server/query/change/ChangeDataSource.java
@@ -17,6 +17,6 @@
 import com.google.gerrit.index.query.DataSource;
 
 public interface ChangeDataSource extends DataSource<ChangeData> {
-  /** @return true if all returned ChangeData.hasChange() will be true. */
+  /** Returns true if all returned ChangeData.hasChange() will be true. */
   boolean hasChange();
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 044d276..4e638df 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -292,4 +292,15 @@
   public static Predicate<ChangeData> comment(String comment) {
     return new ChangeIndexPredicate(ChangeField.COMMENT, comment);
   }
+
+  /**
+   * Returns a predicate that matches with changes having a specific submit rule evaluating to a
+   * certain result. Value should be in the form of "$ruleName=$status" with $ruleName equals to
+   * '$plugin_name~$rule_name' and $rule_name equals to the name of the class that implements the
+   * {@link com.google.gerrit.server.rules.SubmitRule}. For gerrit core rules, $ruleName should be
+   * in the form of 'gerrit~$rule_name'.
+   */
+  public static Predicate<ChangeData> submitRuleStatus(String value) {
+    return new ChangeIndexPredicate(ChangeField.SUBMIT_RULE_RESULT, value);
+  }
 }
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 383e385..f1fe520 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -78,8 +78,10 @@
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.gerrit.server.project.ChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.submit.SubmitDryRun;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -87,6 +89,7 @@
 import com.google.inject.util.Providers;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -139,6 +142,7 @@
   public static final String FIELD_ADDED = "added";
   public static final String FIELD_AGE = "age";
   public static final String FIELD_ATTENTION_SET_USERS = "attentionusers";
+  public static final String FIELD_ATTENTION_SET_USERS_COUNT = "attentionuserscount";
   public static final String FIELD_ATTENTION_SET_FULL = "attentionfull";
   public static final String FIELD_ASSIGNEE = "assignee";
   public static final String FIELD_AUTHOR = "author";
@@ -176,7 +180,6 @@
   public static final String FIELD_OWNERIN = "ownerin";
   public static final String FIELD_PARENTOF = "parentof";
   public static final String FIELD_PARENTPROJECT = "parentproject";
-  public static final String FIELD_PATH = "path";
   public static final String FIELD_PENDING_REVIEWER = "pendingreviewer";
   public static final String FIELD_PENDING_REVIEWER_BY_EMAIL = "pendingreviewerbyemail";
   public static final String FIELD_PRIVATE = "private";
@@ -184,11 +187,9 @@
   public static final String FIELD_PROJECTS = "projects";
   public static final String FIELD_REF = "ref";
   public static final String FIELD_REVIEWEDBY = "reviewedby";
-  public static final String FIELD_REVIEWER = "reviewer";
   public static final String FIELD_REVIEWERIN = "reviewerin";
   public static final String FIELD_STAR = "star";
   public static final String FIELD_STARBY = "starby";
-  public static final String FIELD_STARREDBY = "starredby";
   public static final String FIELD_STARTED = "started";
   public static final String FIELD_STATUS = "status";
   public static final String FIELD_SUBMISSIONID = "submissionid";
@@ -198,7 +199,7 @@
   public static final String FIELD_WATCHEDBY = "watchedby";
   public static final String FIELD_WIP = "wip";
   public static final String FIELD_REVERTOF = "revertof";
-  public static final String FIELD_CHERRY_PICK_OF = "cherrypickof";
+  public static final String FIELD_CHERRYPICK = "cherrypick";
   public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
   public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
 
@@ -206,7 +207,9 @@
   public static final String ARG_ID_USER = "user";
   public static final String ARG_ID_GROUP = "group";
   public static final String ARG_ID_OWNER = "owner";
+  public static final String ARG_ID_NON_UPLOADER = "non_uploader";
   public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
+  public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
 
   public static final String OPERATOR_MERGED_BEFORE = "mergedbefore";
   public static final String OPERATOR_MERGED_AFTER = "mergedafter";
@@ -249,6 +252,7 @@
     final boolean indexMergeable;
     final boolean conflictsPredicateEnabled;
     final HasOperandAliasConfig hasOperandAliasConfig;
+    final PluginSetContext<SubmitRule> submitRules;
 
     private final Provider<CurrentUser> self;
 
@@ -283,7 +287,8 @@
         OperatorAliasConfig operatorAliasConfig,
         @GerritServerConfig Config gerritConfig,
         HasOperandAliasConfig hasOperandAliasConfig,
-        ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory) {
+        ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
+        PluginSetContext<SubmitRule> submitRules) {
       this(
           queryProvider,
           rewriter,
@@ -314,7 +319,8 @@
           MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(),
           gerritConfig.getBoolean("change", null, "conflictsPredicateEnabled", true),
           hasOperandAliasConfig,
-          changeIsVisbleToPredicateFactory);
+          changeIsVisbleToPredicateFactory,
+          submitRules);
     }
 
     private Arguments(
@@ -347,7 +353,8 @@
         boolean indexMergeable,
         boolean conflictsPredicateEnabled,
         HasOperandAliasConfig hasOperandAliasConfig,
-        ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory) {
+        ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
+        PluginSetContext<SubmitRule> submitRules) {
       this.queryProvider = queryProvider;
       this.rewriter = rewriter;
       this.opFactories = opFactories;
@@ -378,6 +385,7 @@
       this.indexMergeable = indexMergeable;
       this.conflictsPredicateEnabled = conflictsPredicateEnabled;
       this.hasOperandAliasConfig = hasOperandAliasConfig;
+      this.submitRules = submitRules;
     }
 
     Arguments asUser(CurrentUser otherUser) {
@@ -411,7 +419,8 @@
           indexMergeable,
           conflictsPredicateEnabled,
           hasOperandAliasConfig,
-          changeIsVisbleToPredicateFactory);
+          changeIsVisbleToPredicateFactory,
+          submitRules);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -471,10 +480,6 @@
     hasOperandAliases = args.hasOperandAliasConfig.getChangeQueryHasOperandAliases();
   }
 
-  public Arguments getArgs() {
-    return args;
-  }
-
   public ChangeQueryBuilder asUser(CurrentUser user) {
     return new ChangeQueryBuilder(builderDef, args.asUser(user));
   }
@@ -567,24 +572,57 @@
   }
 
   @Operator
+  public Predicate<ChangeData> rule(String value) throws QueryParseException {
+    String ruleNameArg = value;
+    String statusArg = null;
+    String[] queryArgs = value.split("=");
+    if (queryArgs.length > 2) {
+      throw new QueryParseException(
+          "Invalid query arguments. Correct format is 'rule:<rule_name>=<status>' "
+              + "with <rule_name> in the form of <plugin>~<rule>. For Gerrit core rules, "
+              + "rule name should be specified either as gerrit~<rule> or <rule>.");
+    }
+    if (queryArgs.length == 2) {
+      ruleNameArg = queryArgs[0];
+      statusArg = queryArgs[1];
+    }
+
+    // If ruleName is not prefixed by the plugin name, add the "gerrit~" prefix to it.
+    if (!ruleNameArg.contains("~")) {
+      ruleNameArg = "gerrit~" + ruleNameArg;
+    }
+
+    return statusArg == null
+        ? Predicate.or(
+            Arrays.asList(
+                ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.OK),
+                ChangePredicates.submitRuleStatus(ruleNameArg + "=" + SubmitRecord.Status.FORCED)))
+        : ChangePredicates.submitRuleStatus(ruleNameArg + "=" + statusArg);
+  }
+
+  @Operator
   public Predicate<ChangeData> has(String value) throws QueryParseException {
     value = hasOperandAliases.getOrDefault(value, value);
     if ("star".equalsIgnoreCase(value)) {
-      return starredby(self());
-    }
-
-    if ("stars".equalsIgnoreCase(value)) {
-      return new HasStarsPredicate(self());
+      return starredBySelf();
     }
 
     if ("draft".equalsIgnoreCase(value)) {
-      return draftby(self());
+      return draftBySelf();
     }
 
     if ("edit".equalsIgnoreCase(value)) {
       return ChangePredicates.editBy(self());
     }
 
+    if ("attention".equalsIgnoreCase(value)) {
+      if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
+        throw new QueryParseException(
+            "'has:attention' operator is not supported by change index version");
+      }
+      return new IsAttentionPredicate();
+    }
+
     if ("unresolved".equalsIgnoreCase(value)) {
       return new IsUnresolvedPredicate();
     }
@@ -604,11 +642,11 @@
   @Operator
   public Predicate<ChangeData> is(String value) throws QueryParseException {
     if ("starred".equalsIgnoreCase(value)) {
-      return starredby(self());
+      return starredBySelf();
     }
 
     if ("watched".equalsIgnoreCase(value)) {
-      return new IsWatchedByPredicate(args, false);
+      return new IsWatchedByPredicate(args);
     }
 
     if ("visible".equalsIgnoreCase(value)) {
@@ -658,6 +696,14 @@
           "'is:private' operator is not supported by change index version");
     }
 
+    if ("attention".equalsIgnoreCase(value)) {
+      if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
+        throw new QueryParseException(
+            "'is:attention' operator is not supported by change index version");
+      }
+      return new IsAttentionPredicate();
+    }
+
     if ("assigned".equalsIgnoreCase(value)) {
       return Predicate.not(ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE)));
     }
@@ -679,7 +725,7 @@
     }
 
     if ("ignored".equalsIgnoreCase(value)) {
-      return star("ignore");
+      return ignoredBySelf();
     }
 
     if ("started".equalsIgnoreCase(value)) {
@@ -697,6 +743,14 @@
       throw new QueryParseException("'is:wip' operator is not supported by change index version");
     }
 
+    if ("cherrypick".equalsIgnoreCase(value)) {
+      if (args.getSchema().hasField(ChangeField.CHERRY_PICK)) {
+        return new BooleanPredicate(ChangeField.CHERRY_PICK);
+      }
+      throw new QueryParseException(
+          "'is:cherrypick' operator is not supported by change index version");
+    }
+
     // for plugins the value will be operandName_pluginName
     List<String> names = Lists.newArrayList(Splitter.on('_').split(value));
     if (names.size() == 2) {
@@ -940,6 +994,8 @@
         if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
           if (pair.getValue().equals(ARG_ID_OWNER)) {
             accounts = Collections.singleton(OWNER_ACCOUNT_ID);
+          } else if (pair.getValue().equals(ARG_ID_NON_UPLOADER)) {
+            accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
           } else {
             accounts = parseAccount(pair.getValue());
           }
@@ -957,6 +1013,8 @@
         try {
           if (value.equals(ARG_ID_OWNER)) {
             accounts = Collections.singleton(OWNER_ACCOUNT_ID);
+          } else if (value.equals(ARG_ID_NON_UPLOADER)) {
+            accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
           } else {
             accounts = parseAccount(value);
           }
@@ -1011,65 +1069,25 @@
 
   @Operator
   public Predicate<ChangeData> star(String label) throws QueryParseException {
-    return new StarPredicate(self(), label);
-  }
-
-  @Operator
-  public Predicate<ChangeData> starredby(String who)
-      throws QueryParseException, IOException, ConfigInvalidException {
-    return starredby(parseAccount(who));
-  }
-
-  private Predicate<ChangeData> starredby(Set<Account.Id> who) {
-    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
-    for (Account.Id id : who) {
-      p.add(starredby(id));
+    if ("ignore".equalsIgnoreCase(label)) {
+      return ignoredBySelf();
     }
-    return Predicate.or(p);
-  }
-
-  private Predicate<ChangeData> starredby(Account.Id who) {
-    return new StarPredicate(who, StarredChangesUtil.DEFAULT_LABEL);
-  }
-
-  @Operator
-  public Predicate<ChangeData> watchedby(String who)
-      throws QueryParseException, IOException, ConfigInvalidException {
-    Set<Account.Id> m = parseAccount(who);
-    List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
-
-    Account.Id callerId;
-    try {
-      CurrentUser caller = args.self.get();
-      callerId = caller.isIdentifiedUser() ? caller.getAccountId() : null;
-    } catch (ProvisionException e) {
-      callerId = null;
+    if ("star".equalsIgnoreCase(label)) {
+      return starredBySelf();
     }
-
-    for (Account.Id id : m) {
-      // Each child IsWatchedByPredicate includes a visibility filter for the
-      // corresponding user, to ensure that predicate subtree only returns
-      // changes visible to that user. The exception is if one of the users is
-      // the caller of this method, in which case visibility is already being
-      // checked at the top level.
-      p.add(new IsWatchedByPredicate(args.asUser(id), !id.equals(callerId)));
-    }
-    return Predicate.or(p);
+    throw new IllegalArgumentException();
   }
 
-  @Operator
-  public Predicate<ChangeData> draftby(String who)
-      throws QueryParseException, IOException, ConfigInvalidException {
-    Set<Account.Id> m = parseAccount(who);
-    List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
-    for (Account.Id id : m) {
-      p.add(draftby(id));
-    }
-    return Predicate.or(p);
+  private Predicate<ChangeData> ignoredBySelf() throws QueryParseException {
+    return new StarPredicate(self(), StarredChangesUtil.IGNORE_LABEL);
   }
 
-  private Predicate<ChangeData> draftby(Account.Id who) {
-    return ChangePredicates.draftBy(who);
+  private Predicate<ChangeData> starredBySelf() throws QueryParseException {
+    return new StarPredicate(self(), StarredChangesUtil.DEFAULT_LABEL);
+  }
+
+  private Predicate<ChangeData> draftBySelf() throws QueryParseException {
+    return ChangePredicates.draftBy(self());
   }
 
   @Operator
@@ -1539,6 +1557,12 @@
   private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate(
       String who, Function<String, Predicate<ChangeData>> fullPredicateFunc)
       throws QueryParseException {
+    if (isSelf(who)) {
+      IdentifiedUser me = args.getIdentifiedUser();
+      List<Predicate<ChangeData>> predicates =
+          me.getEmailAddresses().stream().map(fullPredicateFunc).collect(toList());
+      return Predicate.or(predicates);
+    }
     Set<String> parts = SchemaUtil.getNameParts(who);
     if (parts.isEmpty()) {
       throw error("invalid value");
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index ade615c..12efecb 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -108,16 +108,23 @@
       return false;
     }
 
-    if (account != null
-        && !account.equals(approver)
-        && !account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)) {
-      return false;
-    }
+    if (account != null) {
+      // case when account in query is numeric
+      if (!account.equals(approver) && !isMagicUser()) {
+        return false;
+      }
 
-    if (account != null
-        && account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
-        && !cd.change().getOwner().equals(approver)) {
-      return false;
+      // case when account in query = owner
+      if (account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
+          && !cd.change().getOwner().equals(approver)) {
+        return false;
+      }
+
+      // case when account in query = non_uploader
+      if (account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
+          && cd.currentPatchSet().uploader().equals(approver)) {
+        return false;
+      }
     }
 
     IdentifiedUser reviewer = userFactory.create(approver);
@@ -139,6 +146,11 @@
     }
   }
 
+  private boolean isMagicUser() {
+    return account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
+        || account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID);
+  }
+
   @Override
   public int getCost() {
     return 1 + (group == null ? 0 : 1);
diff --git a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
deleted file mode 100644
index 6482a19..0000000
--- a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class HasStarsPredicate extends ChangeIndexPredicate {
-  protected final Account.Id accountId;
-
-  public HasStarsPredicate(Account.Id accountId) {
-    super(ChangeField.STARBY, accountId.toString());
-    this.accountId = accountId;
-  }
-
-  @Override
-  public boolean match(ChangeData cd) {
-    return cd.stars().containsKey(accountId);
-  }
-
-  @Override
-  public String toString() {
-    return ChangeQueryBuilder.FIELD_STARBY + ":" + accountId;
-  }
-}
diff --git a/java/com/google/gerrit/server/query/change/IsAttentionPredicate.java b/java/com/google/gerrit/server/query/change/IsAttentionPredicate.java
new file mode 100644
index 0000000..d20d64a
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/IsAttentionPredicate.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class IsAttentionPredicate extends IntegerRangeChangePredicate {
+  public IsAttentionPredicate() throws QueryParseException {
+    this(">0");
+  }
+
+  public IsAttentionPredicate(String value) throws QueryParseException {
+    super(ChangeField.ATTENTION_SET_USERS_COUNT, value);
+  }
+
+  @Override
+  protected Integer getValueInt(ChangeData changeData) {
+    return ChangeField.ATTENTION_SET_USERS_COUNT.get(changeData);
+  }
+}
diff --git a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 3a43fd3..054a69e 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -36,14 +36,13 @@
 
   protected final CurrentUser user;
 
-  public IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, boolean checkIsVisible)
-      throws QueryParseException {
-    super(filters(args, checkIsVisible));
+  public IsWatchedByPredicate(ChangeQueryBuilder.Arguments args) throws QueryParseException {
+    super(filters(args));
     this.user = args.getUser();
   }
 
-  protected static List<Predicate<ChangeData>> filters(
-      ChangeQueryBuilder.Arguments args, boolean checkIsVisible) throws QueryParseException {
+  protected static List<Predicate<ChangeData>> filters(ChangeQueryBuilder.Arguments args)
+      throws QueryParseException {
     List<Predicate<ChangeData>> r = new ArrayList<>();
     ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
     for (ProjectWatchKey w : getWatches(args)) {
@@ -82,11 +81,8 @@
     }
     if (r.isEmpty()) {
       return ImmutableList.of(ChangeIndexPredicate.none());
-    } else if (checkIsVisible) {
-      return ImmutableList.of(or(r), builder.isVisible());
-    } else {
-      return ImmutableList.of(or(r));
     }
+    return ImmutableList.of(or(r));
   }
 
   protected static Collection<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args)
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 989b4bb..5f017fb 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -87,7 +87,7 @@
 
     try {
       MagicLabelVote mlv = MagicLabelVote.parseWithEquals(v);
-      return ImmutableList.of(new MagicLabelPredicate(args, mlv));
+      return ImmutableList.of(magicLabelPredicate(args, mlv));
     } catch (IllegalArgumentException e) {
       // Try next format.
     }
@@ -157,6 +157,17 @@
     return or(r);
   }
 
+  protected static Predicate<ChangeData> magicLabelPredicate(Args args, MagicLabelVote mlv) {
+    if (args.accounts == null || args.accounts.isEmpty()) {
+      return new MagicLabelPredicate(args, mlv, /* account= */ null);
+    }
+    List<Predicate<ChangeData>> r = new ArrayList<>();
+    for (Account.Id a : args.accounts) {
+      r.add(new MagicLabelPredicate(args, mlv, a));
+    }
+    return or(r);
+  }
+
   @Override
   public String toString() {
     return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 2c56322..3917c79 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.LabelTypes;
@@ -28,9 +29,12 @@
 public class MagicLabelPredicate extends ChangeIndexPredicate {
   protected final LabelPredicate.Args args;
   private final MagicLabelVote magicLabelVote;
+  private final Account.Id account;
 
-  public MagicLabelPredicate(LabelPredicate.Args args, MagicLabelVote magicLabelVote) {
+  public MagicLabelPredicate(
+      LabelPredicate.Args args, MagicLabelVote magicLabelVote, Account.Id account) {
     super(ChangeField.LABEL, magicLabelVote.formatLabel());
+    this.account = account;
     this.args = args;
     this.magicLabelVote = magicLabelVote;
   }
@@ -83,7 +87,7 @@
   }
 
   private EqualsLabelPredicate numericPredicate(String label, short value) {
-    return new EqualsLabelPredicate(args, label, value, /* account= */ null);
+    return new EqualsLabelPredicate(args, label, value, account);
   }
 
   protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/PredicateArgs.java b/java/com/google/gerrit/server/query/change/PredicateArgs.java
index ad7917e..d82b9bc 100644
--- a/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -40,7 +40,6 @@
    * name]}.
    *
    * @param args arguments to be parsed
-   * @throws QueryParseException
    */
   PredicateArgs(String args) throws QueryParseException {
     positional = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 6d3e222..f70379b 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -33,6 +33,7 @@
         "//lib/auto:auto-value-annotations",
         "//lib/commons:compress",
         "//lib/commons:lang",
+        "//lib/errorprone:annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index baa2951..b4946c4 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.group.GroupResolver;
 import com.google.gerrit.server.group.db.GroupDelta;
@@ -85,6 +86,7 @@
   private final Provider<GroupsUpdate> groupsUpdate;
   private final OutgoingEmailValidator validator;
   private final AuthConfig authConfig;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   CreateAccount(
@@ -97,7 +99,8 @@
       PluginSetContext<AccountExternalIdCreator> externalIdCreators,
       @UserInitiated Provider<GroupsUpdate> groupsUpdate,
       OutgoingEmailValidator validator,
-      AuthConfig authConfig) {
+      AuthConfig authConfig,
+      ExternalIdFactory externalIdFactory) {
     this.seq = seq;
     this.groupResolver = groupResolver;
     this.authorizedKeys = authorizedKeys;
@@ -108,6 +111,7 @@
     this.groupsUpdate = groupsUpdate;
     this.validator = validator;
     this.authConfig = authConfig;
+    this.externalIdFactory = externalIdFactory;
   }
 
   @Override
@@ -142,10 +146,10 @@
       if (!validator.isValid(input.email)) {
         throw new BadRequestException("invalid email address");
       }
-      extIds.add(ExternalId.createEmail(accountId, input.email));
+      extIds.add(externalIdFactory.createEmail(accountId, input.email));
     }
 
-    extIds.add(ExternalId.createUsername(username, accountId, input.httpPassword));
+    extIds.add(externalIdFactory.createUsername(username, accountId, input.httpPassword));
     externalIdCreators.runEach(c -> extIds.addAll(c.create(accountId, username, input.email)));
 
     try {
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 6ee4539..70fbb26 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -83,6 +83,7 @@
   private final OutgoingEmailValidator validator;
   private final MessageIdGenerator messageIdGenerator;
   private final boolean isDevMode;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   CreateEmail(
@@ -94,7 +95,8 @@
       RegisterNewEmailSender.Factory registerNewEmailFactory,
       PutPreferred putPreferred,
       OutgoingEmailValidator validator,
-      MessageIdGenerator messageIdGenerator) {
+      MessageIdGenerator messageIdGenerator,
+      AuthRequest.Factory authRequestFactory) {
     this.self = self;
     this.realm = realm;
     this.permissionBackend = permissionBackend;
@@ -104,6 +106,7 @@
     this.validator = validator;
     this.isDevMode = authConfig.getAuthType() == DEVELOPMENT_BECOME_ANY_ACCOUNT;
     this.messageIdGenerator = messageIdGenerator;
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -151,7 +154,7 @@
         logger.atWarning().log("skipping email validation in developer mode");
       }
       try {
-        accountManager.link(user.getAccountId(), AuthRequest.forEmail(email));
+        accountManager.link(user.getAccountId(), authRequestFactory.createForEmail(email));
       } catch (AccountException e) {
         throw new ResourceConflictException(e.getMessage());
       }
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index 445a5d6..e099a70 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -18,6 +18,8 @@
 import static java.util.stream.Collectors.toMap;
 import static java.util.stream.Collectors.toSet;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -29,6 +31,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -56,17 +59,20 @@
   private final AccountManager accountManager;
   private final ExternalIds externalIds;
   private final Provider<CurrentUser> self;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
   DeleteExternalIds(
       PermissionBackend permissionBackend,
       AccountManager accountManager,
       ExternalIds externalIds,
-      Provider<CurrentUser> self) {
+      Provider<CurrentUser> self,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.permissionBackend = permissionBackend;
     this.accountManager = accountManager;
     this.externalIds = externalIds;
     this.self = self;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   @Override
@@ -87,15 +93,24 @@
     List<ExternalId> toDelete = new ArrayList<>();
     Optional<ExternalId.Key> last = resource.getUser().getLastLoginExternalIdKey();
     for (String externalIdStr : extIds) {
-      ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr));
+      ExternalId id = externalIdMap.get(externalIdKeyFactory.parse(externalIdStr));
 
       if (id == null) {
         throw new UnprocessableEntityException(
             String.format("External id %s does not exist", externalIdStr));
       }
 
-      if ((!id.isScheme(SCHEME_USERNAME))
-          && (!last.isPresent() || (!last.get().equals(id.key())))) {
+      if (!last.isPresent() || !last.get().equals(id.key())) {
+        if (id.isScheme(SCHEME_USERNAME)) {
+          if (self.get().hasSameAccountId(resource.getUser())) {
+            throw new AuthException("User cannot delete its own externalId in 'username:' scheme");
+          }
+          permissionBackend
+              .currentUser()
+              .checkAny(
+                  ImmutableSet.of(
+                      GlobalPermission.ADMINISTRATE_SERVER, GlobalPermission.MAINTAIN_SERVER));
+        }
         toDelete.add(id);
       } else {
         throw new ResourceConflictException(
diff --git a/java/com/google/gerrit/server/restapi/account/Module.java b/java/com/google/gerrit/server/restapi/account/Module.java
index 7570465..dd38ccf 100644
--- a/java/com/google/gerrit/server/restapi/account/Module.java
+++ b/java/com/google/gerrit/server/restapi/account/Module.java
@@ -99,10 +99,6 @@
     delete(STARRED_CHANGE_KIND).to(StarredChanges.Delete.class);
     bind(StarredChanges.Create.class);
 
-    child(ACCOUNT_KIND, "stars.changes").to(Stars.class);
-    get(STAR_KIND).to(Stars.Get.class);
-    post(STAR_KIND).to(Stars.Post.class);
-
     get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class);
     post(ACCOUNT_KIND, "external.ids:delete").to(DeleteExternalIds.class);
 
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 2427def..9361e27 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -34,6 +34,8 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -77,6 +79,8 @@
   private final ExternalIds externalIds;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory;
+  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
   PutHttpPassword(
@@ -84,12 +88,16 @@
       PermissionBackend permissionBackend,
       ExternalIds externalIds,
       @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
-      HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory) {
+      HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory,
+      ExternalIdFactory externalIdFactory,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.httpPasswordUpdateSenderFactory = httpPasswordUpdateSenderFactory;
+    this.externalIdFactory = externalIdFactory;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   @Override
@@ -125,7 +133,7 @@
     String userName =
         user.getUserName().orElseThrow(() -> new ResourceConflictException("username must be set"));
     Optional<ExternalId> optionalExtId =
-        externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, userName));
+        externalIds.get(externalIdKeyFactory.create(SCHEME_USERNAME, userName));
     ExternalId extId = optionalExtId.orElseThrow(ResourceNotFoundException::new);
     accountsUpdateProvider
         .get()
@@ -134,7 +142,7 @@
             extId.accountId(),
             u ->
                 u.updateExternalId(
-                    ExternalId.createWithPassword(
+                    externalIdFactory.createWithPassword(
                         extId.key(), extId.accountId(), extId.email(), newPassword)));
 
     try {
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index 32b5ff2..aee0b78 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -62,17 +63,20 @@
   private final PermissionBackend permissionBackend;
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final ExternalIds externalIds;
+  private final ExternalIdFactory externalIdFactory;
 
   @Inject
   PutPreferred(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
-      ExternalIds externalIds) {
+      ExternalIds externalIds,
+      ExternalIdFactory externalIdFactory) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.externalIds = externalIds;
+    this.externalIdFactory = externalIdFactory;
   }
 
   @Override
@@ -137,7 +141,8 @@
                     }
 
                     // claim the email now
-                    u.addExternalId(ExternalId.createEmail(a.account().id(), preferredEmail));
+                    u.addExternalId(
+                        externalIdFactory.createEmail(a.account().id(), preferredEmail));
                     matchingEmail = preferredEmail;
                   } else {
                     // Realm says that the email doesn't belong to the user. This can only happen as
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 05bf1fd..f295389 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -34,6 +34,8 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -65,6 +67,8 @@
   private final Provider<AccountsUpdate> accountsUpdateProvider;
   private final SshKeyCache sshKeyCache;
   private final Realm realm;
+  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdKeyFactory externalIdKeyFactory;
 
   @Inject
   PutUsername(
@@ -73,13 +77,17 @@
       ExternalIds externalIds,
       @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
       SshKeyCache sshKeyCache,
-      Realm realm) {
+      Realm realm,
+      ExternalIdFactory externalIdFactory,
+      ExternalIdKeyFactory externalIdKeyFactory) {
     this.self = self;
     this.permissionBackend = permissionBackend;
     this.externalIds = externalIds;
     this.accountsUpdateProvider = accountsUpdateProvider;
     this.sshKeyCache = sshKeyCache;
     this.realm = realm;
+    this.externalIdFactory = externalIdFactory;
+    this.externalIdKeyFactory = externalIdKeyFactory;
   }
 
   @Override
@@ -107,14 +115,14 @@
       throw new UnprocessableEntityException("invalid username");
     }
 
-    ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, input.username);
+    ExternalId.Key key = externalIdKeyFactory.create(SCHEME_USERNAME, input.username);
     try {
       accountsUpdateProvider
           .get()
           .update(
               "Set Username via API",
               accountId,
-              u -> u.addExternalId(ExternalId.create(key, accountId, null, null)));
+              u -> u.addExternalId(externalIdFactory.create(key, accountId, null, null)));
     } catch (DuplicateKeyException dupeErr) {
       // If we are using this identity, don't report the exception.
       Optional<ExternalId> other = externalIds.get(key);
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index e67fe9e..39c1fef 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -90,7 +90,7 @@
     return (RestReadView<AccountResource>)
         self -> {
           QueryChanges query = changes.list();
-          query.addQuery("starredby:" + self.getUser().getAccountId().get());
+          query.addQuery("has:star");
           return query.apply(TopLevelResource.INSTANCE);
         };
   }
diff --git a/java/com/google/gerrit/server/restapi/account/Stars.java b/java/com/google/gerrit/server/restapi/account/Stars.java
deleted file mode 100644
index cc362f2..0000000
--- a/java/com/google/gerrit/server/restapi/account/Stars.java
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.account;
-
-import com.google.gerrit.extensions.api.changes.StarsInput;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ChildCollection;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.AccountResource.Star;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.restapi.change.ChangesCollection;
-import com.google.gerrit.server.restapi.change.QueryChanges;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-import java.util.SortedSet;
-
-/**
- * Implements adding label stars to changes.
- *
- * <p>This handles {@code POST} and {@code GET} for {@code
- * /accounts/<account-identifier>/stars.changes/<change ID>}.
- */
-@Singleton
-public class Stars implements ChildCollection<AccountResource, AccountResource.Star> {
-
-  private final ChangesCollection changes;
-  private final ListStarredChanges listStarredChanges;
-  private final StarredChangesUtil starredChangesUtil;
-  private final DynamicMap<RestView<AccountResource.Star>> views;
-
-  @Inject
-  Stars(
-      ChangesCollection changes,
-      ListStarredChanges listStarredChanges,
-      StarredChangesUtil starredChangesUtil,
-      DynamicMap<RestView<AccountResource.Star>> views) {
-    this.changes = changes;
-    this.listStarredChanges = listStarredChanges;
-    this.starredChangesUtil = starredChangesUtil;
-    this.views = views;
-  }
-
-  @Override
-  public Star parse(AccountResource parent, IdString id)
-      throws RestApiException, PermissionBackendException, IOException {
-    IdentifiedUser user = parent.getUser();
-    // This enforces visibility of the change.
-    ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
-    Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
-    return new AccountResource.Star(user, change, labels);
-  }
-
-  @Override
-  public DynamicMap<RestView<Star>> views() {
-    return views;
-  }
-
-  @Override
-  public ListStarredChanges list() {
-    return listStarredChanges;
-  }
-
-  @Singleton
-  public static class ListStarredChanges implements RestReadView<AccountResource> {
-
-    private final Provider<CurrentUser> self;
-    private final ChangesCollection changes;
-
-    @Inject
-    ListStarredChanges(Provider<CurrentUser> self, ChangesCollection changes) {
-      this.self = self;
-      this.changes = changes;
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public Response<List<ChangeInfo>> apply(AccountResource rsrc)
-        throws RestApiException, PermissionBackendException {
-      if (!self.get().hasSameAccountId(rsrc.getUser())) {
-        throw new AuthException("not allowed to list stars of another account");
-      }
-
-      // The type of the value in the response that is returned by QueryChanges depends on the
-      // number of queries that is provided as input. If a single query is provided as input the
-      // value type is {@code List<ChangeInfo>}, if multiple queries are provided as input the value
-      // type is {@code List<List<ChangeInfo>>) (one {@code List<ChangeInfo>} as result to each
-      // query). Since in this case we provide exactly one query ("has:stars") as input we know that
-      // the value always has the type {@code List<ChangeInfo>} and hence we can safely cast the
-      // value to this type.
-      QueryChanges query = changes.list();
-      query.addQuery("has:stars");
-      Response<?> response = query.apply(TopLevelResource.INSTANCE);
-      List<ChangeInfo> value = (List<ChangeInfo>) response.value();
-      return Response.ok(value);
-    }
-  }
-
-  @Singleton
-  public static class Get implements RestReadView<AccountResource.Star> {
-
-    private final Provider<CurrentUser> self;
-    private final StarredChangesUtil starredChangesUtil;
-
-    @Inject
-    Get(Provider<CurrentUser> self, StarredChangesUtil starredChangesUtil) {
-      this.self = self;
-      this.starredChangesUtil = starredChangesUtil;
-    }
-
-    @Override
-    public Response<SortedSet<String>> apply(AccountResource.Star rsrc) throws AuthException {
-      if (!self.get().hasSameAccountId(rsrc.getUser())) {
-        throw new AuthException("not allowed to get stars of another account");
-      }
-      return Response.ok(
-          starredChangesUtil.getLabels(self.get().getAccountId(), rsrc.getChange().getId()));
-    }
-  }
-
-  @Singleton
-  public static class Post implements RestModifyView<AccountResource.Star, StarsInput> {
-
-    private final Provider<CurrentUser> self;
-    private final StarredChangesUtil starredChangesUtil;
-
-    @Inject
-    Post(Provider<CurrentUser> self, StarredChangesUtil starredChangesUtil) {
-      this.self = self;
-      this.starredChangesUtil = starredChangesUtil;
-    }
-
-    @Override
-    public Response<Collection<String>> apply(AccountResource.Star rsrc, StarsInput in)
-        throws AuthException, BadRequestException {
-      if (!self.get().hasSameAccountId(rsrc.getUser())) {
-        throw new AuthException("not allowed to update stars of another account");
-      }
-      try {
-        return Response.ok(
-            starredChangesUtil.star(
-                self.get().getAccountId(),
-                rsrc.getChange().getProject(),
-                rsrc.getChange().getId(),
-                in.add,
-                in.remove));
-      } catch (IllegalLabelException e) {
-        throw new BadRequestException(e.getMessage());
-      }
-    }
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
index 67b5870..517fbdf 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeIncludedIn.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.IncludedIn;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -38,7 +39,8 @@
   }
 
   @Override
-  public Response<IncludedInInfo> apply(ChangeResource rsrc) throws RestApiException, IOException {
+  public Response<IncludedInInfo> apply(ChangeResource rsrc)
+      throws RestApiException, IOException, PermissionBackendException {
     PatchSet ps = psUtil.current(rsrc.getNotes());
     return Response.ok(includedIn.apply(rsrc.getProject(), ps.commitId().name()));
   }
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
deleted file mode 100644
index fa4555b..0000000
--- a/java/com/google/gerrit/server/restapi/change/MarkAsReviewed.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.common.Input;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-@Singleton
-public class MarkAsReviewed
-    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final ChangeData.Factory changeDataFactory;
-  private final StarredChangesUtil stars;
-
-  @Inject
-  MarkAsReviewed(ChangeData.Factory changeDataFactory, StarredChangesUtil stars) {
-    this.changeDataFactory = changeDataFactory;
-    this.stars = stars;
-  }
-
-  @Override
-  public Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Mark Reviewed")
-        .setTitle("Mark the change as reviewed to unhighlight it in the dashboard")
-        .setVisible(!isReviewed(rsrc));
-  }
-
-  @Override
-  public Response<String> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, IllegalLabelException {
-    stars.markAsReviewed(rsrc);
-    return Response.ok();
-  }
-
-  private boolean isReviewed(ChangeResource rsrc) {
-    try {
-      return changeDataFactory
-          .create(rsrc.getNotes())
-          .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("failed to check if change is reviewed");
-    }
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java b/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
deleted file mode 100644
index 601fc4a..0000000
--- a/java/com/google/gerrit/server/restapi/change/MarkAsUnreviewed.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.restapi.change;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.common.Input;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.StarredChangesUtil.IllegalLabelException;
-import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-@Singleton
-public class MarkAsUnreviewed
-    implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
-  private final ChangeData.Factory changeDataFactory;
-  private final StarredChangesUtil stars;
-
-  @Inject
-  MarkAsUnreviewed(ChangeData.Factory changeDataFactory, StarredChangesUtil stars) {
-    this.changeDataFactory = changeDataFactory;
-    this.stars = stars;
-  }
-
-  @Override
-  public Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Mark Unreviewed")
-        .setTitle("Mark the change as unreviewed to highlight it in the dashboard")
-        .setVisible(isReviewed(rsrc));
-  }
-
-  @Override
-  public Response<String> apply(ChangeResource rsrc, Input input) throws IllegalLabelException {
-    stars.markAsUnreviewed(rsrc);
-    return Response.ok();
-  }
-
-  private boolean isReviewed(ChangeResource rsrc) {
-    try {
-      return changeDataFactory
-          .create(rsrc.getNotes())
-          .isReviewedBy(rsrc.getUser().asIdentifiedUser().getAccountId());
-    } catch (StorageException e) {
-      logger.atSevere().withCause(e).log("failed to check if change is reviewed");
-    }
-    return false;
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index f87c9a1..000a17e 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -120,8 +120,6 @@
     delete(CHANGE_KIND, "private").to(DeletePrivate.class);
     put(CHANGE_KIND, "ignore").to(Ignore.class);
     put(CHANGE_KIND, "unignore").to(Unignore.class);
-    put(CHANGE_KIND, "reviewed").to(MarkAsReviewed.class);
-    put(CHANGE_KIND, "unreviewed").to(MarkAsUnreviewed.class);
     post(CHANGE_KIND, "wip").to(SetWorkInProgress.class);
     post(CHANGE_KIND, "ready").to(SetReadyForReview.class);
     put(CHANGE_KIND, "message").to(PutMessage.class);
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 4dbb6ee..5002a82 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -80,6 +80,10 @@
 import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CommentsUtil;
@@ -151,6 +155,29 @@
 public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  @Singleton
+  private static class Metrics {
+    final Counter1<String> draftHandling;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      draftHandling =
+          metricMaker.newCounter(
+              "change/post_review/draft_handling",
+              new Description(
+                      "Total number of draft handling option "
+                          + "(KEEP, PUBLISH, PUBLISH_ALL_REVISIONS) "
+                          + "selected by users while posting a review.")
+                  .setRate()
+                  .setUnit("count"),
+              Field.ofString("type", Metadata.Builder::eventType)
+                  .description(
+                      "The type of the draft handling option"
+                          + " (KEEP, PUBLISH, PUBLISH_ALL_REVISIONS).")
+                  .build());
+    }
+  }
+
   private static final String ERROR_ADDING_REVIEWER = "error adding reviewer";
   public static final String ERROR_WIP_READY_MUTUALLY_EXCLUSIVE =
       "work_in_progress and ready are mutually exclusive";
@@ -170,6 +197,7 @@
   private final EmailReviewComments.Factory email;
   private final CommentAdded commentAdded;
   private final ReviewerModifier reviewerModifier;
+  private final Metrics metrics;
   private final ModifyReviewersEmail modifyReviewersEmail;
   private final NotifyResolver notifyResolver;
   private final WorkInProgressOp.Factory workInProgressOpFactory;
@@ -196,6 +224,7 @@
       EmailReviewComments.Factory email,
       CommentAdded commentAdded,
       ReviewerModifier reviewerModifier,
+      Metrics metrics,
       ModifyReviewersEmail modifyReviewersEmail,
       NotifyResolver notifyResolver,
       @GerritServerConfig Config gerritConfig,
@@ -218,6 +247,7 @@
     this.email = email;
     this.commentAdded = commentAdded;
     this.reviewerModifier = reviewerModifier;
+    this.metrics = metrics;
     this.modifyReviewersEmail = modifyReviewersEmail;
     this.notifyResolver = notifyResolver;
     this.workInProgressOpFactory = workInProgressOpFactory;
@@ -252,6 +282,7 @@
 
     logger.atFine().log("strict label checking is %s", (strictLabels ? "enabled" : "disabled"));
 
+    metrics.draftHandling.increment(input.drafts == null ? "N/A" : input.drafts.name());
     input.drafts = firstNonNull(input.drafts, DraftHandling.KEEP);
     logger.atFine().log("draft handling = %s", input.drafts);
 
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index 1d550f1..0634081 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -19,6 +19,7 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
@@ -266,8 +267,9 @@
       return psId().changeId();
     }
 
+    @Memoized
     @Override
-    public final int hashCode() {
+    public int hashCode() {
       return Objects.hash(patchSet().id(), commit());
     }
 
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index f44abec..4cb2f47 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -94,6 +94,7 @@
   private final AccountCache accountCache;
   private final AccountLoader.Factory infoFactory;
   private final Provider<GroupsUpdate> groupsUpdateProvider;
+  private final AuthRequest.Factory authRequestFactory;
 
   @Inject
   AddMembers(
@@ -102,13 +103,15 @@
       AccountResolver accountResolver,
       AccountCache accountCache,
       AccountLoader.Factory infoFactory,
-      @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
+      @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
+      AuthRequest.Factory authRequestFactory) {
     this.accountManager = accountManager;
     this.authType = authConfig.getAuthType();
     this.accountResolver = accountResolver;
     this.accountCache = accountCache;
     this.infoFactory = infoFactory;
     this.groupsUpdateProvider = groupsUpdateProvider;
+    this.authRequestFactory = authRequestFactory;
   }
 
   @Override
@@ -190,7 +193,7 @@
     }
 
     try {
-      AuthRequest req = AuthRequest.forUser(user);
+      AuthRequest req = authRequestFactory.createForUser(user);
       req.setSkipAuthentication(true);
       return accountCache
           .get(accountManager.authenticate(req).getAccountId())
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 37616cd..5c2f932 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -95,7 +95,6 @@
       } catch (AuthException e) {
         return Response.ok(
             createInfo(
-                traceContext,
                 HttpServletResponse.SC_FORBIDDEN,
                 String.format("user %s cannot see project %s", match, rsrc.getName())));
       }
@@ -126,7 +125,6 @@
         } catch (AuthException e) {
           return Response.ok(
               createInfo(
-                  traceContext,
                   HttpServletResponse.SC_FORBIDDEN,
                   String.format(
                       "user %s lacks permission %s for %s in project %s",
@@ -141,15 +139,15 @@
           }
         }
       }
-      return Response.ok(createInfo(traceContext, HttpServletResponse.SC_OK, message));
+      return Response.ok(createInfo(HttpServletResponse.SC_OK, message));
     }
   }
 
-  private AccessCheckInfo createInfo(TraceContext traceContext, int statusCode, String message) {
+  private AccessCheckInfo createInfo(int statusCode, String message) {
     AccessCheckInfo info = new AccessCheckInfo();
     info.status = statusCode;
     info.message = message;
-    info.debugLogs = traceContext.getAclLogRecords();
+    info.debugLogs = TraceContext.getAclLogRecords();
     if (info.debugLogs.isEmpty()) {
       info.debugLogs =
           ImmutableList.of("Found no rules that apply, so defaulting to no permission");
diff --git a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
index a4a82ce..e566858 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitIncludedIn.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.change.IncludedIn;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.CommitResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -36,7 +37,8 @@
   }
 
   @Override
-  public Response<IncludedInInfo> apply(CommitResource rsrc) throws RestApiException, IOException {
+  public Response<IncludedInInfo> apply(CommitResource rsrc)
+      throws RestApiException, IOException, PermissionBackendException {
     RevCommit commit = rsrc.getCommit();
     Project.NameKey project = rsrc.getProjectState().getNameKey();
     return Response.ok(includedIn.apply(project, commit.getId().getName()));
diff --git a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
index ae7f540..09951b2 100644
--- a/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/CommitsCollection.java
@@ -116,14 +116,14 @@
   }
 
   /**
-   * @return true if {@code commit} is visible to the caller and {@code commit} is reachable from
-   *     the given branch.
+   * Returns true if {@code commit} is visible to the caller and {@code commit} is reachable from
+   * the given branch.
    */
   public boolean canRead(ProjectState state, Repository repo, RevCommit commit, Ref ref) {
     return reachable.fromRefs(state.getNameKey(), repo, commit, ImmutableList.of(ref));
   }
 
-  /** @return true if {@code commit} is visible to the caller. */
+  /** Returns true if {@code commit} is visible to the caller. */
   public boolean canRead(ProjectState state, Repository repo, RevCommit commit) throws IOException {
     Project.NameKey project = state.getNameKey();
     if (indexes.getSearchIndex() == null) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 4e13ba9..60405a6 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -86,8 +86,6 @@
    *
    * @param projectState the {@code ProjectState} of the project containing the target ref.
    * @param ref the ref to be deleted.
-   * @throws IOException
-   * @throws ResourceConflictException
    */
   public void deleteSingleRef(ProjectState projectState, String ref)
       throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
@@ -100,8 +98,6 @@
    * @param projectState the {@code ProjectState} of the project containing the target ref.
    * @param ref the ref to be deleted.
    * @param prefix the prefix of the ref.
-   * @throws IOException
-   * @throws ResourceConflictException
    */
   public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix)
       throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
@@ -161,9 +157,6 @@
    * @param projectState the {@code ProjectState} of the project whose refs are to be deleted.
    * @param refsToDelete the refs to be deleted.
    * @param prefix the prefix to add to abbreviated refs, eg. "refs/heads/".
-   * @throws IOException
-   * @throws ResourceConflictException
-   * @throws PermissionBackendException
    */
   public void deleteMultipleRefs(
       ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix)
diff --git a/java/com/google/gerrit/server/restapi/project/IndexChanges.java b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
index 73a064c..6ad0005 100644
--- a/java/com/google/gerrit/server/restapi/project/IndexChanges.java
+++ b/java/com/google/gerrit/server/restapi/project/IndexChanges.java
@@ -24,9 +24,9 @@
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.change.AllChangesIndexer;
 import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -41,18 +41,18 @@
 @Singleton
 public class IndexChanges implements RestModifyView<ProjectResource, Input> {
 
-  private final ExperimentFeatures experimentFeatures;
+  private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
   private final Provider<AllChangesIndexer> allChangesIndexerProvider;
   private final ChangeIndexer indexer;
   private final ListeningExecutorService executor;
 
   @Inject
   IndexChanges(
-      ExperimentFeatures experimentFeatures,
+      MultiProgressMonitor.Factory multiProgressMonitorFactory,
       Provider<AllChangesIndexer> allChangesIndexerProvider,
       ChangeIndexer indexer,
       @IndexExecutor(BATCH) ListeningExecutorService executor) {
-    this.experimentFeatures = experimentFeatures;
+    this.multiProgressMonitorFactory = multiProgressMonitorFactory;
     this.allChangesIndexerProvider = allChangesIndexerProvider;
     this.indexer = indexer;
     this.executor = executor;
@@ -62,8 +62,8 @@
   public Response.Accepted apply(ProjectResource resource, Input input) {
     Project.NameKey project = resource.getNameKey();
     Task mpt =
-        new MultiProgressMonitor(
-                experimentFeatures, ByteStreams.nullOutputStream(), "Reindexing project")
+        multiProgressMonitorFactory
+            .create(ByteStreams.nullOutputStream(), TaskKind.INDEXING, "Reindexing project")
             .beginSubTask("", MultiProgressMonitor.UNKNOWN);
     AllChangesIndexer allChangesIndexer = allChangesIndexerProvider.get();
     allChangesIndexer.setVerboseOut(NullOutputStream.INSTANCE);
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index c4ae33a..4d8005b 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -490,7 +490,7 @@
                 continue;
               }
 
-              List<Ref> refs = retrieveBranchRefs(e);
+              List<Ref> refs = retrieveBranchRefs(e, git);
               if (!hasValidRef(refs)) {
                 continue;
               }
@@ -578,17 +578,12 @@
     }
   }
 
-  private List<Ref> retrieveBranchRefs(ProjectState e) throws PermissionBackendException {
-    boolean canReadAllRefs = e.statePermitsRead();
-    if (canReadAllRefs) {
-      try {
-        permissionBackend.user(currentUser).project(e.getNameKey()).check(ProjectPermission.READ);
-      } catch (AuthException exp) {
-        canReadAllRefs = false;
-      }
+  private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
+    if (!e.statePermitsRead()) {
+      return ImmutableList.of();
     }
 
-    return getBranchRefs(e.getNameKey(), canReadAllRefs);
+    return getBranchRefs(e.getNameKey(), git);
   }
 
   private void addParentProjectInfo(
@@ -708,15 +703,13 @@
     stdout.flush();
   }
 
-  private List<Ref> getBranchRefs(Project.NameKey projectName, boolean canReadAllRefs) {
+  private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
     Ref[] result = new Ref[showBranch.size()];
-    try (Repository git = repoManager.openRepository(projectName)) {
+    try {
       PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
       for (int i = 0; i < showBranch.size(); i++) {
         Ref ref = git.findRef(showBranch.get(i));
-        if (all && canReadAllRefs) {
-          result[i] = ref;
-        } else if (ref != null && ref.getObjectId() != null) {
+        if (ref != null && ref.getObjectId() != null) {
           try {
             perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
             result[i] = ref;
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
index efc739c..6174798 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectsCollection.java
@@ -105,7 +105,6 @@
    * @throws RestApiException thrown if the project ID cannot be resolved or if the project is not
    *     visible to the calling user
    * @throws IOException thrown when there is an error.
-   * @throws PermissionBackendException
    */
   public ProjectResource parse(String id)
       throws RestApiException, IOException, PermissionBackendException {
@@ -121,7 +120,6 @@
    * @throws RestApiException thrown if the project ID cannot be resolved or if the project is not
    *     visible to the calling user and checkVisibility is true.
    * @throws IOException thrown when there is an error.
-   * @throws PermissionBackendException
    */
   public ProjectResource parse(String id, boolean checkAccess)
       throws RestApiException, IOException, PermissionBackendException {
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 7d626da..bc0bb1a 100644
--- a/java/com/google/gerrit/server/rules/PrologEnvironment.java
+++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.DiffOperations;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
@@ -173,7 +173,7 @@
     private final PermissionBackend permissionBackend;
     private final GitRepositoryManager repositoryManager;
     private final PluginConfigFactory pluginConfigFactory;
-    private final PatchListCache patchListCache;
+    private final DiffOperations diffOperations;
     private final PatchSetInfoFactory patchSetInfoFactory;
     private final IdentifiedUser.GenericFactory userFactory;
     private final Provider<AnonymousUser> anonymousUser;
@@ -188,7 +188,7 @@
         PermissionBackend permissionBackend,
         GitRepositoryManager repositoryManager,
         PluginConfigFactory pluginConfigFactory,
-        PatchListCache patchListCache,
+        DiffOperations diffOperations,
         PatchSetInfoFactory patchSetInfoFactory,
         IdentifiedUser.GenericFactory userFactory,
         Provider<AnonymousUser> anonymousUser,
@@ -199,7 +199,7 @@
       this.permissionBackend = permissionBackend;
       this.repositoryManager = repositoryManager;
       this.pluginConfigFactory = pluginConfigFactory;
-      this.patchListCache = patchListCache;
+      this.diffOperations = diffOperations;
       this.patchSetInfoFactory = patchSetInfoFactory;
       this.userFactory = userFactory;
       this.anonymousUser = anonymousUser;
@@ -237,8 +237,8 @@
       return pluginConfigFactory;
     }
 
-    public PatchListCache getPatchListCache() {
-      return patchListCache;
+    public DiffOperations getDiffOperations() {
+      return diffOperations;
     }
 
     public PatchSetInfoFactory getPatchSetInfoFactory() {
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index 1e08a24..1d10c1f 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
@@ -30,10 +29,9 @@
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffOperations;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -87,24 +85,27 @@
         }
       };
 
-  public static final StoredValue<PatchList> PATCH_LIST =
-      new StoredValue<PatchList>() {
+  public static final StoredValue<Map<String, FileDiffOutput>> DIFF_LIST =
+      new StoredValue<Map<String, FileDiffOutput>>() {
         @Override
-        public PatchList createValue(Prolog engine) {
+        public Map<String, FileDiffOutput> createValue(Prolog engine) {
           PrologEnvironment env = (PrologEnvironment) engine.control;
           PatchSet ps = getPatchSet(engine);
-          PatchListCache plCache = env.getArgs().getPatchListCache();
+          DiffOperations diffOperations = env.getArgs().getDiffOperations();
           Change change = getChange(engine);
           Project.NameKey project = change.getProject();
-          Whitespace ws = Whitespace.IGNORE_NONE;
-          PatchListKey plKey = PatchListKey.againstDefaultBase(ps.commitId(), ws);
-          PatchList patchList;
+          Map<String, FileDiffOutput> diffList;
           try {
-            patchList = plCache.get(plKey, project);
-          } catch (PatchListNotAvailableException e) {
-            throw new SystemException(String.format("Cannot create %s: %s", plKey, e.getMessage()));
+            diffList =
+                diffOperations.listModifiedFilesAgainstParent(
+                    project, ps.commitId(), /* parentNum= */ 0);
+          } catch (DiffNotAvailableException e) {
+            throw new SystemException(
+                String.format(
+                    "Cannot create modified files for project %s, commit Id %s: %s",
+                    project, ps.commitId(), e.getMessage()));
           }
-          return patchList;
+          return diffList;
         }
       };
 
diff --git a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
index 868e7ea..3bf9d02 100644
--- a/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
+++ b/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -18,12 +18,11 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.PermissionRule;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.config.AllProjectsConfigProvider;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.project.ProjectConfig;
@@ -31,6 +30,7 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -40,32 +40,30 @@
 
 public class ProjectConfigSchemaUpdate extends VersionedMetaData {
   public static class Factory {
-    private final SitePaths sitePaths;
     private final AllProjectsName allProjectsName;
+    private final AllProjectsConfigProvider allProjectsConfigProvider;
 
     @Inject
-    Factory(SitePaths sitePaths, AllProjectsName allProjectsName) {
-      this.sitePaths = sitePaths;
+    Factory(AllProjectsName allProjectsName, AllProjectsConfigProvider allProjectsConfigProvider) {
       this.allProjectsName = allProjectsName;
+      this.allProjectsConfigProvider = allProjectsConfigProvider;
     }
 
     ProjectConfigSchemaUpdate read(MetaDataUpdate update)
         throws IOException, ConfigInvalidException {
       ProjectConfigSchemaUpdate r =
-          new ProjectConfigSchemaUpdate(
-              update,
-              ProjectConfig.Factory.getBaseConfig(sitePaths, allProjectsName, allProjectsName));
+          new ProjectConfigSchemaUpdate(update, allProjectsConfigProvider.get(allProjectsName));
       r.load(update);
       return r;
     }
   }
 
   private final MetaDataUpdate update;
-  @Nullable private final StoredConfig baseConfig;
+  private final Optional<StoredConfig> baseConfig;
   private Config config;
   private boolean updated;
 
-  private ProjectConfigSchemaUpdate(MetaDataUpdate update, @Nullable StoredConfig baseConfig) {
+  private ProjectConfigSchemaUpdate(MetaDataUpdate update, Optional<StoredConfig> baseConfig) {
     this.update = update;
     this.baseConfig = baseConfig;
   }
@@ -77,8 +75,8 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    if (baseConfig != null) {
-      baseConfig.load();
+    if (baseConfig.isPresent()) {
+      baseConfig.get().load();
     }
     config = readConfig(ProjectConfig.PROJECT_CONFIG, baseConfig);
   }
diff --git a/java/com/google/gerrit/server/securestore/SecureStore.java b/java/com/google/gerrit/server/securestore/SecureStore.java
index b5aebee..b53e38c 100644
--- a/java/com/google/gerrit/server/securestore/SecureStore.java
+++ b/java/com/google/gerrit/server/securestore/SecureStore.java
@@ -39,13 +39,7 @@
     public final String section;
     public final String subsection;
 
-    /**
-     * Creates EntryKey.
-     *
-     * @param section
-     * @param subsection
-     * @param name
-     */
+    /** Creates EntryKey */
     public EntryKey(String section, String subsection, String name) {
       this.name = name;
       this.section = section;
@@ -57,9 +51,6 @@
    * Extract decrypted value of stored property from SecureStore or {@code null} when property was
    * not found.
    *
-   * @param section
-   * @param subsection
-   * @param name
    * @return decrypted String value or {@code null} if not found
    */
   public final String get(String section, String subsection, String name) {
@@ -74,10 +65,6 @@
    * Extract decrypted value of stored plugin config property from SecureStore or {@code null} when
    * property was not found.
    *
-   * @param pluginName
-   * @param section
-   * @param subsection
-   * @param name
    * @return decrypted String value or {@code null} if not found
    */
   public final String getForPlugin(
@@ -93,10 +80,6 @@
    * Extract list of plugin config values from SecureStore and decrypt every value in that list, or
    * {@code null} when property was not found.
    *
-   * @param pluginName
-   * @param section
-   * @param subsection
-   * @param name
    * @return decrypted list of string values or {@code null}
    */
   public abstract String[] getListForPlugin(
@@ -106,9 +89,6 @@
    * Extract list of values from SecureStore and decrypt every value in that list or {@code null}
    * when property was not found.
    *
-   * @param section
-   * @param subsection
-   * @param name
    * @return decrypted list of string values or {@code null}
    */
   public abstract String[] getList(String section, String subsection, String name);
@@ -118,9 +98,6 @@
    *
    * <p>This method is responsible for encrypting value and storing it.
    *
-   * @param section
-   * @param subsection
-   * @param name
    * @param value plain text value
    */
   public final void set(String section, String subsection, String name, String value) {
@@ -132,26 +109,19 @@
    *
    * <p>This method is responsible for encrypting all values in the list and storing them.
    *
-   * @param section
-   * @param subsection
-   * @param name
    * @param values list of plain text values
    */
   public abstract void setList(String section, String subsection, String name, List<String> values);
 
   /**
    * Remove value for given {@code section}, {@code subsection} and {@code name} from SecureStore.
-   *
-   * @param section
-   * @param subsection
-   * @param name
    */
   public abstract void unset(String section, String subsection, String name);
 
-  /** @return list of stored entries. */
+  /** Returns list of stored entries. */
   public abstract Iterable<EntryKey> list();
 
-  /** @return <code>true</code> if currently loaded values are outdated */
+  /** Returns <code>true</code> if currently loaded values are outdated */
   public abstract boolean isOutdated();
 
   /** Reload the values */
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index f181c36..7d428eb 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -521,32 +521,22 @@
     }
   }
 
-  /**
-   * @see #updateRepo(RepoContext)
-   * @param ctx
-   */
+  /** See {@link #updateRepo(RepoContext)} */
   protected void updateRepoImpl(RepoContext ctx) throws Exception {}
 
   /**
-   * @see #updateChange(ChangeContext)
-   * @param ctx
-   * @return a new patch set if one was created by the submit strategy, or null if not.
+   * Returns a new patch set if one was created by the submit strategy, or null if not
+   *
+   * <p>See {@link #updateChange(ChangeContext)}
    */
   protected PatchSet updateChangeImpl(ChangeContext ctx) throws Exception {
     return null;
   }
 
-  /**
-   * @see #postUpdate(PostUpdateContext)
-   * @param ctx
-   */
+  /** See {@link #postUpdate(PostUpdateContext)} */
   protected void postUpdateImpl(PostUpdateContext ctx) throws Exception {}
 
-  /**
-   * Amend the commit with gitlink update
-   *
-   * @param commit
-   */
+  /** Amend the commit with gitlink update */
   protected CodeReviewCommit amendGitlink(CodeReviewCommit commit)
       throws IntegrationConflictException {
     if (!args.subscriptionGraph.hasSubscription(args.destBranch)) {
diff --git a/java/com/google/gerrit/server/tools/ToolsCatalog.java b/java/com/google/gerrit/server/tools/ToolsCatalog.java
index aaa366c..9c1483f 100644
--- a/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -175,28 +175,28 @@
       return type;
     }
 
-    /** @return the preferred UNIX file mode, e.g. {@code 0755}. */
+    /** Returns the preferred UNIX file mode, e.g. {@code 0755}. */
     public int getMode() {
       return mode;
     }
 
-    /** @return path of the entry, relative to the catalog root. */
+    /** Returns path of the entry, relative to the catalog root. */
     public String getPath() {
       return path;
     }
 
-    /** @return name of the entry, within its parent directory. */
+    /** Returns the name of the entry, within its parent directory. */
     public String getName() {
       final int s = path.lastIndexOf('/');
       return s < 0 ? path : path.substring(s + 1);
     }
 
-    /** @return collection of entries below this one, if this is a directory. */
+    /** Returns collection of entries below this one, if this is a directory. */
     public List<Entry> getChildren() {
       return Collections.unmodifiableList(children);
     }
 
-    /** @return a copy of the file's contents. */
+    /** Returns a copy of the file's contents. */
     public byte[] getBytes() {
       byte[] data = read(getPath());
 
diff --git a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
index c223aec..99c72f2 100644
--- a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
+++ b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
@@ -118,7 +118,7 @@
     }
   }
 
-  /** @return an unmodifiable view of commands. */
+  /** Returns an unmodifiable view of commands. */
   public Map<String, ReceiveCommand> getCommands() {
     return Collections.unmodifiableMap(commands);
   }
diff --git a/java/com/google/gerrit/server/update/ChangeContext.java b/java/com/google/gerrit/server/update/ChangeContext.java
index 5a53e2a..aeabde4 100644
--- a/java/com/google/gerrit/server/update/ChangeContext.java
+++ b/java/com/google/gerrit/server/update/ChangeContext.java
@@ -69,7 +69,7 @@
    */
   void deleteChange();
 
-  /** @return change corresponding to {@link #getNotes()}. */
+  /** Returns change corresponding to {@link #getNotes()}. */
   default Change getChange() {
     return requireNonNull(getNotes().getChange());
   }
diff --git a/java/com/google/gerrit/server/update/RepoContext.java b/java/com/google/gerrit/server/update/RepoContext.java
index 9faf628..66831cd 100644
--- a/java/com/google/gerrit/server/update/RepoContext.java
+++ b/java/com/google/gerrit/server/update/RepoContext.java
@@ -22,9 +22,9 @@
 /** Context for performing the {@link BatchUpdateOp#updateRepo} phase. */
 public interface RepoContext extends Context {
   /**
-   * @return inserter for writing to the repo. Callers should not flush; the walk returned by {@link
-   *     #getRevWalk()} is able to read back objects inserted by this inserter without flushing
-   *     first.
+   * Returns inserter for writing to the repo. Callers should not flush; the walk returned by {@link
+   * #getRevWalk()} is able to read back objects inserted by this inserter without flushing first.
+   *
    * @throws IOException if an error occurred opening the repo.
    */
   ObjectInserter getInserter() throws IOException;
diff --git a/java/com/google/gerrit/server/update/RetryHelper.java b/java/com/google/gerrit/server/update/RetryHelper.java
index b846662..7e6974c 100644
--- a/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/java/com/google/gerrit/server/update/RetryHelper.java
@@ -121,7 +121,9 @@
     @Inject
     Metrics(MetricMaker metricMaker) {
       Field<String> actionTypeField =
-          Field.ofString("action_type", Metadata.Builder::actionType).build();
+          Field.ofString("action_type", Metadata.Builder::actionType)
+              .description("The type of the action that was retried.")
+              .build();
       Field<String> operationNameField =
           Field.ofString("operation_name", Metadata.Builder::operationName)
               .description("The name of the operation that was retried.")
@@ -479,7 +481,7 @@
                   }
 
                   String cause = formatCause(t);
-                  if (!traceContext.isTracing()) {
+                  if (!TraceContext.isTracing()) {
                     String traceId = "retry-on-failure-" + new RequestId();
                     traceContext.addTag(RequestId.Type.TRACE_ID, traceId).forceLogging();
                     logger.atWarning().withCause(t).log(
@@ -548,8 +550,8 @@
    * @param retryer the retryer
    * @param listener metric listener
    * @return the result of executing the action
-   * @throws Throwable any error or exception that made the action fail, callers are expected to
-   *     catch and inspect this Throwable to decide carefully whether it should be re-thrown
+   * @throws Exception any exception that made the action fail, callers are expected to catch and
+   *     inspect this exception to decide carefully whether it should be re-thrown
    */
   private <T> T executeWithTimeoutCount(
       String actionType,
diff --git a/java/com/google/gerrit/server/update/RetryableAction.java b/java/com/google/gerrit/server/update/RetryableAction.java
index fbb643e..f79a849 100644
--- a/java/com/google/gerrit/server/update/RetryableAction.java
+++ b/java/com/google/gerrit/server/update/RetryableAction.java
@@ -57,6 +57,7 @@
     PLUGIN_UPDATE,
     REST_READ_REQUEST,
     REST_WRITE_REQUEST,
+    SEND_EMAIL,
   }
 
   @FunctionalInterface
diff --git a/java/com/google/gerrit/server/util/AttentionSetEmail.java b/java/com/google/gerrit/server/util/AttentionSetEmail.java
index 56b1dda..48ddd31 100644
--- a/java/com/google/gerrit/server/util/AttentionSetEmail.java
+++ b/java/com/google/gerrit/server/util/AttentionSetEmail.java
@@ -56,6 +56,7 @@
   }
 
   private ExecutorService sendEmailsExecutor;
+  private AccountTemplateUtil accountTemplateUtil;
   private AttentionSetSender sender;
   private Context ctx;
   private Change change;
@@ -67,6 +68,7 @@
   @Inject
   AttentionSetEmail(
       @SendEmailExecutor ExecutorService executor,
+      AccountTemplateUtil accountTemplateUtil,
       @Assisted AttentionSetSender sender,
       @Assisted Context ctx,
       @Assisted Change change,
@@ -74,6 +76,7 @@
       @Assisted MessageIdGenerator.MessageId messageId,
       @Assisted Account.Id attentionUserId) {
     this.sendEmailsExecutor = executor;
+    this.accountTemplateUtil = accountTemplateUtil;
     this.sender = sender;
     this.ctx = ctx;
     this.change = change;
@@ -97,7 +100,7 @@
       }
       sender.setNotify(ctx.getNotify(change.getId()));
       sender.setAttentionSetUser(attentionUserId);
-      sender.setReason(reason);
+      sender.setReason(accountTemplateUtil.replaceTemplates(reason));
       sender.setMessageId(messageId);
       sender.send();
     } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/util/RequestScopePropagator.java b/java/com/google/gerrit/server/util/RequestScopePropagator.java
index dc8a136..10c46fc 100644
--- a/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -160,7 +160,11 @@
     };
   }
 
-  /** @see #wrap(Callable) */
+  /**
+   * Ensures that the current request state is available when the passed in Callable is invoked
+   *
+   * <p>See {@link #wrap(Callable)}
+   */
   protected abstract <T> Callable<T> wrapImpl(Callable<T> callable);
 
   protected <T> Callable<T> context(RequestContext context, Callable<T> callable) {
diff --git a/java/com/google/gerrit/server/util/time/TimeUtil.java b/java/com/google/gerrit/server/util/time/TimeUtil.java
index 639d0a6..54ef305 100644
--- a/java/com/google/gerrit/server/util/time/TimeUtil.java
+++ b/java/com/google/gerrit/server/util/time/TimeUtil.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.util.git.DelegateSystemReader;
 import java.sql.Timestamp;
 import java.time.Instant;
+import java.util.concurrent.TimeUnit;
 import java.util.function.LongSupplier;
 import org.eclipse.jgit.util.SystemReader;
 
@@ -35,6 +36,10 @@
     return currentMillisSupplier.getAsLong();
   }
 
+  public static long nowNanos() {
+    return TimeUnit.NANOSECONDS.convert(TimeUtil.nowMs(), TimeUnit.MILLISECONDS);
+  }
+
   public static Instant now() {
     return Instant.ofEpochMilli(nowMs());
   }
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 42aabfb..f1be04e 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -73,6 +73,7 @@
   static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
   public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
 
+  @SuppressWarnings("unused") // unused here, but triggers logic in EndOfOptionsHandler
   @Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
   private boolean endOfOptions;
 
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index d5f0ee8..628a050 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.account.AccountSshKey;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.logging.Metadata;
@@ -97,11 +98,16 @@
   static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
     private final ExternalIds externalIds;
     private final VersionedAuthorizedKeys.Accessor authorizedKeys;
+    private final ExternalIdKeyFactory externalIdKeyFactory;
 
     @Inject
-    Loader(ExternalIds externalIds, VersionedAuthorizedKeys.Accessor authorizedKeys) {
+    Loader(
+        ExternalIds externalIds,
+        VersionedAuthorizedKeys.Accessor authorizedKeys,
+        ExternalIdKeyFactory externalIdKeyFactory) {
       this.externalIds = externalIds;
       this.authorizedKeys = authorizedKeys;
+      this.externalIdKeyFactory = externalIdKeyFactory;
     }
 
     @Override
@@ -111,7 +117,7 @@
               "Loading SSH keys for account with username",
               Metadata.builder().username(username).build())) {
         Optional<ExternalId> user =
-            externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
+            externalIds.get(externalIdKeyFactory.create(SCHEME_USERNAME, username));
         if (!user.isPresent()) {
           return NO_SUCH_USER;
         }
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 616f7d1..7c96342 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -92,7 +92,7 @@
     }
   }
 
-  /** @return true if a change in state has occurred */
+  /** Returns true if a change in state has occurred */
   public boolean enableLogging() {
     synchronized (lock) {
       if (async == null) {
@@ -112,7 +112,7 @@
     }
   }
 
-  /** @return true if a change in state has occurred */
+  /** Returns true if a change in state has occurred */
   public boolean disableLogging() {
     synchronized (lock) {
       if (async != null) {
diff --git a/java/com/google/gerrit/sshd/SshSession.java b/java/com/google/gerrit/sshd/SshSession.java
index b39eaed..d545844 100644
--- a/java/com/google/gerrit/sshd/SshSession.java
+++ b/java/com/google/gerrit/sshd/SshSession.java
@@ -114,7 +114,7 @@
     identity.setAccessPath(path);
   }
 
-  /** @return {@code true} if the authentication did not succeed. */
+  /** Returns {@code true} if the authentication did not succeed. */
   boolean isAuthenticationError() {
     return authError != null;
   }
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 43a1670..0c286ca 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.RawInputUtil;
@@ -36,6 +37,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -43,6 +45,7 @@
 import com.google.gerrit.server.restapi.account.CreateEmail;
 import com.google.gerrit.server.restapi.account.DeleteActive;
 import com.google.gerrit.server.restapi.account.DeleteEmail;
+import com.google.gerrit.server.restapi.account.DeleteExternalIds;
 import com.google.gerrit.server.restapi.account.DeleteSshKey;
 import com.google.gerrit.server.restapi.account.GetEmails;
 import com.google.gerrit.server.restapi.account.GetSshKeys;
@@ -122,10 +125,18 @@
   @Option(name = "--generate-http-password", usage = "generate a new HTTP password for the account")
   private boolean generateHttpPassword;
 
+  @Option(
+      name = "--delete-external-id",
+      metaVar = "EXTERNALID",
+      usage = "external id to delete from the account")
+  private List<String> externalIdsToDelete = new ArrayList<>();
+
   @Inject private IdentifiedUser.GenericFactory genericUserFactory;
 
   @Inject private CreateEmail createEmail;
 
+  @Inject private DeleteExternalIds deleteExternalIds;
+
   @Inject private GetEmails getEmails;
 
   @Inject private DeleteEmail deleteEmail;
@@ -150,6 +161,8 @@
 
   @Inject private Provider<CurrentUser> userProvider;
 
+  @Inject private ExternalIds externalIds;
+
   private AccountResource rsrc;
 
   @Override
@@ -210,6 +223,9 @@
           "--preferred-email and --delete-email options are mutually "
               + "exclusive for the same email address.");
     }
+    if (externalIdsToDelete.contains("ALL")) {
+      externalIdsToDelete = Collections.singletonList("ALL");
+    }
   }
 
   private void setAccount() throws Failure {
@@ -265,6 +281,10 @@
       if (!deleteSshKeys.isEmpty()) {
         deleteSshKeys(deleteSshKeys);
       }
+
+      for (String externalId : externalIdsToDelete) {
+        deleteExternalId(externalId);
+      }
     } catch (RestApiException e) {
       throw die(e.getMessage());
     } catch (Exception e) {
@@ -355,4 +375,21 @@
     }
     return sshKeys;
   }
+
+  private void deleteExternalId(String externalId)
+      throws IOException, RestApiException, ConfigInvalidException, PermissionBackendException {
+    List<String> ids;
+    if (externalId.equals("ALL")) {
+      ids =
+          externalIds.byAccount(rsrc.getUser().getAccountId()).stream()
+              .map(e -> e.key().get())
+              .collect(toList());
+      if (ids.isEmpty()) {
+        throw new ResourceNotFoundException("Account has no external Ids");
+      }
+    } else {
+      ids = Collections.singletonList(externalId);
+    }
+    deleteExternalIds.apply(rsrc, ids);
+  }
 }
diff --git a/java/com/google/gerrit/testing/GerritJUnit.java b/java/com/google/gerrit/testing/GerritJUnit.java
index 0771c39..e80afa9 100644
--- a/java/com/google/gerrit/testing/GerritJUnit.java
+++ b/java/com/google/gerrit/testing/GerritJUnit.java
@@ -26,11 +26,11 @@
    * <p>This construction is recommended by the Truth team for use in conjunction with asserting
    * over a {@code ThrowableSubject} on the return type:
    *
-   * <pre>
-   *   MyException e = assertThrows(MyException.class, () -> doSomething(foo));
-   *   assertThat(e).isInstanceOf(MySubException.class);
-   *   assertThat(e).hasMessageThat().contains("sub-exception occurred");
-   * </pre>
+   * <pre>{@code
+   * MyException e = assertThrows(MyException.class, () -> doSomething(foo));
+   * assertThat(e).isInstanceOf(MySubException.class);
+   * assertThat(e).hasMessageThat().contains("sub-exception occurred");
+   * }</pre>
    *
    * @param throwableClass expected exception type.
    * @param runnable runnable containing arbitrary code.
diff --git a/java/com/google/gerrit/testing/GerritServerTests.java b/java/com/google/gerrit/testing/GerritServerTests.java
index 363a07d..752c13d 100644
--- a/java/com/google/gerrit/testing/GerritServerTests.java
+++ b/java/com/google/gerrit/testing/GerritServerTests.java
@@ -26,7 +26,6 @@
 @RunWith(ConfigSuite.class)
 public class GerritServerTests {
   @ConfigSuite.Parameter public Config config;
-  @ConfigSuite.Name private String configName;
 
   @Rule
   public TestRule testRunner =
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 12da2c1..3949de0 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.cache.h2.H2CacheModule;
 import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
 import com.google.gerrit.server.change.FileInfoJsonModule;
+import com.google.gerrit.server.config.AllProjectsConfigProvider;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
@@ -54,6 +55,7 @@
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.DefaultUrlFormatter;
+import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.FileBasedGlobalPluginConfigProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritInstanceIdModule;
@@ -196,6 +198,7 @@
     bind(Path.class).annotatedWith(SitePath.class).toInstance(Paths.get("."));
     bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
     bind(GerritOptions.class).toInstance(new GerritOptions(false, false));
+    bind(AllProjectsConfigProvider.class).to(FileBasedAllProjectsConfigProvider.class);
     bind(GlobalPluginConfigProvider.class).to(FileBasedGlobalPluginConfigProvider.class);
 
     bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index 09ae115..362e23c 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.inject.Inject;
@@ -79,6 +80,16 @@
   }
 
   @Override
+  public synchronized Status getRepositoryStatus(NameKey name) {
+    try {
+      get(name);
+      return Status.ACTIVE;
+    } catch (RepositoryNotFoundException e) {
+      return Status.NON_EXISTENT;
+    }
+  }
+
+  @Override
   public synchronized Repo openRepository(Project.NameKey name) throws RepositoryNotFoundException {
     return get(name);
   }
diff --git a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
index 44d5cea..77df46c 100644
--- a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
+++ b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
@@ -46,6 +46,7 @@
   @Inject private IdentifiedUser.GenericFactory userFactory;
   @Inject private SchemaCreator schemaCreator;
   @Inject private ThreadLocalRequestContext requestContext;
+  @Inject private AuthRequest.Factory authRequestFactory;
 
   private LifecycleManager lifecycle;
 
@@ -99,7 +100,8 @@
     schemaCreator.create();
 
     // The first user is added to the "Administrators" group. See AccountManager#create().
-    setApiUser(accountManager.authenticate(AuthRequest.forUser("admin")).getAccountId());
+    setApiUser(
+        accountManager.authenticate(authRequestFactory.createForUser("admin")).getAccountId());
 
     // Inject target members after setting API user, so it can @Inject request-scoped objects if it
     // wants.
diff --git a/java/com/google/gerrit/util/http/RequestUtil.java b/java/com/google/gerrit/util/http/RequestUtil.java
index 92d9967..f64ce5a 100644
--- a/java/com/google/gerrit/util/http/RequestUtil.java
+++ b/java/com/google/gerrit/util/http/RequestUtil.java
@@ -31,8 +31,8 @@
   }
 
   /**
-   * @return the same value as {@link HttpServletRequest#getPathInfo()}, but without decoding
-   *     URL-encoded characters.
+   * Returns the same value as {@link HttpServletRequest#getPathInfo()}, but without decoding
+   * URL-encoded characters.
    */
   public static String getEncodedPathInfo(HttpServletRequest req) {
     // CS IGNORE LineLength FOR NEXT 3 LINES. REASON: URL.
diff --git a/java/gerrit/BUILD b/java/gerrit/BUILD
index db831b7..fea2696 100644
--- a/java/gerrit/BUILD
+++ b/java/gerrit/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/extensions:api",
         "//java/com/google/gerrit/server",
diff --git a/java/gerrit/PRED_commit_edits_2.java b/java/gerrit/PRED_commit_edits_2.java
index 12e7086..6083010 100644
--- a/java/gerrit/PRED_commit_edits_2.java
+++ b/java/gerrit/PRED_commit_edits_2.java
@@ -14,10 +14,13 @@
 
 package gerrit;
 
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Patch;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.FilePathAdapter;
 import com.google.gerrit.server.patch.Text;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.filediff.TaggedEdit;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
 import com.googlecode.prolog_cafe.exceptions.JavaException;
@@ -31,7 +34,9 @@
 import com.googlecode.prolog_cafe.lang.VariableTerm;
 import java.io.IOException;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -40,7 +45,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
@@ -69,27 +73,26 @@
     Pattern fileRegex = getRegexParameter(a1);
     Pattern editRegex = getRegexParameter(a2);
 
-    PatchList pl = StoredValues.PATCH_LIST.get(engine);
+    Map<String, FileDiffOutput> modifiedFiles = StoredValues.DIFF_LIST.get(engine);
+    FileDiffOutput firstDiff = Iterables.getFirst(modifiedFiles.values(), /* defaultValue= */ null);
+    if (firstDiff == null) {
+      // No available diffs. We cannot identify old and new commit IDs.
+      engine.fail();
+    }
     Repository repo = StoredValues.REPOSITORY.get(engine);
 
     try (ObjectReader reader = repo.newObjectReader();
         RevWalk rw = new RevWalk(reader)) {
-      final RevTree aTree;
-      final RevTree bTree;
-      final RevCommit bCommit = rw.parseCommit(pl.getNewId());
+      final RevTree aTree =
+          firstDiff.oldCommitId().equals(ObjectId.zeroId())
+              ? null
+              : rw.parseTree(firstDiff.oldCommitId());
+      final RevTree bTree = rw.parseCommit(firstDiff.newCommitId()).getTree();
 
-      if (pl.getOldId() != null) {
-        aTree = rw.parseTree(pl.getOldId());
-      } else {
-        // Octopus merge with unknown automatic merge result, since the
-        // web UI returns no files to match against, just fail.
-        return engine.fail();
-      }
-      bTree = bCommit.getTree();
-
-      for (PatchListEntry entry : pl.getPatches()) {
-        String newName = entry.getNewName();
-        String oldName = entry.getOldName();
+      for (FileDiffOutput entry : modifiedFiles.values()) {
+        String newName =
+            FilePathAdapter.getNewPath(entry.oldPath(), entry.newPath(), entry.changeType());
+        String oldName = FilePathAdapter.getOldPath(entry.oldPath(), entry.changeType());
 
         if (Patch.isMagic(newName)) {
           continue;
@@ -97,7 +100,8 @@
 
         if (fileRegex.matcher(newName).find()
             || (oldName != null && fileRegex.matcher(oldName).find())) {
-          List<Edit> edits = entry.getEdits();
+          List<Edit> edits =
+              entry.edits().stream().map(TaggedEdit::jgitEdit).collect(Collectors.toList());
           if (edits.isEmpty()) {
             continue;
           }
@@ -141,10 +145,10 @@
     return Pattern.compile(term.name(), Pattern.MULTILINE);
   }
 
-  private Text load(ObjectId tree, String path, ObjectReader reader)
+  private Text load(@Nullable ObjectId tree, String path, ObjectReader reader)
       throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
           IOException {
-    if (path == null) {
+    if (tree == null || path == null) {
       return Text.EMPTY;
     }
     final TreeWalk tw = TreeWalk.forPath(reader, path, tree);
diff --git a/java/gerrit/PRED_commit_stats_3.java b/java/gerrit/PRED_commit_stats_3.java
index 286bc2c..82fad3d 100644
--- a/java/gerrit/PRED_commit_stats_3.java
+++ b/java/gerrit/PRED_commit_stats_3.java
@@ -15,8 +15,7 @@
 package gerrit;
 
 import com.google.gerrit.entities.Patch;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -24,7 +23,8 @@
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.Term;
-import java.util.List;
+import java.util.Collection;
+import java.util.Map;
 
 /**
  * Exports basic commit statistics.
@@ -49,25 +49,30 @@
     Term a2 = arg2.dereference();
     Term a3 = arg3.dereference();
 
-    PatchList pl = StoredValues.PATCH_LIST.get(engine);
+    Map<String, FileDiffOutput> modifiedFiles = StoredValues.DIFF_LIST.get(engine);
     // Account for magic files
     if (!a1.unify(
-        new IntegerTerm(pl.getPatches().size() - countMagicFiles(pl.getPatches())), engine.trail)) {
+        new IntegerTerm(modifiedFiles.size() - countMagicFiles(modifiedFiles.values())),
+        engine.trail)) {
       return engine.fail();
     }
-    if (!a2.unify(new IntegerTerm(pl.getInsertions()), engine.trail)) {
+    Integer insertions =
+        modifiedFiles.values().stream().map(FileDiffOutput::insertions).reduce(0, Integer::sum);
+    Integer deletions =
+        modifiedFiles.values().stream().map(FileDiffOutput::deletions).reduce(0, Integer::sum);
+    if (!a2.unify(new IntegerTerm(insertions), engine.trail)) {
       return engine.fail();
     }
-    if (!a3.unify(new IntegerTerm(pl.getDeletions()), engine.trail)) {
+    if (!a3.unify(new IntegerTerm(deletions), engine.trail)) {
       return engine.fail();
     }
     return cont;
   }
 
-  private int countMagicFiles(List<PatchListEntry> entries) {
+  private int countMagicFiles(Collection<FileDiffOutput> entries) {
     int count = 0;
-    for (PatchListEntry e : entries) {
-      if (Patch.isMagic(e.getNewName())) {
+    for (FileDiffOutput e : entries) {
+      if (e.newPath().isPresent() && Patch.isMagic(e.newPath().get())) {
         count++;
       }
     }
diff --git a/java/gerrit/PRED_files_1.java b/java/gerrit/PRED_files_1.java
index ac45449..dbf96da 100644
--- a/java/gerrit/PRED_files_1.java
+++ b/java/gerrit/PRED_files_1.java
@@ -15,7 +15,8 @@
 package gerrit;
 
 import com.google.gerrit.entities.Patch;
-import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.FilePathAdapter;
+import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -26,8 +27,8 @@
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 import java.io.IOException;
+import java.util.Collection;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.FileMode;
@@ -54,17 +55,20 @@
 
     try (RevWalk revWalk = new RevWalk(StoredValues.REPOSITORY.get(engine))) {
       RevCommit commit = revWalk.parseCommit(StoredValues.getPatchSet(engine).commitId());
-      List<PatchListEntry> patches = StoredValues.PATCH_LIST.get(engine).getPatches();
+      Collection<FileDiffOutput> modifiedFiles = StoredValues.DIFF_LIST.get(engine).values();
       Set<String> submodules =
-          getAllSubmodulePaths(StoredValues.REPOSITORY.get(engine), commit, patches);
-      for (PatchListEntry entry : patches) {
-        if (Patch.isMagic(entry.getNewName())) {
+          getAllSubmodulePaths(StoredValues.REPOSITORY.get(engine), commit, modifiedFiles);
+      for (FileDiffOutput fileDiff : modifiedFiles) {
+        if (fileDiff.newPath().isPresent() && Patch.isMagic(fileDiff.newPath().get())) {
           continue;
         }
-        SymbolTerm fileNameTerm = SymbolTerm.create(entry.getNewName());
-        SymbolTerm changeType = SymbolTerm.create(entry.getChangeType().getCode());
+        String newPath =
+            FilePathAdapter.getNewPath(
+                fileDiff.oldPath(), fileDiff.newPath(), fileDiff.changeType());
+        SymbolTerm fileNameTerm = SymbolTerm.create(newPath);
+        SymbolTerm changeType = SymbolTerm.create(fileDiff.changeType().getCode());
         SymbolTerm fileType;
-        if (submodules.contains(entry.getNewName())) {
+        if (submodules.contains(newPath)) {
           fileType = SymbolTerm.create("SUBMODULE");
         } else {
           fileType = SymbolTerm.create("REGULAR");
@@ -83,14 +87,14 @@
 
   /** Returns the paths for all {@code GITLINK} files. */
   private static Set<String> getAllSubmodulePaths(
-      Repository repository, RevCommit commit, List<PatchListEntry> patches)
+      Repository repository, RevCommit commit, Collection<FileDiffOutput> modifiedFiles)
       throws PrologException, IOException {
     Set<String> submodules = new HashSet<>();
     try (TreeWalk treeWalk = new TreeWalk(repository)) {
       treeWalk.addTree(commit.getTree());
       Set<String> allPaths =
-          patches.stream()
-              .map(PatchListEntry::getNewName)
+          modifiedFiles.stream()
+              .map(f -> FilePathAdapter.getNewPath(f.oldPath(), f.newPath(), f.changeType()))
               .filter(f -> !Patch.isMagic(f))
               .collect(Collectors.toSet());
       treeWalk.setFilter(PathFilterGroup.createFromStrings(allPaths));
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 8c7d892..1da2176c 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -32,7 +32,6 @@
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
 import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
-import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -98,7 +97,6 @@
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
-import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
@@ -133,6 +131,8 @@
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -231,6 +231,8 @@
   @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
   @Inject private ExtensionRegistry extensionRegistry;
   @Inject private PluginSetContext<ExceptionHook> exceptionHooks;
+  @Inject private ExternalIdKeyFactory externalIdKeyFactory;
+  @Inject private ExternalIdFactory externalIdFactory;
 
   @Inject protected Emails emails;
 
@@ -374,8 +376,8 @@
       accountIndexedCounter.assertReindexOf(accountId, 1);
       assertThat(externalIds.byAccount(accountId))
           .containsExactly(
-              ExternalId.createUsername(input.username, accountId, null),
-              ExternalId.createEmail(accountId, input.email));
+              externalIdFactory.createUsername(input.username, accountId, null),
+              externalIdFactory.createEmail(accountId, input.email));
     }
   }
 
@@ -428,7 +430,7 @@
   public void createAtomically() throws Exception {
     Account.Id accountId = Account.id(seq.nextAccountId());
     String fullName = "Foo";
-    ExternalId extId = ExternalId.createEmail(accountId, "foo@example.com");
+    ExternalId extId = externalIdFactory.createEmail(accountId, "foo@example.com");
     AccountState accountState =
         accountsUpdateProvider
             .get()
@@ -792,126 +794,7 @@
   }
 
   @Test
-  public void starUnstarChangeWithLabels() throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    RefUpdateCounter refUpdateCounter = new RefUpdateCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter).add(refUpdateCounter)) {
-      PushOneCommit.Result r = createChange();
-      String triplet = project.get() + "~master~" + r.getChangeId();
-      refUpdateCounter.clear();
-
-      assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
-      assertThat(gApi.accounts().self().getStarredChanges()).isEmpty();
-
-      gApi.accounts()
-          .self()
-          .setStars(triplet, new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "red", "blue")));
-      ChangeInfo change = info(triplet);
-      assertThat(change.starred).isTrue();
-      assertThat(change.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
-      assertThat(gApi.accounts().self().getStars(triplet))
-          .containsExactly("blue", "red", DEFAULT_LABEL)
-          .inOrder();
-      List<ChangeInfo> starredChanges = gApi.accounts().self().getStarredChanges();
-      assertThat(starredChanges).hasSize(1);
-      ChangeInfo starredChange = starredChanges.get(0);
-      assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
-      assertThat(starredChange.starred).isTrue();
-      assertThat(starredChange.stars).containsExactly("blue", "red", DEFAULT_LABEL).inOrder();
-      refUpdateCounter.assertRefUpdateFor(
-          RefUpdateCounter.projectRef(
-              allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
-
-      gApi.accounts()
-          .self()
-          .setStars(
-              triplet,
-              new StarsInput(ImmutableSet.of("yellow"), ImmutableSet.of(DEFAULT_LABEL, "blue")));
-      change = info(triplet);
-      assertThat(change.starred).isNull();
-      assertThat(change.stars).containsExactly("red", "yellow").inOrder();
-      assertThat(gApi.accounts().self().getStars(triplet))
-          .containsExactly("red", "yellow")
-          .inOrder();
-      starredChanges = gApi.accounts().self().getStarredChanges();
-      assertThat(starredChanges).hasSize(1);
-      starredChange = starredChanges.get(0);
-      assertThat(starredChange._number).isEqualTo(r.getChange().getId().get());
-      assertThat(starredChange.starred).isNull();
-      assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder();
-      refUpdateCounter.assertRefUpdateFor(
-          RefUpdateCounter.projectRef(
-              allUsers, RefNames.refsStarredChanges(Change.id(change._number), admin.id())));
-
-      accountIndexedCounter.assertNoReindex();
-
-      requestScopeOperations.setApiUser(user.id());
-      AuthException thrown =
-          assertThrows(
-              AuthException.class,
-              () -> gApi.accounts().id(Integer.toString((admin.id().get()))).getStars(triplet));
-      assertThat(thrown).hasMessageThat().contains("not allowed to get stars of another account");
-    }
-  }
-
-  @Test
-  public void starWithInvalidLabels() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String triplet = project.get() + "~master~" + r.getChangeId();
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () ->
-                gApi.accounts()
-                    .self()
-                    .setStars(
-                        triplet,
-                        new StarsInput(
-                            ImmutableSet.of(
-                                DEFAULT_LABEL, "invalid label", "blue", "another invalid label"))));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains("invalid labels: another invalid label, invalid label");
-  }
-
-  @Test
-  public void deleteStarLabelsFromChangeWithoutStarLabels() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String triplet = project.get() + "~master~" + r.getChangeId();
-    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
-
-    gApi.accounts().self().setStars(triplet, new StarsInput());
-
-    assertThat(gApi.accounts().self().getStars(triplet)).isEmpty();
-  }
-
-  @Test
-  public void starWithDefaultAndIgnoreLabel() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String triplet = project.get() + "~master~" + r.getChangeId();
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () ->
-                gApi.accounts()
-                    .self()
-                    .setStars(
-                        triplet,
-                        new StarsInput(ImmutableSet.of(DEFAULT_LABEL, "blue", IGNORE_LABEL))));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            "The labels "
-                + DEFAULT_LABEL
-                + " and "
-                + IGNORE_LABEL
-                + " are mutually exclusive."
-                + " Only one of them can be set.");
-  }
-
-  @Test
-  public void ignoreChangeBySetStars() throws Exception {
+  public void ignoreChange() throws Exception {
     AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
     try (Registration registration =
         extensionRegistry.newRegistration().add(accountIndexedCounter)) {
@@ -929,9 +812,7 @@
       gApi.changes().id(r.getChangeId()).addReviewer(in);
 
       requestScopeOperations.setApiUser(user.id());
-      gApi.accounts()
-          .self()
-          .setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+      gApi.changes().id(r.getChangeId()).ignore(true);
 
       sender.clear();
       requestScopeOperations.setApiUser(admin.id());
@@ -951,9 +832,7 @@
       PushOneCommit.Result r = createChange();
 
       requestScopeOperations.setApiUser(user.id());
-      gApi.accounts()
-          .self()
-          .setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+      gApi.changes().id(r.getChangeId()).ignore(true);
 
       sender.clear();
       requestScopeOperations.setApiUser(admin.id());
@@ -962,11 +841,7 @@
       in.reviewer = user.email();
       gApi.changes().id(r.getChangeId()).addReviewer(in);
       List<Message> messages = sender.getMessages();
-      assertThat(messages).hasSize(1);
-      Message message = messages.get(0);
-      assertThat(message.rcpt()).containsExactly(user.getNameEmail());
-      assertMailReplyTo(message, admin.email());
-      accountIndexedCounter.assertNoReindex();
+      assertThat(messages).hasSize(0);
     }
   }
 
@@ -1407,11 +1282,11 @@
               admin.id(),
               u ->
                   u.addExternalId(
-                          ExternalId.createWithEmail(
-                              ExternalId.Key.parse(extId1), admin.id(), email))
+                          externalIdFactory.createWithEmail(
+                              externalIdKeyFactory.parse(extId1), admin.id(), email))
                       .addExternalId(
-                          ExternalId.createWithEmail(
-                              ExternalId.Key.parse(extId2), admin.id(), email)));
+                          externalIdFactory.createWithEmail(
+                              externalIdKeyFactory.parse(extId2), admin.id(), email)));
       accountIndexedCounter.assertReindexOf(admin);
       assertThat(
               gApi.accounts().self().getExternalIds().stream()
@@ -1447,8 +1322,8 @@
             admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(
-                        ExternalId.Key.parse(ldapExternalId), admin.id(), ldapEmail)));
+                    externalIdFactory.createWithEmail(
+                        externalIdKeyFactory.parse(ldapExternalId), admin.id(), ldapEmail)));
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
         .contains(ldapExternalId);
@@ -1482,11 +1357,13 @@
             admin.id(),
             u ->
                 u.addExternalId(
-                        ExternalId.createWithEmail(
-                            ExternalId.Key.parse(nonLdapExternalId), admin.id(), nonLdapEMail))
+                        externalIdFactory.createWithEmail(
+                            externalIdKeyFactory.parse(nonLdapExternalId),
+                            admin.id(),
+                            nonLdapEMail))
                     .addExternalId(
-                        ExternalId.createWithEmail(
-                            ExternalId.Key.parse(ldapExternalId), admin.id(), ldapEmail)));
+                        externalIdFactory.createWithEmail(
+                            externalIdKeyFactory.parse(ldapExternalId), admin.id(), ldapEmail)));
     assertThat(
             gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
         .containsAtLeast(ldapExternalId, nonLdapExternalId);
@@ -1549,8 +1426,8 @@
             admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(
-                        ExternalId.Key.parse("foo:bar"), admin.id(), email)));
+                    externalIdFactory.createWithEmail(
+                        externalIdKeyFactory.parse("foo:bar"), admin.id(), email)));
     assertEmail(emails.getAccountFor(email), admin);
 
     // wrong case doesn't match
@@ -1866,7 +1743,7 @@
           .update(
               "Add External ID",
               user.id(),
-              u -> u.addExternalId(ExternalId.create("foo", "myId", user.id())));
+              u -> u.addExternalId(externalIdFactory.create("foo", "myId", user.id())));
       accountIndexedCounter.assertReindexOf(user);
 
       TestKey key = validKeyWithSecondUserId();
@@ -2169,7 +2046,7 @@
         .update(
             "Delete External ID",
             account.id(),
-            u -> u.deleteExternalId(ExternalId.createEmail(account.id(), email)));
+            u -> u.deleteExternalId(externalIdFactory.createEmail(account.id(), email)));
     expectedProblems.add(
         new ConsistencyProblemInfo(
             ConsistencyProblemInfo.Status.ERROR,
@@ -2504,7 +2381,7 @@
         .update();
 
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId extIdA1 = ExternalId.create("foo", "A-1", accountId);
+    ExternalId extIdA1 = externalIdFactory.create("foo", "A-1", accountId);
     accountsUpdateProvider
         .get()
         .insert("Create Test Account", accountId, u -> u.addExternalId(extIdA1));
@@ -2512,7 +2389,7 @@
     AtomicInteger bgCounterA1 = new AtomicInteger(0);
     AtomicInteger bgCounterA2 = new AtomicInteger(0);
     PersonIdent ident = serverIdent.get();
-    ExternalId extIdA2 = ExternalId.create("foo", "A-2", accountId);
+    ExternalId extIdA2 = externalIdFactory.create("foo", "A-2", accountId);
     AccountsUpdate update =
         new AccountsUpdate(
             repoManager,
@@ -2553,8 +2430,8 @@
                 .collect(toSet()))
         .containsExactly(extIdA1.key().get());
 
-    ExternalId extIdB1 = ExternalId.create("foo", "B-1", accountId);
-    ExternalId extIdB2 = ExternalId.create("foo", "B-2", accountId);
+    ExternalId extIdB1 = externalIdFactory.create("foo", "B-1", accountId);
+    ExternalId extIdB2 = externalIdFactory.create("foo", "B-2", accountId);
     Optional<AccountState> updatedAccount =
         update.update(
             "Update External ID",
@@ -2617,23 +2494,24 @@
     // Manually inserting/updating/deleting an external ID of the user makes the index document
     // stale.
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo);
+      ExternalIdNotes extIdNotes =
+          ExternalIdNotes.loadNoCacheUpdate(allUsers, repo, externalIdFactory);
 
-      ExternalId.Key key = ExternalId.Key.create("foo", "foo");
-      extIdNotes.insert(ExternalId.create(key, accountId));
+      ExternalId.Key key = externalIdKeyFactory.create("foo", "foo");
+      extIdNotes.insert(externalIdFactory.create(key, accountId));
       try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
         extIdNotes.commit(update);
       }
       assertStaleAccountAndReindex(accountId);
 
-      extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo);
-      extIdNotes.upsert(ExternalId.createWithEmail(key, accountId, "foo@example.com"));
+      extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo, externalIdFactory);
+      extIdNotes.upsert(externalIdFactory.createWithEmail(key, accountId, "foo@example.com"));
       try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
         extIdNotes.commit(update);
       }
       assertStaleAccountAndReindex(accountId);
 
-      extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo);
+      extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo, externalIdFactory);
       extIdNotes.delete(accountId, key);
       try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
         extIdNotes.commit(update);
@@ -2894,9 +2772,11 @@
     String extId1String = "foo:bar";
     String extId2String = "foo:baz";
     ExternalId extId1 =
-        ExternalId.createWithEmail(ExternalId.Key.parse(extId1String), admin.id(), "1@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse(extId1String), admin.id(), "1@foo.com");
     ExternalId extId2 =
-        ExternalId.createWithEmail(ExternalId.Key.parse(extId2String), user.id(), "2@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse(extId2String), user.id(), "2@foo.com");
 
     ObjectId revBefore;
     try (Repository repo = repoManager.openRepository(allUsers)) {
@@ -2936,9 +2816,11 @@
   @Test
   public void externalIdBatchUpdates_fail_sameAccount() {
     ExternalId extId1 =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id(), "1@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
     ExternalId extId2 =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:baz"), user.id(), "2@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:baz"), user.id(), "2@foo.com");
 
     AccountsUpdate.UpdateArguments ua1 =
         new AccountsUpdate.UpdateArguments(
@@ -2957,9 +2839,11 @@
   @Test
   public void externalIdBatchUpdates_fail_duplicateKey() {
     ExternalId extIdAdmin =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id(), "1@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
     ExternalId extIdUser =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), user.id(), "2@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:bar"), user.id(), "2@foo.com");
 
     AccountsUpdate.UpdateArguments ua1 =
         new AccountsUpdate.UpdateArguments(
@@ -2977,9 +2861,11 @@
   @Test
   public void externalIdBatchUpdates_commitMsg_multipleAccounts() throws Exception {
     ExternalId extId1 =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id(), "1@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
     ExternalId extId2 =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:baz"), user.id(), "2@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:baz"), user.id(), "2@foo.com");
 
     AccountsUpdate.UpdateArguments ua1 =
         new AccountsUpdate.UpdateArguments(
@@ -3001,7 +2887,8 @@
   @Test
   public void externalIdBatchUpdates_commitMsg_singleAccount() throws Exception {
     ExternalId extId =
-        ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id(), "1@foo.com");
+        externalIdFactory.createWithEmail(
+            externalIdKeyFactory.parse("foo:bar"), admin.id(), "1@foo.com");
 
     accountsUpdateProvider.get().update("foobar", admin.id(), (a, u) -> u.addExternalId(extId));
 
@@ -3158,7 +3045,7 @@
               account.id(),
               u ->
                   u.addExternalId(
-                      ExternalId.createWithEmail(name("test"), email, account.id(), email)));
+                      externalIdFactory.createWithEmail(name("test"), email, account.id(), email)));
       accountIndexedCounter.assertReindexOf(account);
       requestScopeOperations.setApiUser(account.id());
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index b41a2f3..7e23f0e 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -37,6 +37,8 @@
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.SetInactiveFlag;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -62,14 +64,17 @@
   @Inject private SshKeyCache sshKeyCache;
   @Inject private GroupsUpdate.Factory groupsUpdateFactory;
   @Inject private SetInactiveFlag setInactiveFlag;
+  @Inject private AuthRequest.Factory authRequestFactory;
+  @Inject private ExternalIdFactory externalIdFactory;
+  @Inject private ExternalIdKeyFactory externalIdKeyFactory;
 
   @Test
   public void authenticateNewAccountWithEmail() throws Exception {
     String email = "foo@example.com";
-    ExternalId.Key mailtoExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_MAILTO, email);
+    ExternalId.Key mailtoExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_MAILTO, email);
     assertNoSuchExternalIds(mailtoExtIdKey);
 
-    AuthRequest who = AuthRequest.forEmail(email);
+    AuthRequest who = authRequestFactory.createForEmail(email);
     AuthResult authResult = accountManager.authenticate(who);
     assertAuthResultForNewAccount(authResult, mailtoExtIdKey);
     assertExternalId(mailtoExtIdKey, email);
@@ -78,11 +83,12 @@
   @Test
   public void authenticateNewAccountWithUsername() throws Exception {
     String username = "foo";
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
-    ExternalId.Key usernameExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_USERNAME, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key usernameExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, username);
     assertNoSuchExternalIds(gerritExtIdKey, usernameExtIdKey);
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     AuthResult authResult = accountManager.authenticate(who);
     assertAuthResultForNewAccount(authResult, gerritExtIdKey);
     assertExternalIdsWithoutEmail(gerritExtIdKey, usernameExtIdKey);
@@ -91,11 +97,12 @@
   @Test
   public void authenticateNewAccountWithUsernameAndEmail() throws Exception {
     String username = "foo";
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
-    ExternalId.Key usernameExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_USERNAME, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key usernameExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, username);
     assertNoSuchExternalIds(gerritExtIdKey, usernameExtIdKey);
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     String email = "foo@example.com";
     who.setEmailAddress(email);
     AuthResult authResult = accountManager.authenticate(who);
@@ -107,12 +114,14 @@
   @Test
   public void authenticateNewAccountWithExternalUser() throws Exception {
     String username = "foo";
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
-    ExternalId.Key usernameExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_USERNAME, username);
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key usernameExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     assertNoSuchExternalIds(externalExtIdKey, usernameExtIdKey, gerritExtIdKey);
 
-    AuthRequest who = AuthRequest.forExternalUser(username);
+    AuthRequest who = authRequestFactory.createForExternalUser(username);
     AuthResult authResult = accountManager.authenticate(who);
     assertAuthResultForNewAccount(authResult, externalExtIdKey);
     assertExternalIdsWithoutEmail(externalExtIdKey, usernameExtIdKey);
@@ -122,12 +131,14 @@
   @Test
   public void authenticateNewAccountWithExternalUserAndEmail() throws Exception {
     String username = "foo";
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
-    ExternalId.Key usernameExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_USERNAME, username);
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key usernameExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     assertNoSuchExternalIds(externalExtIdKey, usernameExtIdKey, gerritExtIdKey);
 
-    AuthRequest who = AuthRequest.forExternalUser(username);
+    AuthRequest who = authRequestFactory.createForExternalUser(username);
     String email = "foo@example.com";
     who.setEmailAddress(email);
     AuthResult authResult = accountManager.authenticate(who);
@@ -141,13 +152,13 @@
   public void authenticateWithEmail() throws Exception {
     String email = "foo@example.com";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key mailtoExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_MAILTO, email);
+    ExternalId.Key mailtoExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_MAILTO, email);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(mailtoExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(mailtoExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forEmail(email);
+    AuthRequest who = authRequestFactory.createForEmail(email);
     AuthResult authResult = accountManager.authenticate(who);
     assertAuthResultForExistingAccount(authResult, accountId, mailtoExtIdKey);
   }
@@ -156,13 +167,13 @@
   public void authenticateWithUsername() throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     AuthResult authResult = accountManager.authenticate(who);
     assertAuthResultForExistingAccount(authResult, accountId, gerritExtIdKey);
   }
@@ -171,13 +182,14 @@
   public void authenticateWithExternalUser() throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(externalExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(externalExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forExternalUser(username);
+    AuthRequest who = authRequestFactory.createForExternalUser(username);
     AuthResult authResult = accountManager.authenticate(who);
     assertAuthResultForExistingAccount(authResult, accountId, externalExtIdKey);
   }
@@ -187,15 +199,16 @@
     String username = "foo";
     String email = "foo@example.com";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
         u ->
             u.setPreferredEmail(email)
-                .addExternalId(ExternalId.createWithEmail(gerritExtIdKey, accountId, email)));
+                .addExternalId(
+                    externalIdFactory.createWithEmail(gerritExtIdKey, accountId, email)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     String newEmail = "bar@example.com";
     who.setEmailAddress(newEmail);
     AuthResult authResult = accountManager.authenticate(who);
@@ -233,23 +246,26 @@
             projectCache,
             externalIds,
             groupsUpdateFactory,
-            setInactiveFlag));
+            setInactiveFlag,
+            externalIdFactory,
+            externalIdKeyFactory));
   }
 
   private void authenticateWithUsernameAndUpdateDisplayName(AccountManager am) throws Exception {
     String username = "foo";
     String email = "foo@example.com";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
         u ->
             u.setFullName("Initial Name")
                 .setPreferredEmail(email)
-                .addExternalId(ExternalId.createWithEmail(gerritExtIdKey, accountId, email)));
+                .addExternalId(
+                    externalIdFactory.createWithEmail(gerritExtIdKey, accountId, email)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     String newName = "Updated Name";
     who.setDisplayName(newName);
     AuthResult authResult = am.authenticate(who);
@@ -263,12 +279,12 @@
   @Test
   public void cannotAuthenticateWithOrphanedExtId() throws Exception {
     String username = "foo";
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     assertNoSuchExternalIds(gerritExtIdKey);
 
     // Create orphaned SCHEME_GERRIT external ID.
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId gerritExtId = ExternalId.create(gerritExtIdKey, accountId);
+    ExternalId gerritExtId = externalIdFactory.create(gerritExtIdKey, accountId);
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
@@ -276,7 +292,7 @@
       extIdNotes.commit(md);
     }
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
     assertThat(thrown).hasMessageThat().contains("Authentication error, account not found");
@@ -286,13 +302,13 @@
   public void cannotAuthenticateWithInactiveAccount() throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.setActive(false).addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.setActive(false).addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
     assertThat(thrown).hasMessageThat().contains("Authentication error, account inactive");
@@ -303,13 +319,13 @@
       throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.setActive(false).addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.setActive(false).addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     who.setActive(true);
     who.setAuthProvidesAccountActiveStatus(true);
     AccountException thrown =
@@ -323,13 +339,13 @@
       throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.setActive(false).addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.setActive(false).addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     who.setActive(true);
     who.setAuthProvidesAccountActiveStatus(true);
     AuthResult authResult = accountManager.authenticate(who);
@@ -344,13 +360,13 @@
       throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     who.setActive(false);
     who.setAuthProvidesAccountActiveStatus(true);
     AuthResult authResult = accountManager.authenticate(who);
@@ -366,13 +382,13 @@
       throws Exception {
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     who.setActive(false);
     who.setAuthProvidesAccountActiveStatus(true);
     AccountException thrown =
@@ -391,15 +407,17 @@
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.createWithEmail(externalExtIdKey, accountId, email)));
+        u ->
+            u.addExternalId(externalIdFactory.createWithEmail(externalExtIdKey, accountId, email)));
 
     // Try to authenticate with this email to create a new account with a SCHEME_MAILTO external ID.
     // Expect that this fails because the email is already assigned to the other account.
-    AuthRequest who = AuthRequest.forEmail(email);
+    AuthRequest who = authRequestFactory.createForEmail(email);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
     assertThat(thrown)
@@ -414,15 +432,17 @@
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.createWithEmail(externalExtIdKey, accountId, email)));
+        u ->
+            u.addExternalId(externalIdFactory.createWithEmail(externalExtIdKey, accountId, email)));
 
     // Try to authenticate with a new username and claim the same email.
     // Expect that this fails because the email is already assigned to the other account.
-    AuthRequest who = AuthRequest.forUser("bar");
+    AuthRequest who = authRequestFactory.createForUser("bar");
     who.setEmailAddress(email);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
@@ -439,25 +459,29 @@
     // Create an account with a SCHEME_GERRIT external ID and an email.
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
         u ->
             u.setPreferredEmail(email)
-                .addExternalId(ExternalId.createWithEmail(gerritExtIdKey, accountId, email)));
+                .addExternalId(
+                    externalIdFactory.createWithEmail(gerritExtIdKey, accountId, email)));
 
     // Create another account with an SCHEME_EXTERNAL external ID that occupies the new email.
     Account.Id accountId2 = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, "bar");
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, "bar");
     accountsUpdate.insert(
         "Create Test Account",
         accountId2,
-        u -> u.addExternalId(ExternalId.createWithEmail(externalExtIdKey, accountId2, newEmail)));
+        u ->
+            u.addExternalId(
+                externalIdFactory.createWithEmail(externalExtIdKey, accountId2, newEmail)));
 
     // Try to authenticate and update the email for the first account.
     // Expect that this fails because the new email is already assigned to the other account.
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     who.setEmailAddress(newEmail);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.authenticate(who));
@@ -482,21 +506,21 @@
 
     // Create an account with a SCHEME_GERRIT external ID
     String username = "foo";
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     Account.Id accountId = Account.id(seq.nextAccountId());
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
     // Add the additional mail external ID with SCHEME_EMAIL
-    accountManager.link(accountId, AuthRequest.forEmail(email));
+    accountManager.link(accountId, authRequestFactory.createForEmail(email));
 
     // Try to authenticate and update the email for the account.
     // Expect that this to succeed because even if the email already exist
     // it is associated to the same account-id and thus is not really
     // a duplicate but simply a promotion of external id to preferred email.
-    AuthRequest who = AuthRequest.forUser(username);
+    AuthRequest who = authRequestFactory.createForUser(username);
     who.setEmailAddress(email);
     AuthResult authResult = accountManager.authenticate(who);
 
@@ -519,20 +543,20 @@
     // Create an account with a SCHEME_GERRIT external ID and no email
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username);
+    ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId)));
+        u -> u.addExternalId(externalIdFactory.create(gerritExtIdKey, accountId)));
 
     // Check that email is not used yet.
     String email = "foo@example.com";
-    ExternalId.Key mailtoExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_MAILTO, email);
+    ExternalId.Key mailtoExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_MAILTO, email);
     assertNoSuchExternalIds(mailtoExtIdKey);
 
     // Link the email to the account.
     // Expect that a MAILTO external ID is created.
-    AuthRequest who = AuthRequest.forEmail(email);
+    AuthRequest who = authRequestFactory.createForEmail(email);
     AuthResult authResult = accountManager.link(accountId, who);
     assertAuthResultForExistingAccount(authResult, accountId, mailtoExtIdKey);
     assertExternalId(mailtoExtIdKey, accountId, email);
@@ -543,17 +567,18 @@
     // Create an account with a SCHEME_GERRIT external ID and no email
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
         u ->
             u.addExternalId(
-                ExternalId.createWithEmail(externalExtIdKey, accountId, "old@example.com")));
+                externalIdFactory.createWithEmail(externalExtIdKey, accountId, "old@example.com")));
 
     // Link the email to the existing SCHEME_EXTERNAL external ID, but with a new email.
     // Expect that the email of the existing external ID is updated.
-    AuthRequest who = AuthRequest.forExternalUser(username);
+    AuthRequest who = authRequestFactory.createForExternalUser(username);
     String newEmail = "new@example.com";
     who.setEmailAddress(newEmail);
     AuthResult authResult = accountManager.link(accountId, who);
@@ -566,24 +591,26 @@
     // Create an account with a SCHEME_EXTERNAL external ID
     String username1 = "foo";
     Account.Id accountId1 = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey1 = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username1);
+    ExternalId.Key externalExtIdKey1 =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username1);
     accountsUpdate.insert(
         "Create Test Account",
         accountId1,
-        u -> u.addExternalId(ExternalId.create(externalExtIdKey1, accountId1)));
+        u -> u.addExternalId(externalIdFactory.create(externalExtIdKey1, accountId1)));
 
     // Create another account with a SCHEME_EXTERNAL external ID
     String username2 = "bar";
     Account.Id accountId2 = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey2 = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username2);
+    ExternalId.Key externalExtIdKey2 =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username2);
     accountsUpdate.insert(
         "Create Test Account",
         accountId2,
-        u -> u.addExternalId(ExternalId.create(externalExtIdKey2, accountId2)));
+        u -> u.addExternalId(externalIdFactory.create(externalExtIdKey2, accountId2)));
 
     // Try to link external ID of the first account to the second account.
     // Expect that this fails because the external ID is already assigned to the first account.
-    AuthRequest who = AuthRequest.forExternalUser(username1);
+    AuthRequest who = authRequestFactory.createForExternalUser(username1);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.link(accountId2, who));
     assertThat(thrown)
@@ -598,24 +625,27 @@
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.createWithEmail(externalExtIdKey, accountId, email)));
+        u ->
+            u.addExternalId(externalIdFactory.createWithEmail(externalExtIdKey, accountId, email)));
 
     // Create another account with a SCHEME_GERRIT external ID and no email
     String username2 = "foo";
     Account.Id accountId2 = Account.id(seq.nextAccountId());
-    ExternalId.Key gerritExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_GERRIT, username2);
+    ExternalId.Key gerritExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username2);
     accountsUpdate.insert(
         "Create Test Account",
         accountId2,
-        u -> u.addExternalId(ExternalId.create(gerritExtIdKey, accountId2)));
+        u -> u.addExternalId(externalIdFactory.create(gerritExtIdKey, accountId2)));
 
     // Try to link the email to the second account (via a new MAILTO external ID) and expect that
     // this fails because the email is already assigned to the first account.
-    AuthRequest who = AuthRequest.forEmail(email);
+    AuthRequest who = authRequestFactory.createForEmail(email);
     AccountException thrown =
         assertThrows(AccountException.class, () -> accountManager.link(accountId2, who));
     assertThat(thrown)
@@ -630,13 +660,15 @@
     // Create an account with an SCHEME_EXTERNAL external ID that occupies the email.
     String username = "foo";
     Account.Id accountId = Account.id(seq.nextAccountId());
-    ExternalId.Key externalExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_EXTERNAL, username);
+    ExternalId.Key externalExtIdKey =
+        externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
     accountsUpdate.insert(
         "Create Test Account",
         accountId,
-        u -> u.addExternalId(ExternalId.createWithEmail(externalExtIdKey, accountId, email)));
+        u ->
+            u.addExternalId(externalIdFactory.createWithEmail(externalExtIdKey, accountId, email)));
 
-    AuthRequest who = AuthRequest.forEmail(email);
+    AuthRequest who = authRequestFactory.createForEmail(email);
     AuthResult result = accountManager.link(accountId, who);
     assertThat(result.isNew()).isFalse();
     assertThat(result.getAccountId().get()).isEqualTo(accountId.get());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 5c5d127..59011f6 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -99,10 +99,12 @@
 import com.google.gerrit.entities.LabelFunction;
 import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LegacySubmitRequirement;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
@@ -127,7 +129,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewerResult;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
-import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
@@ -149,7 +150,9 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.SubmitRecordInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
 import com.google.gerrit.extensions.common.TrackingIdInfo;
@@ -170,6 +173,7 @@
 import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.testing.TestChangeETagComputation;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.index.change.ChangeIndex;
@@ -186,6 +190,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.PostReview;
+import com.google.gerrit.server.rules.SubmitRule;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -4033,6 +4038,51 @@
   }
 
   @Test
+  public void submitRecords() throws Exception {
+    PushOneCommit.Result r = createChange();
+    TestSubmitRule testSubmitRule = new TestSubmitRule();
+    try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
+      String changeId = r.getChangeId();
+
+      ChangeInfo change = gApi.changes().id(changeId).get();
+      assertThat(change.submitRecords).hasSize(2);
+      // Check the default submit record for the code-review label
+      SubmitRecordInfo codeReviewRecord = Iterables.get(change.submitRecords, 0);
+      assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule");
+      assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.NOT_READY);
+      assertThat(codeReviewRecord.labels).hasSize(1);
+      SubmitRecordInfo.Label label = Iterables.getOnlyElement(codeReviewRecord.labels);
+      assertThat(label.label).isEqualTo("Code-Review");
+      assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.NEED);
+      assertThat(label.appliedBy).isNull();
+      // Check the custom test record created by the TestSubmitRule
+      SubmitRecordInfo testRecord = Iterables.get(change.submitRecords, 1);
+      assertThat(testRecord.ruleName).isEqualTo("gerrit~TestSubmitRule");
+      assertThat(testRecord.status).isEqualTo(SubmitRecordInfo.Status.OK);
+      assertThat(testRecord.requirements)
+          .containsExactly(new LegacySubmitRequirementInfo("OK", "fallback text", "type"));
+      assertThat(testRecord.labels).hasSize(1);
+      SubmitRecordInfo.Label testLabel = Iterables.getOnlyElement(testRecord.labels);
+      assertThat(testLabel.label).isEqualTo("label");
+      assertThat(testLabel.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
+      assertThat(testLabel.appliedBy).isNull();
+
+      voteLabel(changeId, "code-review", 2);
+      // Code review record is satisfied after voting +2
+      change = gApi.changes().id(changeId).get();
+      assertThat(change.submitRecords).hasSize(2);
+      codeReviewRecord = Iterables.get(change.submitRecords, 0);
+      assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule");
+      assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.OK);
+      assertThat(codeReviewRecord.labels).hasSize(1);
+      label = Iterables.getOnlyElement(codeReviewRecord.labels);
+      assertThat(label.label).isEqualTo("Code-Review");
+      assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
+      assertThat(label.appliedBy._accountId).isEqualTo(admin.id().get());
+    }
+  }
+
+  @Test
   public void submitRequirement_withLabelEqualsMax() throws Exception {
     configSubmitRequirement(
         project,
@@ -4048,12 +4098,54 @@
 
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 2);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+  }
+
+  @Test
+  public void submitRequirement_withLabelEqualsMax_fromNonUploader() throws Exception {
+    configLabel("my-label", LabelFunction.NO_OP); // label function has no effect
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
+        .update();
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("my-label")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create("label:my-label=MAX,user=non_uploader"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // Voting with a max vote as the uploader will not satisfy the submit requirement.
+    voteLabel(changeId, "my-label", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // Voting as a non-uploader will satisfy the submit requirement.
+    requestScopeOperations.setApiUser(user.id());
+    voteLabel(changeId, "my-label", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false);
   }
 
   @Test
@@ -4073,19 +4165,70 @@
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
     // Requirement is satisfied because there are no votes
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", -1);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
     // Requirement is still satisfied because -1 is not the max negative value
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", -2);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
     // Requirement is now unsatisfied because -2 is the max negative value
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+  }
+
+  @Test
+  public void submitRequirement_withMaxWithBlock_ignoringSelfApproval() throws Exception {
+    configLabel("my-label", LabelFunction.MAX_WITH_BLOCK);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
+        .update();
+
+    configSubmitRequirement(
+        project,
+        SubmitRequirement.builder()
+            .setName("my-label")
+            .setSubmittabilityExpression(
+                SubmitRequirementExpression.create(
+                    "label:my-label=MAX,user=non_uploader -label:my-label=MIN"))
+            .setAllowOverrideInChildProjects(false)
+            .build());
+
+    // Create the change as admin
+    requestScopeOperations.setApiUser(admin.id());
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+
+    // Admin (a.k.a uploader) adds a -1 min vote. This is going to block submission.
+    voteLabel(changeId, "my-label", -1);
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // user (i.e. non_uploader) votes 1. Requirement is still blocking because of -1 of uploader.
+    requestScopeOperations.setApiUser(user.id());
+    voteLabel(changeId, "my-label", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);
+
+    // Admin (a.k.a uploader) removes -1. Now requirement is fulfilled.
+    requestScopeOperations.setApiUser(admin.id());
+    voteLabel(changeId, "my-label", 0);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(1);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false);
   }
 
   @Test
@@ -4104,12 +4247,14 @@
 
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 1);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
   }
 
   @Test
@@ -4135,15 +4280,19 @@
 
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(2);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
-    assertSubmitRequirementStatus(change.submitRequirements, "verified", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "verified", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 2);
 
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(2);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
-    assertSubmitRequirementStatus(change.submitRequirements, "verified", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "verified", Status.UNSATISFIED, /* isLegacy= */ false);
   }
 
   @Test
@@ -4163,7 +4312,8 @@
 
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.NOT_APPLICABLE);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
   }
 
   @Test
@@ -4192,13 +4342,15 @@
     String changeId = r.getChangeId();
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "build-cop-override", 1);
 
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.OVERRIDDEN);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.OVERRIDDEN, /* isLegacy= */ false);
   }
 
   @Test
@@ -4227,17 +4379,20 @@
     String changeId = r.getChangeId();
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 1);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 2);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
   }
 
   @Test
@@ -4255,12 +4410,14 @@
     String changeId = r.getChangeId();
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 1);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
   }
 
   @Test
@@ -4290,47 +4447,56 @@
     String changeId = r.getChangeId();
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 1);
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
     // +1 was enough to fulfill the requirement: override in child project was ignored
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
   }
 
   @Test
   public void submitRequirement_storedForClosedChanges() throws Exception {
-    configSubmitRequirement(
-        project,
-        SubmitRequirement.builder()
-            .setName("code-review")
-            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
-            .setAllowOverrideInChildProjects(false)
-            .build());
+    for (SubmitType submitType : SubmitType.values()) {
+      Project.NameKey project = createProjectForPush(submitType);
+      TestRepository<InMemoryRepository> repo = cloneProject(project);
+      configSubmitRequirement(
+          project,
+          SubmitRequirement.builder()
+              .setName("code-review")
+              .setSubmittabilityExpression(
+                  SubmitRequirementExpression.create("label:code-review=+2"))
+              .setAllowOverrideInChildProjects(false)
+              .build());
 
-    PushOneCommit.Result r = createChange("Add a file", "foo", "content");
-    String changeId = r.getChangeId();
+      PushOneCommit.Result r =
+          createChange(repo, "master", "Add a file", "foo", "content", "topic");
+      String changeId = r.getChangeId();
 
-    voteLabel(changeId, "code-review", 2);
+      voteLabel(changeId, "code-review", 2);
 
-    ChangeInfo change = gApi.changes().id(changeId).get();
-    assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+      ChangeInfo change = gApi.changes().id(changeId).get();
+      assertThat(change.submitRequirements).hasSize(1);
+      assertSubmitRequirementStatus(
+          change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
 
-    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
-    revision.review(ReviewInput.approve());
-    revision.submit();
+      RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+      revision.review(ReviewInput.approve());
+      revision.submit();
 
-    ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
+      ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
 
-    SubmitRequirementResult result =
-        notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
-    assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
-    assertThat(result.submittabilityExpressionResult().status())
-        .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
-    assertThat(result.submittabilityExpressionResult().expression().expressionString())
-        .isEqualTo("label:code-review=+2");
+      SubmitRequirementResult result =
+          notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
+      assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+      assertThat(result.submittabilityExpressionResult().status())
+          .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
+      assertThat(result.submittabilityExpressionResult().expression().expressionString())
+          .isEqualTo("label:code-review=+2");
+    }
   }
 
   @Test
@@ -4348,13 +4514,15 @@
 
     ChangeInfo change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.UNSATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
 
     voteLabel(changeId, "code-review", 2);
 
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
 
     gApi.changes().id(changeId).current().submit();
 
@@ -4370,7 +4538,64 @@
     // The new "verified" submit requirement is not returned, since this change is closed
     change = gApi.changes().id(changeId).get();
     assertThat(change.submitRequirements).hasSize(1);
-    assertSubmitRequirementStatus(change.submitRequirements, "code-review", Status.SATISFIED);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+  }
+
+  @Test
+  @GerritConfig(
+      name = "experiments.enabled",
+      value =
+          ExperimentFeaturesConstants
+              .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_LEGACY_SUBMIT_REQUIREMENTS)
+  public void submitRequirements_returnForLegacySubmitRecords_ifEnabled() throws Exception {
+    configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel("build-cop-override")
+                .ref("refs/heads/master")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
+
+    // 1. Project has two legacy requirements: Code-Review and bco. Both unsatisfied.
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    ChangeInfo change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(2);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "build-cop-override", Status.UNSATISFIED, /* isLegacy= */ true);
+
+    // 2. Vote +1 on bco. bco becomes satisfied
+    voteLabel(changeId, "build-cop-override", 1);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(2);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true);
+
+    // 3. Vote +1 on Code-Review. Code-Review becomes satisfied
+    voteLabel(changeId, "Code-Review", 2);
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(2);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true);
+
+    // 4. Merge the change. Submit requirements status is presented from NoteDb.
+    gApi.changes().id(changeId).current().submit();
+    change = gApi.changes().id(changeId).get();
+    assertThat(change.submitRequirements).hasSize(2);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+    assertSubmitRequirementStatus(
+        change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true);
   }
 
   @Test
@@ -4397,7 +4622,10 @@
             .get();
     assertThat(changeInfos).hasSize(1);
     assertSubmitRequirementStatus(
-        changeInfos.get(0).submitRequirements, "code-review", Status.SATISFIED);
+        changeInfos.get(0).submitRequirements,
+        "code-review",
+        Status.SATISFIED,
+        /* isLegacy= */ false);
   }
 
   @Test
@@ -4425,7 +4653,10 @@
             .get();
     assertThat(changeInfos).hasSize(1);
     assertSubmitRequirementStatus(
-        changeInfos.get(0).submitRequirements, "code-review", Status.SATISFIED);
+        changeInfos.get(0).submitRequirements,
+        "code-review",
+        Status.SATISFIED,
+        /* isLegacy= */ false);
   }
 
   @Test
@@ -4747,6 +4978,28 @@
   }
 
   @Test
+  @GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:")
+  @GerritConfig(name = "trackingid.jira-bug.match", value = "\\d+")
+  @GerritConfig(name = "trackingid.jira-bug.system", value = "JIRA")
+  public void multipleTrackingIdsInSingleFooter() throws Exception {
+    PushOneCommit push =
+        pushFactory.create(
+            admin.newIdent(),
+            testRepo,
+            PushOneCommit.SUBJECT + "\n\n" + "Bug: 123, 456",
+            PushOneCommit.FILE_NAME,
+            PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS);
+    Collection<TrackingIdInfo> trackingIds = change.trackingIds;
+    assertThat(trackingIds).isNotNull();
+    assertThat(trackingIds).hasSize(2);
+    assertThat(trackingIds.stream().map(t -> t.id)).containsExactly("123", "456");
+  }
+
+  @Test
   public void starUnstar() throws Exception {
     ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
     try (Registration registration =
@@ -4874,132 +5127,6 @@
   }
 
   @Test
-  public void markAsReviewed() throws Exception {
-    com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
-
-    PushOneCommit.Result r = createChange();
-
-    ReviewerInput in = new ReviewerInput();
-    in.reviewer = user.email();
-    gApi.changes().id(r.getChangeId()).addReviewer(in);
-
-    requestScopeOperations.setApiUser(user.id());
-    assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
-    gApi.changes().id(r.getChangeId()).markAsReviewed(true);
-    assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue();
-
-    requestScopeOperations.setApiUser(user2.id());
-    sender.clear();
-    amendChange(r.getChangeId());
-
-    requestScopeOperations.setApiUser(user.id());
-    assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull();
-
-    List<Message> messages = sender.getMessages();
-    assertThat(messages).hasSize(1);
-    assertThat(messages.get(0).rcpt()).containsExactly(user.getNameEmail());
-  }
-
-  @Test
-  public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    requestScopeOperations.setApiUser(user.id());
-    gApi.changes().id(changeId).markAsReviewed(true);
-    assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () ->
-                gApi.accounts()
-                    .self()
-                    .setStars(
-                        changeId,
-                        new StarsInput(
-                            ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1"))));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            "The labels "
-                + StarredChangesUtil.REVIEWED_LABEL
-                + "/"
-                + 1
-                + " and "
-                + StarredChangesUtil.UNREVIEWED_LABEL
-                + "/"
-                + 1
-                + " are mutually exclusive. Only one of them can be set.");
-  }
-
-  @Test
-  public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    requestScopeOperations.setApiUser(user.id());
-    gApi.changes().id(changeId).markAsReviewed(false);
-    assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
-
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () ->
-                gApi.accounts()
-                    .self()
-                    .setStars(
-                        changeId,
-                        new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1"))));
-    assertThat(thrown)
-        .hasMessageThat()
-        .contains(
-            "The labels "
-                + StarredChangesUtil.REVIEWED_LABEL
-                + "/"
-                + 1
-                + " and "
-                + StarredChangesUtil.UNREVIEWED_LABEL
-                + "/"
-                + 1
-                + " are mutually exclusive. Only one of them can be set.");
-  }
-
-  @Test
-  public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    requestScopeOperations.setApiUser(user.id());
-    gApi.changes().id(changeId).markAsReviewed(true);
-    assertThat(gApi.changes().id(changeId).get().reviewed).isTrue();
-
-    amendChange(changeId);
-    assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
-
-    gApi.changes().id(changeId).markAsReviewed(false);
-    assertThat(gApi.changes().id(changeId).get().reviewed).isNull();
-
-    assertThat(gApi.accounts().self().getStars(changeId))
-        .containsExactly(
-            StarredChangesUtil.REVIEWED_LABEL + "/" + 1,
-            StarredChangesUtil.UNREVIEWED_LABEL + "/" + 2);
-  }
-
-  @Test
-  public void cannotSetInvalidLabel() throws Exception {
-    String changeId = createChange().getChangeId();
-
-    // label cannot contain whitespace
-    String invalidLabel = "invalid label";
-    BadRequestException thrown =
-        assertThrows(
-            BadRequestException.class,
-            () ->
-                gApi.accounts()
-                    .self()
-                    .setStars(changeId, new StarsInput(ImmutableSet.of(invalidLabel))));
-    assertThat(thrown).hasMessageThat().contains("invalid labels: " + invalidLabel);
-  }
-
-  @Test
   public void changeDetailsDoesNotRequireIndex() throws Exception {
     // This set of options must be kept in sync with gr-rest-api-interface.js
     Set<ListChangesOption> options =
@@ -5077,9 +5204,12 @@
   private void assertSubmitRequirementStatus(
       Collection<SubmitRequirementResultInfo> results,
       String requirementName,
-      SubmitRequirementResultInfo.Status status) {
+      SubmitRequirementResultInfo.Status status,
+      boolean isLegacy) {
     for (SubmitRequirementResultInfo result : results) {
-      if (result.name.equals(requirementName) && result.status == status) {
+      if (result.name.equals(requirementName)
+          && result.status == status
+          && result.isLegacy == isLegacy) {
         return;
       }
     }
@@ -5092,4 +5222,36 @@
                 .map(r -> String.format("%s=%s", r.name, r.status))
                 .collect(toImmutableList())));
   }
+
+  private Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
+    Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
+        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
+        .update();
+    return project;
+  }
+
+  /** Returns a hard-coded submit record containing all fields. */
+  private static class TestSubmitRule implements SubmitRule {
+    @Override
+    public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+      SubmitRecord record = new SubmitRecord();
+      record.ruleName = "testSubmitRule";
+      record.status = SubmitRecord.Status.OK;
+      SubmitRecord.Label label = new SubmitRecord.Label();
+      label.label = "label";
+      label.status = SubmitRecord.Label.Status.OK;
+      record.labels = Arrays.asList(label);
+      record.requirements =
+          Arrays.asList(
+              LegacySubmitRequirement.builder()
+                  .setType("type")
+                  .setFallbackText("fallback text")
+                  .build());
+      return Optional.of(record);
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index df281d9..cd9e876 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.acceptance.api.change;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
@@ -28,13 +29,16 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static java.util.Comparator.comparing;
 
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.acceptance.testsuite.change.ChangeKindCreator;
 import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -43,17 +47,21 @@
 import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.LabelType;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
@@ -439,6 +447,64 @@
 
   @Test
   public void
+      notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileAlreadyExists_withoutCopyCondition()
+          throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(
+              LabelId.CODE_REVIEW, b -> b.setCopyAllScoresIfListOfFilesDidNotChange(true));
+      u.save();
+    }
+    notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileAlreadyExists();
+  }
+
+  @Test
+  public void
+      notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileAlreadyExists_withCopyCondition()
+          throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("has:unchanged-files"));
+      u.save();
+    }
+
+    notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileAlreadyExists();
+  }
+
+  private void notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileAlreadyExists()
+      throws Exception {
+    // create "existing file" and submit it.
+    String existingFile = "existing file";
+    Change.Id prep =
+        changeOperations
+            .newChange()
+            .project(project)
+            .file(existingFile)
+            .content("content")
+            .create();
+    vote(admin, prep.toString(), 2, 1);
+    gApi.changes().id(prep.get()).current().submit();
+
+    Change.Id changeId = changeOperations.newChange().project(project).create();
+    vote(admin, changeId.toString(), 2, 1);
+    vote(user, changeId.toString(), -2, -1);
+
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .file(existingFile)
+        .content("new content")
+        .create();
+    ChangeInfo c = detailedChange(changeId.toString());
+
+    // no votes are copied since the list of files changed ("existing file" was added to the
+    // change).
+    assertVotes(c, admin, 0, 0);
+    assertVotes(c, user, 0, 0);
+  }
+
+  @Test
+  public void
       notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsDeleted_withoutCopyCondition()
           throws Exception {
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -517,6 +583,94 @@
     assertVotes(c, user, -2, 0);
   }
 
+  @TestProjectInput(createEmptyCommit = false)
+  public void
+      stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedAsInitialCommit_withoutCopyCondition()
+          throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(
+              LabelId.CODE_REVIEW, b -> b.setCopyAllScoresIfListOfFilesDidNotChange(true));
+      u.save();
+    }
+    stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedAsInitialCommit();
+  }
+
+  @TestProjectInput(createEmptyCommit = false)
+  public void
+      stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedAsInitialCommit_withCopyCondition()
+          throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("has:unchanged-files"));
+      u.save();
+    }
+    stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedAsInitialCommit();
+  }
+
+  private void stickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedAsInitialCommit()
+      throws Exception {
+    Change.Id changeId =
+        changeOperations.newChange().project(project).file("file").content("content").create();
+    vote(admin, changeId.toString(), 2, 1);
+    vote(user, changeId.toString(), -2, -1);
+
+    changeOperations.change(changeId).newPatchset().file("file").content("new content").create();
+    ChangeInfo c = detailedChange(changeId.toString());
+
+    // only code review votes are copied since copyAllScoresIfListOfFilesDidNotChange is
+    // configured for that label, and list of files didn't change.
+    assertVotes(c, admin, 2, 0);
+    assertVotes(c, user, -2, 0);
+  }
+
+  @Test
+  public void
+      notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedOnEarlierPatchset_withoutCopyCondition()
+          throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(
+              LabelId.CODE_REVIEW, b -> b.setCopyAllScoresIfListOfFilesDidNotChange(true));
+      u.save();
+    }
+    notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedOnEarlierPatchset();
+  }
+
+  @Test
+  public void
+      notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedOnEarlierPatchset_withCopyCondition()
+          throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("has:unchanged-files"));
+      u.save();
+    }
+    notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedOnEarlierPatchset();
+  }
+
+  private void
+      notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsModifiedOnEarlierPatchset()
+          throws Exception {
+    Change.Id changeId =
+        changeOperations.newChange().project(project).file("file").content("content").create();
+    vote(admin, changeId.toString(), 2, 1);
+    vote(user, changeId.toString(), -2, -1);
+
+    changeOperations.change(changeId).newPatchset().file("new file").content("content").create();
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .file("new file")
+        .content("new content")
+        .create();
+    ChangeInfo c = detailedChange(changeId.toString());
+
+    // Don't copy over votes since ps1->ps2 should copy over, but ps2->ps3 should not.
+    assertVotes(c, admin, 0, 0);
+    assertVotes(c, user, 0, 0);
+  }
+
   @Test
   public void
       notStickyWithCopyAllScoresIfListOfFilesDidNotChangeWhenFileIsRenamed_withoutCopyCondition()
@@ -687,6 +841,120 @@
   }
 
   @Test
+  public void copyWithListOfFilesUnchanged_withoutCopyCondition() throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(
+              LabelId.CODE_REVIEW, b -> b.setCopyAllScoresIfListOfFilesDidNotChange(true));
+      u.save();
+    }
+    copyWithListOfFilesUnchanged();
+  }
+
+  @Test
+  public void copyWithListOfFilesUnchanged_withCopyCondition() throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("has:unchanged-files"));
+      u.save();
+    }
+    copyWithListOfFilesUnchanged();
+  }
+
+  private void copyWithListOfFilesUnchanged() throws Exception {
+    Change.Id changeId =
+        changeOperations.newChange().project(project).file("file").content("content").create();
+    vote(admin, changeId.toString(), 2, 1);
+    vote(user, changeId.toString(), -2, -1);
+
+    changeOperations.change(changeId).newPatchset().file("file").content("new content").create();
+    ChangeInfo c = detailedChange(changeId.toString());
+
+    // Code-Review votes are copied over from ps1-> ps2 since the list of files were unchanged.
+    assertVotes(c, admin, 2, 0);
+    assertVotes(c, user, -2, 0);
+
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .file("file")
+        .content("very new content")
+        .create();
+    c = detailedChange(changeId.toString());
+
+    // Code-Review votes are copied over from ps1-> ps3 since the list of files were unchanged.
+    assertVotes(c, admin, 2, 0);
+    assertVotes(c, user, -2, 0);
+
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .file("new file")
+        .content("new content")
+        .create();
+
+    c = detailedChange(changeId.toString());
+    // Code-Review votes are not copied over from ps1-> ps4 since a file was added.
+    assertVotes(c, admin, 0, 0);
+    assertVotes(c, user, 0, 0);
+
+    changeOperations.change(changeId).newPatchset().file("file").content("content").create();
+
+    c = detailedChange(changeId.toString());
+    // Code-Review votes are not copied over from ps1 -> ps5 since a file was added on ps4.
+    // Although the list of files is the same between ps4->ps5, we don't copy votes from before
+    // ps4.
+    assertVotes(c, admin, 0, 0);
+    assertVotes(c, user, 0, 0);
+  }
+
+  @Test
+  public void copyWithListOfFilesUnchangedButAddedMergeList() throws Exception {
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig()
+          .updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyCondition("has:unchanged-files"));
+      u.save();
+    }
+    Change.Id parent1ChangeId = changeOperations.newChange().create();
+    Change.Id parent2ChangeId = changeOperations.newChange().create();
+    Change.Id dummyParentChangeId = changeOperations.newChange().create();
+    Change.Id changeId =
+        changeOperations
+            .newChange()
+            .mergeOf()
+            .change(parent1ChangeId)
+            .and()
+            .change(parent2ChangeId)
+            .create();
+
+    Map<String, FileInfo> changedFilesFirstPatchset =
+        gApi.changes().id(changeId.get()).current().files();
+
+    assertThat(changedFilesFirstPatchset.keySet()).containsExactly("/COMMIT_MSG", "/MERGE_LIST");
+
+    // Make a Code-Review vote that should be sticky.
+    gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
+
+    changeOperations
+        .change(changeId)
+        .newPatchset()
+        .parent()
+        .patchset(PatchSet.id(dummyParentChangeId, 1))
+        .create();
+
+    Map<String, FileInfo> changedFilesSecondPatchset =
+        gApi.changes().id(changeId.get()).current().files();
+
+    // Only "/MERGE_LIST" was removed.
+    assertThat(changedFilesSecondPatchset.keySet()).containsExactly("/COMMIT_MSG");
+    ApprovalInfo approvalInfo =
+        Iterables.getOnlyElement(
+            gApi.changes().id(changeId.get()).current().votes().get(LabelId.CODE_REVIEW));
+    assertThat(approvalInfo._accountId).isEqualTo(admin.id().get());
+    assertThat(approvalInfo.value).isEqualTo(2);
+  }
+
+  @Test
   public void deleteStickyVote() throws Exception {
     String label = LabelId.CODE_REVIEW;
     try (ProjectConfigUpdate u = updateProject(project)) {
@@ -733,6 +1001,237 @@
     assertThat(r.getChange().approvals().get(PatchSet.id(r.getChange().getId(), 2))).hasSize(1);
   }
 
+  @Test
+  public void stickyVoteStoredOnUpload() throws Exception {
+    // Code-Review will be sticky.
+    String label = LabelId.CODE_REVIEW;
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    PushOneCommit.Result r = createChange();
+    // Add a new vote.
+    ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2);
+    input.tag = "tag";
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    // Make new patchsets, keeping the Code-Review +2 vote.
+    for (int i = 0; i < 9; i++) {
+      amendChange(r.getChangeId());
+    }
+
+    List<PatchSetApproval> patchSetApprovals =
+        r.getChange().notes().getApprovalsWithCopied().values().stream()
+            .sorted(comparing(a -> a.patchSetId().get()))
+            .collect(toImmutableList());
+
+    for (int i = 0; i < 10; i++) {
+      int patchSet = i + 1;
+      assertThat(patchSetApprovals.get(i).patchSetId().get()).isEqualTo(patchSet);
+      assertThat(patchSetApprovals.get(i).accountId().get()).isEqualTo(admin.id().get());
+      assertThat(patchSetApprovals.get(i).realAccountId().get()).isEqualTo(admin.id().get());
+      assertThat(patchSetApprovals.get(i).label()).isEqualTo(LabelId.CODE_REVIEW);
+      assertThat(patchSetApprovals.get(i).value()).isEqualTo((short) 2);
+      assertThat(patchSetApprovals.get(i).tag().get()).isEqualTo("tag");
+      if (patchSet == 1) {
+        assertThat(patchSetApprovals.get(i).copied()).isFalse();
+      } else {
+        assertThat(patchSetApprovals.get(i).copied()).isTrue();
+      }
+    }
+  }
+
+  @Test
+  public void stickyVoteStoredOnRebase() throws Exception {
+    // Code-Review will be sticky.
+    String label = LabelId.CODE_REVIEW;
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    // Create two changes both with the same parent
+    PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Approve and submit the first change
+    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+    revision.review(ReviewInput.approve().label(LabelId.VERIFIED, 1));
+    revision.submit();
+
+    // Add an approval whose score should be copied.
+    gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+
+    // Rebase the second change
+    gApi.changes().id(r2.getChangeId()).rebase();
+
+    List<PatchSetApproval> patchSetApprovals =
+        r2.getChange().notes().getApprovalsWithCopied().values().stream()
+            .sorted(comparing(a -> a.patchSetId().get()))
+            .collect(toImmutableList());
+    PatchSetApproval nonCopied = patchSetApprovals.get(0);
+
+    assertThat(nonCopied.patchSetId().get()).isEqualTo(1);
+    assertThat(nonCopied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(nonCopied.value()).isEqualTo((short) 1);
+    assertThat(nonCopied.copied()).isFalse();
+
+    PatchSetApproval copied = patchSetApprovals.get(1);
+    assertThat(copied.patchSetId().get()).isEqualTo(2);
+    assertThat(copied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(copied.value()).isEqualTo((short) 1);
+    assertThat(copied.copied()).isTrue();
+  }
+
+  @Test
+  public void stickyVoteStoredOnUploadWithRealAccount() throws Exception {
+    // Give "user" permission to vote on behalf of other users.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .impersonation(true)
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
+
+    // Code-Review will be sticky.
+    String label = LabelId.CODE_REVIEW;
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    PushOneCommit.Result r = createChange();
+
+    // Add a new vote as user
+    requestScopeOperations.setApiUser(user.id());
+    ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1);
+    input.onBehalfOf = admin.email();
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    // Make a new patchset, keeping the Code-Review +1 vote.
+    amendChange(r.getChangeId());
+
+    List<PatchSetApproval> patchSetApprovals =
+        r.getChange().notes().getApprovalsWithCopied().values().stream()
+            .sorted(comparing(a -> a.patchSetId().get()))
+            .collect(toImmutableList());
+
+    PatchSetApproval nonCopied = patchSetApprovals.get(0);
+    assertThat(nonCopied.patchSetId().get()).isEqualTo(1);
+    assertThat(nonCopied.accountId().get()).isEqualTo(admin.id().get());
+    assertThat(nonCopied.realAccountId().get()).isEqualTo(user.id().get());
+    assertThat(nonCopied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(nonCopied.value()).isEqualTo((short) 1);
+    assertThat(nonCopied.copied()).isFalse();
+
+    PatchSetApproval copied = patchSetApprovals.get(1);
+    assertThat(copied.patchSetId().get()).isEqualTo(2);
+    assertThat(copied.accountId().get()).isEqualTo(admin.id().get());
+    assertThat(copied.realAccountId().get()).isEqualTo(user.id().get());
+    assertThat(copied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(copied.value()).isEqualTo((short) 1);
+    assertThat(copied.copied()).isTrue();
+  }
+
+  @Test
+  public void stickyVoteStoredOnUploadWithRealAccountAndTag() throws Exception {
+    // Give "user" permission to vote on behalf of other users.
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(
+            allowLabel(TestLabels.codeReview().getName())
+                .impersonation(true)
+                .ref("refs/heads/*")
+                .group(REGISTERED_USERS)
+                .range(-1, 1))
+        .update();
+
+    // Code-Review will be sticky.
+    String label = LabelId.CODE_REVIEW;
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    PushOneCommit.Result r = createChange();
+
+    // Add a new vote as user
+    requestScopeOperations.setApiUser(user.id());
+    ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1);
+    input.onBehalfOf = admin.email();
+    input.tag = "tag";
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    // Make a new patchset, keeping the Code-Review +1 vote.
+    amendChange(r.getChangeId());
+
+    List<PatchSetApproval> patchSetApprovals =
+        r.getChange().notes().getApprovalsWithCopied().values().stream()
+            .sorted(comparing(a -> a.patchSetId().get()))
+            .collect(toImmutableList());
+
+    PatchSetApproval nonCopied = patchSetApprovals.get(0);
+    assertThat(nonCopied.patchSetId().get()).isEqualTo(1);
+    assertThat(nonCopied.accountId().get()).isEqualTo(admin.id().get());
+    assertThat(nonCopied.realAccountId().get()).isEqualTo(user.id().get());
+    assertThat(nonCopied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(nonCopied.value()).isEqualTo((short) 1);
+    assertThat(nonCopied.tag().get()).isEqualTo("tag");
+    assertThat(nonCopied.copied()).isFalse();
+
+    PatchSetApproval copied = patchSetApprovals.get(1);
+    assertThat(copied.patchSetId().get()).isEqualTo(2);
+    assertThat(copied.accountId().get()).isEqualTo(admin.id().get());
+    assertThat(copied.realAccountId().get()).isEqualTo(user.id().get());
+    assertThat(copied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(copied.value()).isEqualTo((short) 1);
+    assertThat(nonCopied.tag().get()).isEqualTo("tag");
+    assertThat(copied.copied()).isTrue();
+  }
+
+  @Test
+  public void stickyVoteStoredCanBeRemoved() throws Exception {
+    // Code-Review will be sticky.
+    String label = LabelId.CODE_REVIEW;
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    PushOneCommit.Result r = createChange();
+
+    // Add a new vote
+    ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2);
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    // Make a new patchset, keeping the Code-Review +2 vote.
+    amendChange(r.getChangeId());
+    assertVotes(detailedChange(r.getChangeId()), admin, label, 2, null);
+
+    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.noScore());
+
+    PatchSetApproval nonCopiedSecondPatchsetRemovedVote =
+        Iterables.getOnlyElement(
+            r.getChange()
+                .notes()
+                .getApprovalsWithCopied()
+                .get(r.getChange().change().currentPatchSetId()));
+
+    assertThat(nonCopiedSecondPatchsetRemovedVote.patchSetId().get()).isEqualTo(2);
+    assertThat(nonCopiedSecondPatchsetRemovedVote.accountId().get()).isEqualTo(admin.id().get());
+    assertThat(nonCopiedSecondPatchsetRemovedVote.label()).isEqualTo(LabelId.CODE_REVIEW);
+    // The vote got removed since the latest patch-set only has one vote and it's "0".
+    assertThat(nonCopiedSecondPatchsetRemovedVote.value()).isEqualTo((short) 0);
+    assertThat(nonCopiedSecondPatchsetRemovedVote.copied()).isFalse();
+  }
+
   private void assertChangeKindCacheContains(ObjectId prior, ObjectId next) {
     ChangeKind kind =
         changeKindCache.getIfPresent(ChangeKindCacheImpl.Key.create(prior, next, "recursive"));
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRuleIT.java
index bc9f50a5..636b71d 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRuleIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.inject.Inject;
 import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 
 public class SubmitRuleIT extends AbstractDaemonTest {
@@ -39,6 +40,11 @@
     PushOneCommit.Result r = createChange();
     approve(r.getChangeId());
     List<SubmitRecord> recordsBeforeSubmission = submitRuleEvaluator.evaluate(r.getChange());
+    assertThat(
+            recordsBeforeSubmission.stream()
+                .map(record -> record.ruleName)
+                .collect(Collectors.toList()))
+        .containsExactly("gerrit~DefaultSubmitRule");
     gApi.changes().id(r.getChangeId()).current().submit();
     // Add a new label that blocks submission if not granted. In case we reevaluate the rules,
     // this would show up as blocking submission.
@@ -57,6 +63,11 @@
     PushOneCommit.Result r = createChange();
     approve(r.getChangeId());
     List<SubmitRecord> recordsBeforeSubmission = submitRuleEvaluator.evaluate(r.getChange());
+    assertThat(
+            recordsBeforeSubmission.stream()
+                .map(record -> record.ruleName)
+                .collect(Collectors.toList()))
+        .containsExactly("gerrit~DefaultSubmitRule");
     gApi.changes().id(r.getChangeId()).current().submit();
     // Add a new label that blocks submission if not granted. In case we reevaluate the rules,
     // this would show up as blocking submission.
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
index 607fbc0..5124d11 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
@@ -37,6 +37,8 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.server.project.testing.TestLabels;
 import com.google.inject.Inject;
+import java.util.HashSet;
+import java.util.Set;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Before;
 import org.junit.Test;
@@ -602,22 +604,38 @@
       int deletions2,
       String expectedFileDiff2,
       String oldFileName2) {
-    String expectedMessage =
+    String beginningOfMessage =
         "1 is the latest approved patch-set.\n"
             + "The change was submitted with unreviewed changes in the following files:\n"
             + "\n";
-    expectedMessage += fileDiff(expectedFileDiff1, oldFileName1, file1, insertions1, deletions1);
-    expectedMessage += fileDiff(expectedFileDiff2, oldFileName2, file2, insertions2, deletions2);
-    String expectedChangeMessage = "Change has been successfully merged\n\n" + expectedMessage;
-    assertThat(message.trim()).isEqualTo(expectedChangeMessage.trim());
-    assertThat(Iterables.getLast(sender.getMessages()).body()).contains(expectedMessage);
+    String fileDiff1 = fileDiff(expectedFileDiff1, oldFileName1, file1, insertions1, deletions1);
+    String expectedMessage1 = beginningOfMessage + fileDiff1;
+    String expectedMessage2 = "";
+    Set<String> expectedChangeMessages = new HashSet<>();
+    if (file2 != null) {
+      String fileDiff2 = fileDiff(expectedFileDiff2, oldFileName2, file2, insertions2, deletions2);
+      expectedMessage2 = beginningOfMessage + fileDiff2 + fileDiff1;
+      String expectedChangeMessage2 = "Change has been successfully merged\n\n" + expectedMessage2;
+      expectedMessage1 += fileDiff2;
+      expectedChangeMessages.add(expectedChangeMessage2.trim());
+    }
+    String expectedChangeMessage1 = "Change has been successfully merged\n\n" + expectedMessage1;
+    expectedChangeMessage1 = expectedChangeMessage1.trim();
+    expectedChangeMessages.add(expectedChangeMessage1.trim());
+
+    // The order of appearance in the diff for multiple files is not defined, so check both
+    // possible orders.
+    assertThat(expectedChangeMessages).contains(message.trim());
+    String email = Iterables.getLast(sender.getMessages()).body();
+    if (email.contains(expectedMessage1) || expectedMessage2.isEmpty()) {
+      assertThat(email).contains(expectedMessage1.trim());
+    } else {
+      assertThat(email).contains(expectedMessage2.trim());
+    }
   }
 
   private String fileDiff(
       String expectedFileDiff, String oldFileName, String file, int insertions, int deletions) {
-    if (file == null) {
-      return "";
-    }
     String expectedMessage =
         "```\n"
             + String.format("The name of the file: %s\n", file)
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
index bdb03d2..18e192d 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CommitIT.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.acceptance.api.project;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
@@ -28,6 +31,7 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.IncludedInInfo;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -99,6 +103,53 @@
   }
 
   @Test
+  public void includedInMergedChange_filtersOutNonVisibleBranches() throws Exception {
+    Result baseChange = createAndSubmitChange("refs/for/master");
+
+    createBranch(BranchNameKey.create(project, "test-branch-1"));
+    createBranch(BranchNameKey.create(project, "test-branch-2"));
+    createAndSubmitChange("refs/for/test-branch-1");
+    createAndSubmitChange("refs/for/test-branch-2");
+
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
+        .containsExactly("master", "test-branch-1", "test-branch-2");
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(block(Permission.READ).ref("refs/heads/test-branch-1").group(REGISTERED_USERS))
+        .update();
+
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).branches)
+        .containsExactly("master", "test-branch-2");
+  }
+
+  @Test
+  public void includedInMergedChange_filtersOutNonVisibleTags() throws Exception {
+    String tagBase = "tag_base";
+    String tagBranch1 = "tag_1";
+
+    Result baseChange = createAndSubmitChange("refs/for/master");
+    createLightWeightTag(tagBase);
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags).containsExactly(tagBase);
+
+    createBranch(BranchNameKey.create(project, "test-branch-1"));
+    createAndSubmitChange("refs/for/test-branch-1");
+    createLightWeightTag(tagBranch1);
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags)
+        .containsExactly(tagBase, tagBranch1);
+
+    projectOperations
+        .project(project)
+        .forUpdate()
+        // Tag permissions are controlled by read permissions on branches. Blocking read permission
+        // on test-branch-1 so that tagBranch1 becomes non-visible
+        .add(block(Permission.READ).ref("refs/heads/test-branch-1").group(REGISTERED_USERS))
+        .update();
+    assertThat(getIncludedIn(baseChange.getCommit().getId()).tags).containsExactly(tagBase);
+  }
+
+  @Test
   public void cherryPickWithoutMessageSameBranch() throws Exception {
     String destBranch = "master";
 
@@ -390,4 +441,15 @@
     assertThat(actual.email).isEqualTo(expected.email());
     assertThat(actual.name).isEqualTo(expected.fullName());
   }
+
+  private Result createAndSubmitChange(String branch) throws Exception {
+    Result r = createChange(branch);
+    approve(r.getChangeId());
+    gApi.changes().id(r.getChangeId()).current().submit();
+    return r;
+  }
+
+  private void createLightWeightTag(String tagName) throws Exception {
+    pushHead(testRepo, RefNames.REFS_TAGS + tagName, false, false);
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index dd70d4a..c42628c 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -842,7 +842,7 @@
     String otherLink = "https://other.example.com";
     input = new ConfigInput();
     addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink);
-    info = setConfig(child, input);
+    setConfig(child, input);
 
     expected = new HashMap<>();
     expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink));
@@ -866,7 +866,7 @@
     String otherLink = "https://other.example.com";
     input = new ConfigInput();
     addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink);
-    info = setConfig(project, input);
+    setConfig(project, input);
 
     expected = new HashMap<>();
     expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink));
@@ -888,7 +888,7 @@
 
     input = new ConfigInput();
     addCommentLink(input, BUGZILLA, null);
-    info = setConfig(project, input);
+    setConfig(project, input);
 
     expected = new HashMap<>();
     expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index bcd98d4..58dc0b0 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -50,7 +50,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.webui.EditWebLink;
-import com.google.gerrit.server.change.FileInfoJsonExperimentImpl;
 import com.google.gerrit.server.patch.DiffOperations;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
 import com.google.inject.Inject;
@@ -93,8 +92,6 @@
   @Inject private ProjectOperations projectOperations;
 
   private boolean intraline;
-  private boolean useNewDiffCacheListFiles;
-  private boolean useNewDiffCacheGetDiff;
 
   private ObjectId initialCommit;
   private ObjectId commit1;
@@ -109,11 +106,6 @@
     baseConfig.setString("cache", "diff_intraline", "timeout", "1 minute");
 
     intraline = baseConfig.getBoolean(TEST_PARAMETER_MARKER, "intraline", false);
-    useNewDiffCacheListFiles =
-        Arrays.asList(baseConfig.getStringList("experiments", null, "enabled"))
-            .contains(FileInfoJsonExperimentImpl.NEW_DIFF_CACHE_FEATURE);
-    useNewDiffCacheGetDiff =
-        baseConfig.getBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", false);
 
     ObjectId headCommit = testRepo.getRepository().resolve("HEAD");
     initialCommit = headCommit;
@@ -1325,7 +1317,7 @@
   }
 
   @Test
-  public void addedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
+  public void addedUnrelatedFileIsIgnored_forPatchSetDiffWithRebase() throws Exception {
     ObjectId commit2 = addCommit(commit1, "file_added_in_another_commit.txt", "Some file content");
 
     rebaseChangeOn(changeId, commit2);
@@ -1337,7 +1329,7 @@
   }
 
   @Test
-  public void removedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
+  public void removedUnrelatedFileIsIgnored_forPatchSetDiffWithRebase() throws Exception {
     ObjectId commit2 = addCommitRemovingFiles(commit1, FILE_NAME2);
 
     rebaseChangeOn(changeId, commit2);
@@ -1349,7 +1341,7 @@
   }
 
   @Test
-  public void renamedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
+  public void renamedUnrelatedFileIsIgnored_forPatchSetDiffWithRebase() throws Exception {
     ObjectId commit2 = addCommitRenamingFile(commit1, FILE_NAME2, "a_new_file_name.txt");
 
     rebaseChangeOn(changeId, commit2);
@@ -1361,10 +1353,10 @@
   }
 
   @Test
-  public void renamedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase_WhenEquallyModifiedInBoth()
+  @Ignore
+  public void renamedUnrelatedFileIsIgnored_forPatchSetDiffWithRebase_whenEquallyModifiedInBoth()
       throws Exception {
     // TODO(ghareeb): fix this test for the new diff cache implementation
-    assume().that(useNewDiffCacheListFiles).isFalse();
 
     Function<String, String> contentModification =
         fileContent -> fileContent.replace("1st line\n", "First line\n");
@@ -1387,7 +1379,7 @@
   }
 
   @Test
-  public void renamedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase_WhenModifiedDuringRebase()
+  public void renamedUnrelatedFileIsIgnored_forPatchSetDiffWithRebase_whenModifiedDuringRebase()
       throws Exception {
     String renamedFilePath = "renamed_some_file.txt";
     ObjectId commit2 =
@@ -1455,9 +1447,9 @@
   }
 
   @Test
+  @Ignore
   public void filesTouchedByPatchSetsAndContainingOnlyRebaseHunksAreIgnored() throws Exception {
     // TODO(ghareeb): fix this test for the new diff cache implementation
-    assume().that(useNewDiffCacheListFiles).isFalse();
 
     addModifiedPatchSet(
         changeId, FILE_NAME, fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"));
@@ -2179,7 +2171,7 @@
   }
 
   @Test
-  public void rebaseHunkInRenamedFileIsIdentified_WhenFileIsRenamedDuringRebase() throws Exception {
+  public void rebaseHunkInRenamedFileIsIdentified_whenFileIsRenamedDuringRebase() throws Exception {
     String renamedFilePath = "renamed_some_file.txt";
     ObjectId commit2 =
         addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 1\n", "Line one\n"));
@@ -2208,7 +2200,7 @@
   }
 
   @Test
-  public void rebaseHunkInRenamedFileIsIdentified_WhenFileIsRenamedInPatchSets() throws Exception {
+  public void rebaseHunkInRenamedFileIsIdentified_whenFileIsRenamedInPatchSets() throws Exception {
     String renamedFilePath = "renamed_some_file.txt";
     gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFilePath);
     gApi.changes().id(changeId).edit().publish();
@@ -2247,7 +2239,7 @@
   }
 
   @Test
-  public void renamedFileWithOnlyRebaseHunksIsIdentified_WhenRenamedBetweenPatchSets()
+  public void renamedFileWithOnlyRebaseHunksIsIdentified_whenRenamedBetweenPatchSets()
       throws Exception {
     String newFilePath1 = "renamed_some_file.txt";
     gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath1);
@@ -2282,7 +2274,7 @@
   }
 
   @Test
-  public void renamedFileWithOnlyRebaseHunksIsIdentified_WhenRenamedForRebaseAndForPatchSets()
+  public void renamedFileWithOnlyRebaseHunksIsIdentified_whenRenamedForRebaseAndForPatchSets()
       throws Exception {
     String newFilePath1 = "renamed_some_file.txt";
     gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath1);
@@ -2837,7 +2829,7 @@
   }
 
   @Test
-  public void symlinkConvertedToRegularFileIsIdentifiedAsRewritten() throws Exception {
+  public void addDeleteByJgit_isIdentifiedAsRewritten() throws Exception {
     String target = "file.txt";
     String symlink = "link.lnk";
 
@@ -2846,58 +2838,70 @@
         pushFactory
             .create(admin.newIdent(), testRepo, "Commit Subject", target, "content")
             .addSymlink(symlink, target);
-
     PushOneCommit.Result result = push.to("refs/for/master");
     String initialRev = gApi.changes().id(result.getChangeId()).get().currentRevision;
+    String cId = result.getChangeId();
 
-    // Delete the symlink with patchset 2
-    gApi.changes().id(result.getChangeId()).edit().deleteFile(symlink);
-    gApi.changes().id(result.getChangeId()).edit().publish();
+    // Delete the symlink with PS2
+    gApi.changes().id(cId).edit().deleteFile(symlink);
+    gApi.changes().id(cId).edit().publish();
 
-    // Re-add the symlink as a regular file with patchset 3
-    gApi.changes()
-        .id(result.getChangeId())
-        .edit()
-        .modifyFile(symlink, RawInputUtil.create("Content of the new file named 'symlink'"));
-    gApi.changes().id(result.getChangeId()).edit().publish();
+    // Re-add the symlink as a regular file with PS3
+    gApi.changes().id(cId).edit().modifyFile(symlink, RawInputUtil.create("new content"));
+    gApi.changes().id(cId).edit().publish();
 
-    Map<String, FileInfo> changedFiles =
-        gApi.changes().id(result.getChangeId()).current().files(initialRev);
-
+    // Changed files: JGit returns two {DELETED/ADDED} entries for the file.
+    // The diff logic combines both into a single REWRITTEN entry.
+    Map<String, FileInfo> changedFiles = gApi.changes().id(cId).current().files(initialRev);
     assertThat(changedFiles.keySet()).containsExactly("/COMMIT_MSG", symlink);
-
-    // Both old and new diff caches agree that the state is rewritten
     assertThat(changedFiles.get(symlink).status).isEqualTo('W'); // Rewritten
 
-    DiffInfo diffInfo =
-        gApi.changes().id(result.getChangeId()).current().file(symlink).diff(initialRev);
+    // Detailed diff: Old diff cache returns ADDED entry. New Diff Cache returns REWRITE.
+    DiffInfo diffInfo = gApi.changes().id(cId).current().file(symlink).diff(initialRev);
+    assertThat(diffInfo.content).hasSize(1);
+    assertThat(diffInfo).content().element(0).linesOfB().containsExactly("new content");
+    assertThat(diffInfo.changeType).isEqualTo(ChangeType.REWRITE);
+  }
 
-    // TODO(ghareeb): Remove the else branch when the new diff cache is rolled out as default.
-    if (useNewDiffCacheGetDiff) {
-      // File diff in New diff cache: change type is correctly identified as REWRITTEN
-      assertThat(diffInfo.changeType).isEqualTo(ChangeType.REWRITE);
-      assertThat(diffInfo.content).hasSize(2);
-      assertThat(diffInfo)
-          .content()
-          .element(0)
-          .linesOfB()
-          .containsExactly("Content of the new file named 'symlink'");
-      assertThat(diffInfo).content().element(1).linesOfA().containsExactly("file.txt");
-    } else {
-      // File diff in old diff cache: The diff logic identifies two entries for the file:
-      // 1. One entry as 'DELETED' for the symlink.
-      // 2. Another entry as 'ADDED' for the new regular file.
-      // Since the diff logic returns a single entry, the implementation prioritizes  the 'ADDED'
-      // entry in this case so that the user is able to see the new content that was added to the
-      // file.
-      assertThat(diffInfo.changeType).isEqualTo(ChangeType.ADDED);
-      assertThat(diffInfo.content).hasSize(1);
-      assertThat(diffInfo)
-          .content()
-          .element(0)
-          .linesOfB()
-          .containsExactly("Content of the new file named 'symlink'");
-    }
+  @Test
+  public void renameDeleteByJgit_isIdentifiedAsRewritten() throws Exception {
+    String target = "file.txt";
+    String symlink = "link.lnk";
+    PushOneCommit push =
+        pushFactory
+            .create(admin.newIdent(), testRepo, "Commit Subject", target, "content")
+            .addSymlink(symlink, target);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    String cId = result.getChangeId();
+    String initialRev = gApi.changes().id(cId).get().currentRevision;
+
+    // Delete both symlink and target with PS2
+    gApi.changes().id(cId).edit().deleteFile(symlink);
+    gApi.changes().id(cId).edit().deleteFile(target);
+    gApi.changes().id(cId).edit().publish();
+
+    // Re-create the symlink as a regular file with PS3
+    gApi.changes().id(cId).edit().modifyFile(symlink, RawInputUtil.create("content"));
+    gApi.changes().id(cId).edit().publish();
+
+    // Changed files: JGit returns two {DELETED/RENAMED} entries for the file.
+    // The diff logic combines both into a single REWRITTEN entry.
+    Map<String, FileInfo> changedFiles = gApi.changes().id(cId).current().files(initialRev);
+    assertThat(changedFiles.keySet()).containsExactly("/COMMIT_MSG", symlink);
+    assertThat(changedFiles.get(symlink).status).isEqualTo('W'); // Rewritten
+
+    // Detailed diff: Old diff cache returns RENAMED entry. New Diff Cache returns REWRITE.
+    DiffInfo diffInfo = gApi.changes().id(cId).current().file(symlink).diff(initialRev);
+    assertThat(diffInfo)
+        .diffHeader()
+        .containsExactly(
+            "diff --git a/file.txt b/link.lnk",
+            "similarity index 100%",
+            "rename from file.txt",
+            "rename to link.lnk");
+    assertThat(diffInfo.content).hasSize(1);
+    assertThat(diffInfo).content().element(0).commonLines().containsExactly("content");
+    assertThat(diffInfo.changeType).isEqualTo(ChangeType.REWRITE);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 3d2026b..d3fe83f 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -1199,6 +1200,7 @@
   }
 
   @Test
+  @UseClockStep
   public void cherryPickSetsReadyChangeOnNewPatchset() throws Exception {
     PushOneCommit.Result result = pushTo("refs/for/master");
     CherryPickInput input = new CherryPickInput();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheForSingleFileIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheForSingleFileIT.java
deleted file mode 100644
index 46954e98..0000000
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheForSingleFileIT.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.api.revision;
-
-import com.google.gerrit.testing.ConfigSuite;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * Runs the {@link RevisionDiffIT} tests with the new diff cache, enabled for the single file "Get
- * Diff" endpoint. This is temporary until the new diff cache is fully deployed. The new diff cache
- * will become the default in the future.
- */
-public class RevisionNewDiffCacheForSingleFileIT extends RevisionDiffIT {
-  @ConfigSuite.Default
-  public static Config newDiffCacheConfig() {
-    Config config = new Config();
-    config.setBoolean("cache", "diff_cache", "runNewDiffCache_GetDiff", true);
-    return config;
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java
deleted file mode 100644
index 714bd78..0000000
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionNewDiffCacheIT.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.api.revision;
-
-import com.google.gerrit.server.change.FileInfoJsonExperimentImpl;
-import com.google.gerrit.testing.ConfigSuite;
-import org.eclipse.jgit.lib.Config;
-
-/**
- * Runs the {@link RevisionDiffIT} with the list files endpoint using the new diff cache. This is
- * temporary until the new diff cache is fully deployed. The new diff cache will become the default
- * in the future.
- */
-public class RevisionNewDiffCacheIT extends RevisionDiffIT {
-  @ConfigSuite.Default
-  public static Config newDiffCacheConfig() {
-    Config config = new Config();
-    config.setString(
-        "experiments", null, "enabled", FileInfoJsonExperimentImpl.NEW_DIFF_CACHE_FEATURE);
-    return config;
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 85a7b29..875ce97 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -156,15 +156,15 @@
   @UseClockStep
   @Test
   public void addedRobotCommentsAreLinkedToChangeMessages() throws Exception {
-    TestTimeUtil.resetWithClockStep(0, TimeUnit.SECONDS);
-    createChange();
-    /* Advancing the time after creating the change so that the first robot comment is not in the same timestamp as with the change creation */
+    // Advancing the time after creating the change so that the first robot comment is not in the
+    // same timestamp as with the change creation.
     TestTimeUtil.incrementClock(10, TimeUnit.SECONDS);
 
     RobotCommentInput c1 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
     RobotCommentInput c2 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
     RobotCommentInput c3 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
-    /* Give the robot comments identifiable names for testing */
+
+    // Give the robot comments identifiable names for testing
     c1.message = "robot comment 1";
     c2.message = "robot comment 2";
     c3.message = "robot comment 3";
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index a90cd56..0e0168e 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -281,7 +281,7 @@
   }
 
   @Test
-  public void rebaseEditWithConflictsRest_Conflict() throws Exception {
+  public void rebaseEditWithConflictsRest_conflict() throws Exception {
     PatchSet currentPatchSet = getCurrentPatchSet(changeId2);
     createEmptyEditFor(changeId2);
     gApi.changes().id(changeId2).edit().modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW));
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java b/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java
index 88d0937..f4918b6 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractForcePush.java
@@ -21,6 +21,7 @@
 import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.OK;
 import static org.eclipse.jgit.transport.RemoteRefUpdate.Status.REJECTED_OTHER_REASON;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -34,7 +35,7 @@
 import org.junit.Test;
 
 public abstract class AbstractForcePush extends AbstractDaemonTest {
-  @Inject private ProjectOperations projectOperations;
+  @Inject protected ProjectOperations projectOperations;
 
   @Test
   public void forcePushNotAllowed() throws Exception {
@@ -116,6 +117,28 @@
     assertDeleteRef(OK);
   }
 
+  @Test
+  public void directPushSendsEmail() throws Exception {
+    // create a change
+    PushOneCommit push1 =
+        pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
+    PushOneCommit.Result r = push1.to("refs/for/master");
+    r.assertOkStatus();
+
+    // Add reviewer to receive notifications
+    gApi.changes().id(r.getChangeId()).addReviewer(user.email());
+    sender.clear();
+
+    // direct submit the change
+    PushOneCommit.Result r1 = push1.to("refs/heads/master");
+    r1.assertOkStatus();
+
+    // email received
+    assertThat(sender.getMessages()).hasSize(1);
+    assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
+        .contains("has submitted this change");
+  }
+
   private void assertDeleteRef(RemoteRefUpdate.Status expectedStatus) throws Exception {
     BranchInput in = new BranchInput();
     in.ref = "refs/heads/test";
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 5cf0403..2253202 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -1266,13 +1266,13 @@
   }
 
   @Test
-  public void pushForMasterWithApprovals_MissingLabel() throws Exception {
+  public void pushForMasterWithApprovals_missingLabel() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master%l=Verify");
     r.assertErrorStatus("label \"Verify\" is not a configured label");
   }
 
   @Test
-  public void pushForMasterWithApprovals_ValueOutOfRange() throws Exception {
+  public void pushForMasterWithApprovals_valueOutOfRange() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review-3");
     r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
   }
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 92770ba..194f5f9 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -1434,7 +1434,6 @@
    * Assert that refs seen by a non-admin user match the expected refs.
    *
    * @param expectedRefs expected refs.
-   * @throws Exception
    */
   private void assertUploadPackRefs(String... expectedRefs) throws Exception {
     assertRefs(project, user, true, expectedRefs);
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java b/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java
new file mode 100644
index 0000000..83df896
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/pgm/ChangeExternalIdCaseSensitivityIT.java
@@ -0,0 +1,239 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.pgm;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Test;
+
+@NoHttpd
+public class ChangeExternalIdCaseSensitivityIT extends StandaloneSiteTest {
+
+  private static final boolean CASE_SENSITIVE = false;
+  private static final boolean CASE_INSENSITIVE = true;
+
+  private ServerContext ctx;
+  private ExternalIdNotes extIdNotes;
+  private ExternalIdFactory extIdFactory;
+  private MetaDataUpdate md;
+  private FileBasedConfig config;
+
+  @After
+  public void cleanup() throws Exception {
+    if (ctx != null) {
+      ctx.close();
+    }
+  }
+
+  @Test
+  public void externalIdNoteNameIsMigratedToCaseInsensitive() throws Exception {
+    prepareExternalIdNotes(CASE_SENSITIVE);
+
+    ctx.close();
+    runChangeExternalIdCaseSensitivity();
+    ctx = startServer();
+    extIdNotes = getExternalIdNotes(ctx);
+
+    assertExternalIdNotes(CASE_INSENSITIVE);
+  }
+
+  @Test
+  public void externalIdNoteNameIsMigratedToCaseSensitive() throws Exception {
+    prepareExternalIdNotes(CASE_INSENSITIVE);
+
+    ctx.close();
+    runChangeExternalIdCaseSensitivity();
+    ctx = startServer();
+    extIdNotes = getExternalIdNotes(ctx);
+
+    assertExternalIdNotes(CASE_SENSITIVE);
+  }
+
+  @Test
+  public void migrationFailsWithDuplicates() throws Exception {
+    prepareExternalIdNotes(CASE_SENSITIVE);
+    extIdNotes.insert(extIdFactory.create(SCHEME_USERNAME, "JohnDoe", Account.id(1)));
+    extIdNotes.commit(md);
+
+    assertThat(extIdNotes.get(ExternalId.Key.parse("username:johndoe", false)).isPresent())
+        .isTrue();
+    assertThat(extIdNotes.get(ExternalId.Key.parse("username:JohnDoe", false)).isPresent())
+        .isTrue();
+
+    ctx.close();
+    assertThrows(DuplicateExternalIdKeyException.class, () -> runChangeExternalIdCaseSensitivity());
+    ctx = startServer();
+    extIdNotes = getExternalIdNotes(ctx);
+
+    assertExternalIdNotes(CASE_SENSITIVE);
+    assertThat(extIdNotes.get(ExternalId.Key.parse("username:johndoe", false)).isPresent())
+        .isTrue();
+    assertThat(extIdNotes.get(ExternalId.Key.parse("username:JohnDoe", false)).isPresent())
+        .isTrue();
+  }
+
+  @Test
+  public void userNameCaseInsensitiveOptionIsSwitched() throws Exception {
+    configureUserNameCaseInsensitive(CASE_SENSITIVE);
+    assertThat(config.getBoolean("auth", "userNameCaseInsensitive", false)).isFalse();
+    runChangeExternalIdCaseSensitivity();
+    config.load();
+    assertThat(config.getBoolean("auth", "userNameCaseInsensitive", false)).isTrue();
+    runChangeExternalIdCaseSensitivity();
+    config.load();
+    assertThat(config.getBoolean("auth", "userNameCaseInsensitive", false)).isFalse();
+  }
+
+  @Test
+  public void dryrunDoesNotPersistChanges() throws Exception {
+    prepareExternalIdNotes(CASE_SENSITIVE);
+    ctx.close();
+    runGerrit("ChangeExternalIdCaseSensitivity", "-d", sitePaths.site_path.toString(), "--dryrun");
+
+    config.load();
+    assertThat(config.getBoolean("auth", "userNameCaseInsensitive", false)).isFalse();
+
+    ctx = startServer();
+    assertExternalIdNotes(CASE_SENSITIVE);
+  }
+
+  private void prepareExternalIdNotes(boolean userNameCaseInsensitive) throws Exception {
+    configureUserNameCaseInsensitive(userNameCaseInsensitive);
+    initSite();
+    ctx = startServer();
+    extIdFactory = ctx.getInjector().getInstance(ExternalIdFactory.class);
+    Project.NameKey allUsers = ctx.getInjector().getInstance(AllUsersName.class);
+    extIdNotes = getExternalIdNotes(ctx, allUsers);
+    md = getMetaDataUpdate(ctx, allUsers);
+
+    extIdNotes.insert(extIdFactory.create(SCHEME_USERNAME, "johndoe", Account.id(0)));
+    extIdNotes.insert(extIdFactory.create(SCHEME_GERRIT, "johndoe", Account.id(0)));
+
+    extIdNotes.insert(extIdFactory.create(SCHEME_USERNAME, "JaneDoe", Account.id(1)));
+    extIdNotes.insert(extIdFactory.create(SCHEME_GERRIT, "JaneDoe", Account.id(1)));
+
+    extIdNotes.insert(extIdFactory.create(SCHEME_MAILTO, "Jane@Doe.com", Account.id(1)));
+    extIdNotes.insert(extIdFactory.create(SCHEME_UUID, "Abc123", Account.id(1)));
+    extIdNotes.insert(extIdFactory.create(SCHEME_GPGKEY, "Abc123", Account.id(1)));
+    extIdNotes.insert(extIdFactory.create(SCHEME_EXTERNAL, "saml/JaneDoe", Account.id(1)));
+    extIdNotes.commit(md);
+
+    assertExternalIdNotes(userNameCaseInsensitive);
+  }
+
+  private void assertExternalIdNotes(boolean userNameCaseInsensitive) throws Exception {
+    assertThat(
+            extIdNotes
+                .get(ExternalId.Key.parse("username:johndoe", userNameCaseInsensitive))
+                .isPresent())
+        .isTrue();
+    assertThat(
+            extIdNotes
+                .get(ExternalId.Key.parse("username:JaneDoe", !userNameCaseInsensitive))
+                .isPresent())
+        .isFalse();
+    assertThat(
+            extIdNotes
+                .get(ExternalId.Key.parse("username:JaneDoe", userNameCaseInsensitive))
+                .isPresent())
+        .isTrue();
+
+    assertThat(
+            extIdNotes
+                .get(ExternalId.Key.parse("gerrit:johndoe", userNameCaseInsensitive))
+                .isPresent())
+        .isTrue();
+    assertThat(
+            extIdNotes
+                .get(ExternalId.Key.parse("gerrit:JaneDoe", !userNameCaseInsensitive))
+                .isPresent())
+        .isFalse();
+    assertThat(
+            extIdNotes
+                .get(ExternalId.Key.parse("gerrit:JaneDoe", userNameCaseInsensitive))
+                .isPresent())
+        .isTrue();
+
+    assertThat(extIdNotes.get(ExternalId.Key.parse("mailto:Jane@Doe.com", false)).isPresent())
+        .isTrue();
+    assertThat(extIdNotes.get(ExternalId.Key.parse("uuid:Abc123", false)).isPresent()).isTrue();
+    assertThat(extIdNotes.get(ExternalId.Key.parse("gpgkey:Abc123", false)).isPresent()).isTrue();
+    assertThat(extIdNotes.get(ExternalId.Key.parse("external:saml/JaneDoe", false)).isPresent())
+        .isTrue();
+  }
+
+  private void configureUserNameCaseInsensitive(boolean userNameCaseInsensitive)
+      throws IOException, ConfigInvalidException {
+    config = new FileBasedConfig(baseConfig, sitePaths.gerrit_config.toFile(), FS.DETECTED);
+    config.load();
+    config.setBoolean("auth", null, "userNameCaseInsensitive", userNameCaseInsensitive);
+    config.save();
+    if (userNameCaseInsensitive) {
+      assertThat(config.getBoolean("auth", "userNameCaseInsensitive", false)).isTrue();
+    } else {
+      assertThat(config.getBoolean("auth", "userNameCaseInsensitive", false)).isFalse();
+    }
+  }
+
+  private void initSite() throws Exception {
+    runGerrit("init", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+  }
+
+  private void runChangeExternalIdCaseSensitivity() throws Exception {
+    runGerrit("ChangeExternalIdCaseSensitivity", "-d", sitePaths.site_path.toString(), "--batch");
+  }
+
+  private static ExternalIdNotes getExternalIdNotes(ServerContext ctx) throws Exception {
+    return getExternalIdNotes(ctx, ctx.getInjector().getInstance(AllUsersName.class));
+  }
+
+  private static ExternalIdNotes getExternalIdNotes(ServerContext ctx, Project.NameKey allUsers)
+      throws Exception {
+    GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
+    ExternalIdNotes.FactoryNoReindex extIdNotesFactory =
+        ctx.getInjector().getInstance(ExternalIdNotes.FactoryNoReindex.class);
+    return extIdNotesFactory.load(repoManager.openRepository(allUsers));
+  }
+
+  private static MetaDataUpdate getMetaDataUpdate(ServerContext ctx, Project.NameKey allUsers)
+      throws Exception {
+    MetaDataUpdate.Server metaDataUpdateFactory =
+        ctx.getInjector().getInstance(MetaDataUpdate.Server.class);
+    return metaDataUpdateFactory.create(allUsers);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index 1a96d82..ed5e559 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.cancellation.RequestCancelledException;
 import com.google.gerrit.server.cancellation.RequestStateProvider;
 import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
@@ -153,7 +152,7 @@
             "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "1ms"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
-        .isEqualTo("Client Provided Deadline Exceeded\n\ntimeout=1ms");
+        .isEqualTo("Client Provided Deadline Exceeded\n\nclient.timeout=1ms");
   }
 
   @Test
@@ -206,7 +205,8 @@
   public void stricterDeadlineTakesPrecedence() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
-    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\nfoo.timeout=1ms");
   }
 
   @Test
@@ -215,7 +215,8 @@
   public void abortIfServerDeadlineExceeded_requestType() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
-    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
   }
 
   @Test
@@ -224,7 +225,34 @@
   public void abortIfServerDeadlineExceeded_requestUriPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
-    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(
+      name = "deadline.default.excludedRequestUriPattern",
+      value = "/projects/non-matching")
+  public void abortIfServerDeadlineExceeded_excludedRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(
+      name = "deadline.default.excludedRequestUriPattern",
+      value = "/projects/non-matching")
+  public void abortIfServerDeadlineExceeded_requestUriPatternAndExcludedRequestUriPattern()
+      throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
   }
 
   @Test
@@ -233,7 +261,8 @@
   public void abortIfServerDeadlineExceeded_projectPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
-    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
   }
 
   @Test
@@ -242,7 +271,8 @@
   public void abortIfServerDeadlineExceeded_account() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
-    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=1ms");
   }
 
   @Test
@@ -263,6 +293,24 @@
 
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*")
+  public void nonMatchingServerDeadlineIsIgnored_excludedRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*new")
+  public void nonMatchingServerDeadlineIsIgnored_requestUriPatternAndExcludedRequestUriPattern()
+      throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = ".*foo.*")
   public void nonMatchingServerDeadlineIsIgnored_projectPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
@@ -292,7 +340,8 @@
   public void nonAdvisoryDeadlineIsAppliedIfStricterAdvisoryDeadlineExists() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
-    assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=2ms");
+    assertThat(response.getEntityContent())
+        .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=2ms");
   }
 
   @Test
@@ -334,6 +383,14 @@
 
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
+  @GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "][")
+  public void invalidServerDeadlineIsIgnored_invalidExcludedRequestUriPattern() throws Exception {
+    RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
+    response.assertCreated();
+  }
+
+  @Test
+  @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.projectPattern", value = "][")
   public void invalidServerDeadlineIsIgnored_invalidProjectPattern() throws Exception {
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
@@ -373,7 +430,7 @@
             try {
               Thread.sleep(1000);
             } catch (InterruptedException e) {
-              throw new RuntimeException("interrupted during sleep");
+              throw new RuntimeException("interrupted during sleep", e);
             }
           }
         };
@@ -382,7 +439,7 @@
       RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
       assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
       assertThat(response.getEntityContent())
-          .isEqualTo("Server Deadline Exceeded\n\ntimeout=500ms");
+          .isEqualTo("Server Deadline Exceeded\n\ndefault.timeout=500ms");
     }
     // verify that the exceeded deadline for the previous request, isn't applied to a new request
     RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new2"));
@@ -397,7 +454,7 @@
             "/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "2ms"));
     assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
     assertThat(response.getEntityContent())
-        .isEqualTo("Client Provided Deadline Exceeded\n\ntimeout=2ms");
+        .isEqualTo("Client Provided Deadline Exceeded\n\nclient.timeout=2ms");
   }
 
   @Test
@@ -519,38 +576,24 @@
   public void abortPushIfServerDeadlineExceeded() throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
+    r.assertErrorStatus("Server Deadline Exceeded (default.timeout=1ms)");
   }
 
   @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
-  @GerritConfig(
-      name = "experiments.enabled",
-      value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION)
   public void abortPushIfTimeoutExceeded() throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
-  }
-
-  @Test
-  @GerritConfig(name = "receive.timeout", value = "1ms")
-  public void pushNotAbortedIfTimeoutExceededAndExperimentNotEnabled() throws Exception {
-    PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
-    PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertOkStatus();
+    r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
   }
 
   @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
   @GerritConfig(name = "deadline.default.timeout", value = "10s")
-  @GerritConfig(
-      name = "experiments.enabled",
-      value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION)
   public void receiveTimeoutTakesPrecedence() throws Exception {
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
+    r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
   }
 
   @Test
@@ -560,7 +603,7 @@
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Client Provided Deadline Exceeded (timeout=1ms)");
+    r.assertErrorStatus("Client Provided Deadline Exceeded (client.timeout=1ms)");
   }
 
   @Test
@@ -611,21 +654,18 @@
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Client Provided Deadline Exceeded (timeout=2ms)");
+    r.assertErrorStatus("Client Provided Deadline Exceeded (client.timeout=2ms)");
   }
 
   @Test
   @GerritConfig(name = "receive.timeout", value = "1ms")
-  @GerritConfig(
-      name = "experiments.enabled",
-      value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_PUSH_CANCELLATION)
   public void clientProvidedDeadlineOnPushDoesntOverrideServerTimeout() throws Exception {
     List<String> pushOptions = new ArrayList<>();
     pushOptions.add("deadline=10m");
     PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
     push.setPushOptions(pushOptions);
     PushOneCommit.Result r = push.to("refs/for/master");
-    r.assertErrorStatus("Server Deadline Exceeded (timeout=1ms)");
+    r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
index 219ef12..4efdbba 100644
--- a/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/RestApiServletIT.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.httpd.restapi.ParameterParser;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -83,6 +84,11 @@
   }
 
   @Test
+  public void experimentRequestParamIsReserved() throws Exception {
+    assertRestResponseWithParameters(SC_OK, ParameterParser.EXPERIMENT_PARAMETER, "exp1");
+  }
+
+  @Test
   public void xGerritUpdatedRefNotSetByDefault() throws Exception {
     Result change = createChange();
     String origin = adminRestSession.url();
@@ -415,6 +421,15 @@
     return change.getChange().notes().getRevision();
   }
 
+  private RestResponse assertRestResponseWithParameters(int status, String k, String v)
+      throws Exception {
+    RestResponse response =
+        adminRestSession.getWithHeaders(ANY_REST_API + "?" + k + "=" + v, ACCEPT_STAR_HEADER);
+    assertThat(response.getStatusCode()).isEqualTo(status);
+
+    return response;
+  }
+
   private RestResponse prettyJsonRestResponse(String ppArgument, int ppValue) throws Exception {
     RestResponse response =
         adminRestSession.getWithHeaders(
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 530f2ec..7e40b2b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -711,6 +711,94 @@
   }
 
   @Test
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/.*")
+  public void traceExcludedRequestUriPattern() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz1");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz1");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/no-match")
+  public void traceExcludedRequestUriPatternNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz3");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz3");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/xyz2")
+  public void traceRequestUriPatternAndExcludedRequestUriPattern() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz2");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz2");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.requestUriPattern", value = "/projects/.*")
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "/projects/no-match")
+  public void traceRequestUriPatternAndExcludedRequestUriPatternNoMatch() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz3");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isEqualTo("issue123");
+      assertThat(projectCreationListener.isLoggingForced).isTrue();
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz3");
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "tracing.issue123.excludedRequestUriPattern", value = "][")
+  public void traceExcludedRequestUriInvalidRegEx() throws Exception {
+    TraceValidatingProjectCreationValidationListener projectCreationListener =
+        new TraceValidatingProjectCreationValidationListener();
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(projectCreationListener)) {
+      RestResponse response = adminRestSession.put("/projects/xyz4");
+      assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+      assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+      assertThat(projectCreationListener.traceId).isNull();
+      assertThat(projectCreationListener.isLoggingForced).isFalse();
+
+      // The logging tag with the project name is also set if tracing is off.
+      assertThat(projectCreationListener.tags.get("project")).containsExactly("xyz4");
+    }
+  }
+
+  @Test
   @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
   public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
     String changeId = createChange().getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 0b0f2ec..79e8ab0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
@@ -39,8 +38,7 @@
     if (testAccount.tags().isEmpty()) {
       assertThat(accountInfo.tags).isNull();
     } else {
-      assertThat(accountInfo.tags.stream().map(Enum::name).collect(toImmutableList()))
-          .containsExactlyElementsIn(testAccount.tags());
+      assertThat(accountInfo.tags).containsExactlyElementsIn(testAccount.tags());
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index 8feac20..d055875 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.server.account.Emails;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
@@ -63,6 +65,8 @@
   @Inject private ExternalIds externalIds;
   @Inject private Provider<Emails> emails;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExternalIdFactory externalIdFactory;
+  @Inject private ExternalIdKeyFactory externalIdKeyFactory;
 
   @Test
   public void addEmail() throws Exception {
@@ -138,7 +142,7 @@
             admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(
+                    externalIdFactory.createWithEmail(
                         ExternalId.SCHEME_EXTERNAL, "foo", admin.id(), email)));
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
@@ -182,7 +186,7 @@
   public void setPreferredEmailToEmailFromCustomRealmThatDoesntExistAsExternalId()
       throws Exception {
     String email = "foo@example.com";
-    ExternalId.Key mailtoExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_MAILTO, email);
+    ExternalId.Key mailtoExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_MAILTO, email);
     assertThat(externalIds.get(mailtoExtIdKey)).isEmpty();
     assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
 
@@ -200,7 +204,7 @@
 
   @Test
   public void setPreferredEmailToEmailFromCustomRealmThatBelongsToOtherAccount() throws Exception {
-    ExternalId mailToExtId = ExternalId.createEmail(user.id(), user.email());
+    ExternalId mailToExtId = externalIdFactory.createEmail(user.id(), user.email());
     assertThat(externalIds.get(mailToExtId.key())).isPresent();
 
     Context oldCtx =
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 20b378b..cd123aa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -21,6 +21,8 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
@@ -37,6 +39,8 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -53,7 +57,10 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIdReader;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -92,6 +99,8 @@
   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExternalIdKeyFactory externalIdKeyFactory;
+  @Inject private ExternalIdFactory externalIdFactory;
 
   @ConfigSuite.Default
   public static Config partialCacheReloadingEnabled() {
@@ -194,7 +203,7 @@
   }
 
   @Test
-  public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
+  public void deleteExternalIdOfOtherUserUnderOwnAccount_unprocessableEntity() throws Exception {
     List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
     requestScopeOperations.setApiUser(user.id());
     UnprocessableEntityException thrown =
@@ -249,19 +258,60 @@
     gApi.accounts()
         .self()
         .deleteExternalIds(
-            ImmutableList.of(ExternalId.Key.create(SCHEME_MAILTO, preferredEmail).get()));
+            ImmutableList.of(externalIdKeyFactory.create(SCHEME_MAILTO, preferredEmail).get()));
     assertThat(gApi.accounts().self().get().email).isNull();
   }
 
   @Test
-  public void deleteExternalIds_Conflict() throws Exception {
+  public void deleteExternalIdOfUsernameByNonAdminForbidden() throws Exception {
     List<String> toDelete = new ArrayList<>();
     String externalIdStr = "username:" + user.username();
     toDelete.add(externalIdStr);
-    RestResponse response = userRestSession.post("/accounts/self/external.ids:delete", toDelete);
-    response.assertConflict();
-    assertThat(response.getEntityContent())
-        .isEqualTo(String.format("External id %s cannot be deleted", externalIdStr));
+    RestResponse response =
+        userRestSession.post("/accounts/" + admin.id() + "/external.ids:delete", toDelete);
+    response.assertForbidden();
+  }
+
+  @Test
+  public void deleteExternalIdOfUsernameSelfForbidden() throws Exception {
+    List<String> toDelete = new ArrayList<>();
+    String externalIdStr = "username:" + admin.username();
+    toDelete.add(externalIdStr);
+    RestResponse response = adminRestSession.post("/accounts/self/external.ids:delete", toDelete);
+    response.assertForbidden();
+  }
+
+  @Test
+  public void deleteExternalIdOfUsernameByAdmin() throws Exception {
+    List<String> toDelete = new ArrayList<>();
+    String externalIdStr = "username:" + user.username();
+    toDelete.add(externalIdStr);
+    RestResponse response =
+        adminRestSession.post("/accounts/" + user.id() + "/external.ids:delete", toDelete);
+    response.assertNoContent();
+    List<AccountExternalIdInfo> results = gApi.accounts().id(user.id().get()).getExternalIds();
+    assertThat(results).hasSize(1);
+    assertThat(results.get(0).identity).isEqualTo("mailto:user1@example.com");
+  }
+
+  @Test
+  public void deleteExternalIdOfUsernameMaintainServer() throws Exception {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.MAINTAIN_SERVER).group(REGISTERED_USERS))
+        .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
+        .update();
+
+    List<String> toDelete = new ArrayList<>();
+    TestAccount user2 = accountCreator.user2();
+    String externalIdStr = "username:" + user2.username();
+    toDelete.add(externalIdStr);
+    RestResponse response =
+        userRestSession.post("/accounts/" + user2.id() + "/external.ids:delete", toDelete);
+    response.assertNoContent();
+    List<AccountExternalIdInfo> results = gApi.accounts().id(user2.id().get()).getExternalIds();
+    assertThat(results).hasSize(1);
+    assertThat(results.get(0).identity).isEqualTo("mailto:user2@example.com");
   }
 
   @Test
@@ -525,12 +575,12 @@
 
     // create valid external IDs
     insertExtId(
-        ExternalId.createWithPassword(
-            ExternalId.Key.parse(nextId(scheme, i)),
+        externalIdFactory.createWithPassword(
+            externalIdKeyFactory.parse(nextId(scheme, i)),
             admin.id(),
             "admin.other@example.com",
             "secret-password"));
-    insertExtId(ExternalId.createEmail(admin.id(), "admin.other@example.com"));
+    insertExtId(externalIdFactory.createEmail(admin.id(), "admin.other@example.com"));
     insertExtId(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
   }
 
@@ -630,29 +680,30 @@
   }
 
   private ExternalId createExternalIdWithOtherCaseEmail(String externalId) {
-    return ExternalId.createWithPassword(
-        ExternalId.Key.parse(externalId),
+    return externalIdFactory.createWithPassword(
+        externalIdKeyFactory.parse(externalId),
         admin.id(),
         admin.email().toUpperCase(Locale.US),
         "password");
   }
 
   private ExternalId createExternalIdForNonExistingAccount(String externalId) {
-    return ExternalId.create(ExternalId.Key.parse(externalId), Account.id(1));
+    return externalIdFactory.create(externalIdKeyFactory.parse(externalId), Account.id(1));
   }
 
   private ExternalId createExternalIdWithInvalidEmail(String externalId) {
-    return ExternalId.createWithEmail(
-        ExternalId.Key.parse(externalId), admin.id(), "invalid-email");
+    return externalIdFactory.createWithEmail(
+        externalIdKeyFactory.parse(externalId), admin.id(), "invalid-email");
   }
 
   private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
-    return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), user.id(), admin.email());
+    return externalIdFactory.createWithEmail(
+        externalIdKeyFactory.parse(externalId), user.id(), admin.email());
   }
 
   private ExternalId createExternalIdWithBadPassword(String username) {
-    return ExternalId.create(
-        ExternalId.Key.create(SCHEME_USERNAME, username),
+    return externalIdFactory.create(
+        externalIdKeyFactory.create(SCHEME_USERNAME, username),
         admin.id(),
         null,
         "non-hashed-password-is-not-allowed");
@@ -664,14 +715,14 @@
 
   @Test
   public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception {
-    ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar");
+    ExternalId.Key extIdKey = externalIdKeyFactory.parse("foo:bar");
     Account.Id accountId = Account.id(1024 * 100);
     accountsUpdateProvider
         .get()
         .insert(
             "Create Account with Bad External ID",
             accountId,
-            u -> u.addExternalId(ExternalId.create(extIdKey, accountId)));
+            u -> u.addExternalId(externalIdFactory.create(extIdKey, accountId)));
     Optional<ExternalId> extId = externalIds.get(extIdKey);
     assertThat(extId.map(ExternalId::accountId)).hasValue(accountId);
   }
@@ -681,7 +732,7 @@
     Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id()));
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // insert external ID
-      ExternalId extId = ExternalId.create("foo", "bar", admin.id());
+      ExternalId extId = externalIdFactory.create("foo", "bar", admin.id());
       insertExtId(extId);
       expectedExtIds.add(extId);
       assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExtIds);
@@ -689,7 +740,7 @@
       // update external ID
       expectedExtIds.remove(extId);
       ExternalId extId2 =
-          ExternalId.createWithEmail("foo", "bar", admin.id(), "foo.bar@example.com");
+          externalIdFactory.createWithEmail("foo", "bar", admin.id(), "foo.bar@example.com");
       accountsUpdateProvider
           .get()
           .update("Update External ID", admin.id(), u -> u.updateExternalId(extId2));
@@ -711,7 +762,7 @@
 
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
-      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
+      insertExtIdBehindGerritsBack(externalIdFactory.create("foo", "bar", admin.id()));
 
       assertThrows(IOException.class, () -> externalIds.byAccount(admin.id()));
     }
@@ -723,7 +774,7 @@
 
     try (AutoCloseable ctx = createFailOnLoadContext()) {
       // update external ID branch so that external IDs need to be reloaded
-      insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id()));
+      insertExtIdBehindGerritsBack(externalIdFactory.create("foo", "bar", admin.id()));
 
       assertThrows(IOException.class, () -> externalIds.byEmail(admin.email()));
     }
@@ -732,7 +783,7 @@
   @Test
   public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception {
     Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id()));
-    ExternalId newExtId = ExternalId.create("foo", "bar", admin.id());
+    ExternalId newExtId = externalIdFactory.create("foo", "bar", admin.id());
     insertExtIdBehindGerritsBack(newExtId);
     expectedExternalIds.add(newExtId);
     assertThat(externalIds.byAccount(admin.id())).containsExactlyElementsIn(expectedExternalIds);
@@ -740,10 +791,10 @@
 
   @Test
   public void unsetEmail() throws Exception {
-    ExternalId extId = ExternalId.createWithEmail("x", "1", user.id(), "x@example.com");
+    ExternalId extId = externalIdFactory.createWithEmail("x", "1", user.id(), "x@example.com");
     insertExtId(extId);
 
-    ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id());
+    ExternalId extIdWithoutEmail = externalIdFactory.create("x", "1", user.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -757,10 +808,11 @@
   @Test
   public void unsetHttpPassword() throws Exception {
     ExternalId extId =
-        ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id(), null, "secret");
+        externalIdFactory.createWithPassword(
+            externalIdKeyFactory.create("y", "1"), user.id(), null, "secret");
     insertExtId(extId);
 
-    ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id());
+    ExternalId extIdWithoutPassword = externalIdFactory.create("y", "1", user.id());
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
@@ -771,6 +823,75 @@
     }
   }
 
+  @Test
+  @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+  public void createCaseInsensitiveExternalId_DuplicateKey() throws Exception {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      testCaseInsensitiveExternalIdKey(md, extIdNotes, SCHEME_USERNAME, "JohnDoe", Account.id(42));
+      assertThrows(
+          DuplicateExternalIdKeyException.class,
+          () ->
+              extIdNotes.insert(
+                  externalIdFactory.create(SCHEME_USERNAME, "johndoe", Account.id(23))));
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+  public void createCaseInsensitiveExternalId_SchemeWithUsername() throws Exception {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+      testCaseInsensitiveExternalIdKey(md, extIdNotes, SCHEME_USERNAME, "janedoe", Account.id(66));
+      testCaseInsensitiveExternalIdKey(md, extIdNotes, SCHEME_GERRIT, "JaneDoe", Account.id(66));
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+  public void createCaseSensitiveExternalId_SchemeWithoutUsername() throws Exception {
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+      testCaseSensitiveExternalIdKey(md, extIdNotes, SCHEME_MAILTO, "Jane@doe.com", Account.id(66));
+      testCaseSensitiveExternalIdKey(md, extIdNotes, SCHEME_UUID, "1234ABCD", Account.id(66));
+      testCaseSensitiveExternalIdKey(md, extIdNotes, SCHEME_GPGKEY, "1234ABCD", Account.id(66));
+    }
+  }
+
+  private void testCaseSensitiveExternalIdKey(
+      MetaDataUpdate md, ExternalIdNotes extIdNotes, String scheme, String id, Account.Id accountId)
+      throws DuplicateExternalIdKeyException, IOException, ConfigInvalidException {
+    ExternalId extId = externalIdFactory.create(scheme, id, accountId);
+    extIdNotes.insert(extId);
+    extIdNotes.commit(md);
+    assertThat(extIdNotes.get(externalIdKeyFactory.create(scheme, id)).get().accountId().get())
+        .isEqualTo(accountId.get());
+    assertThat(extIdNotes.get(externalIdKeyFactory.create(scheme, id.toLowerCase())).isPresent())
+        .isFalse();
+  }
+
+  private void testCaseInsensitiveExternalIdKey(
+      MetaDataUpdate md, ExternalIdNotes extIdNotes, String scheme, String id, Account.Id accountId)
+      throws DuplicateExternalIdKeyException, IOException, ConfigInvalidException {
+    ExternalId extId = externalIdFactory.create(scheme, id, accountId);
+    extIdNotes.insert(extId);
+    extIdNotes.commit(md);
+    assertThat(extIdNotes.get(externalIdKeyFactory.create(scheme, id)).get().accountId().get())
+        .isEqualTo(accountId.get());
+    assertThat(
+            extIdNotes
+                .get(externalIdKeyFactory.create(scheme, id.toLowerCase()))
+                .get()
+                .accountId()
+                .get())
+        .isEqualTo(accountId.get());
+  }
+
   private boolean isPartialCacheReloadingEnabled() {
     return cfg.getBoolean("cache", "external_ids_map", "enablePartialReloads", true);
   }
@@ -796,7 +917,8 @@
   private void insertExtIdBehindGerritsBack(ExternalId extId) throws Exception {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       // Inserting an external ID "behind Gerrit's back" means that the caches are not updated.
-      ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsers, repo);
+      ExternalIdNotes extIdNotes =
+          ExternalIdNotes.loadNoCacheUpdate(allUsers, repo, externalIdFactory);
       extIdNotes.insert(extId);
       try (MetaDataUpdate metaDataUpdate =
           new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index 84f95f7..a1e9bf1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -17,22 +17,42 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.common.AccountDetailInfo;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.account.AccountTagProvider;
 import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.inject.Inject;
+import com.google.inject.Module;
+import java.util.List;
 import org.junit.Test;
 
 public class GetAccountDetailIT extends AbstractDaemonTest {
   @Inject private GroupOperations groupOperations;
   @Inject private AccountOperations accountOperations;
 
+  @Override
+  public Module createModule() {
+    return new FactoryModule() {
+      @Override
+      public void configure() {
+        bind(AccountTagProvider.class)
+            .annotatedWith(Exports.named("CustomAccountTagProvider"))
+            .to(CustomAccountTagProvider.class);
+      }
+    };
+  }
+
   @Test
   public void getDetail() throws Exception {
     RestResponse r = adminRestSession.get("/accounts/" + admin.username() + "/detail/");
@@ -56,7 +76,14 @@
         .update();
     RestResponse r = adminRestSession.get("/accounts/" + serviceUser.get() + "/detail/");
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
-    assertThat(info.tags).containsExactly(AccountInfo.Tag.SERVICE_USER);
+    assertThat(info.tags).containsExactly(AccountInfo.Tags.SERVICE_USER);
+  }
+
+  @Test
+  public void getDetailForExtensionPointAccountTag() throws Exception {
+    RestResponse r = userRestSession.get("/accounts/" + user.username() + "/detail/");
+    AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
+    assertThat(info.tags).containsExactly("BASIC_USER");
   }
 
   @Test
@@ -75,4 +102,25 @@
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertThat(info._accountId).isEqualTo(id.get());
   }
+
+  private static class CustomAccountTagProvider implements AccountTagProvider {
+    private PermissionBackend permissions;
+
+    @Inject
+    public CustomAccountTagProvider(PermissionBackend permissions) {
+      this.permissions = permissions;
+    }
+
+    @Override
+    public List<String> getTags(Account.Id id) {
+      try {
+        if (!permissions.currentUser().test(GlobalPermission.ADMINISTRATE_SERVER)) {
+          return ImmutableList.of("BASIC_USER");
+        }
+      } catch (Exception e) {
+        throw new IllegalStateException("can't check admin permissions", e);
+      }
+      return ImmutableList.of();
+    }
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index e05d0db..f46cf0c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.extensions.api.accounts.UsernameInput;
 import org.junit.Test;
 
@@ -42,6 +43,16 @@
   }
 
   @Test
+  @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+  public void setExistingCaseInsensitive_Conflict() throws Exception {
+    UsernameInput in = new UsernameInput();
+    in.username = admin.username().toUpperCase();
+    adminRestSession
+        .put("/accounts/" + accountCreator.create().id().get() + "/username", in)
+        .assertConflict();
+  }
+
+  @Test
   public void setNew_MethodNotAllowed() throws Exception {
     UsernameInput in = new UsernameInput();
     in.username = "newUsername";
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
index eb125a0..13353bd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -29,7 +29,7 @@
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountsUpdate;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import org.junit.Test;
@@ -43,6 +43,7 @@
 public class AccountsRestApiBindingsIT extends AbstractDaemonTest {
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
   @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ExternalIdFactory externalIdFactory;
 
   /**
    * Account REST endpoints to be tested, each URL contains a placeholder for the account
@@ -86,7 +87,6 @@
           RestCall.get("/accounts/%s/preferences.edit"),
           RestCall.put("/accounts/%s/preferences.edit"),
           RestCall.get("/accounts/%s/starred.changes"),
-          RestCall.get("/accounts/%s/stars.changes"),
           RestCall.post("/accounts/%s/index"),
           RestCall.get("/accounts/%s/agreements"),
           RestCall.put("/accounts/%s/agreements"),
@@ -141,9 +141,7 @@
   private static final ImmutableList<RestCall> STAR_ENDPOINTS =
       ImmutableList.of(
           RestCall.put("/accounts/%s/starred.changes/%s"),
-          RestCall.delete("/accounts/%s/starred.changes/%s"),
-          RestCall.get("/accounts/%s/stars.changes/%s"),
-          RestCall.post("/accounts/%s/stars.changes/%s"));
+          RestCall.delete("/accounts/%s/starred.changes/%s"));
 
   @Test
   public void accountEndpoints() throws Exception {
@@ -169,7 +167,7 @@
             admin.id(),
             u ->
                 u.addExternalId(
-                    ExternalId.createWithEmail(name("test"), email, admin.id(), email)));
+                    externalIdFactory.createWithEmail(name("test"), email, admin.id(), email)));
 
     requestScopeOperations.setApiUser(admin.id());
     gApi.accounts()
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 68b24ce..79484ca 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -76,8 +76,6 @@
           RestCall.post("/changes/%s/ready"),
           RestCall.put("/changes/%s/ignore"),
           RestCall.put("/changes/%s/unignore"),
-          RestCall.put("/changes/%s/reviewed"),
-          RestCall.put("/changes/%s/unreviewed"),
           RestCall.get("/changes/%s/messages"),
           RestCall.put("/changes/%s/message"),
           RestCall.post("/changes/%s/merge"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index bcca8bb..0a11b15 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -64,7 +64,6 @@
 import com.google.gerrit.server.restapi.change.GetAttentionSet;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import com.google.inject.Inject;
@@ -89,7 +88,6 @@
   @Inject private AccountOperations accountOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
 
-  @Inject private FakeEmailSender email;
   @Inject private TestCommentHelper testCommentHelper;
   @Inject private Provider<InternalChangeQuery> changeQueryProvider;
   @Inject private ProjectOperations projectOperations;
@@ -145,7 +143,7 @@
     assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
 
     // Only one email since the second add was ignored.
-    String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
+    String emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
     assertThat(emailBody)
         .contains(
             String.format(
@@ -218,7 +216,6 @@
         ReviewInput.create().addUserToAttentionSet(user.email(), manualReason);
 
     change(r).current().review(reviewInput);
-
     AttentionSetInfo attentionSetInfo = change(r).get().attentionSet.get(user.id().get());
     assertThat(attentionSetInfo.reason).isEqualTo(manualReason);
     assertThat(attentionSetInfo.reasonAccount).isEqualTo(getAccountInfo(user.id()));
@@ -270,7 +267,7 @@
     assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
 
     // Only one email since the second remove was ignored.
-    String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
+    String emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
     assertThat(emailBody)
         .contains(
             user.fullName()
@@ -706,7 +703,7 @@
     assertThat(attentionSet).hasReasonThat().isEqualTo("reason");
 
     // No emails for adding to attention set were sent.
-    assertThat(email.getMessages()).isEmpty();
+    assertThat(sender.getMessages()).isEmpty();
   }
 
   @Test
@@ -715,7 +712,7 @@
     // implictly adds the user to the attention set when adding as reviewer
     change(r).addReviewer(user.email());
     requestScopeOperations.setApiUser(user.id());
-    email.clear();
+    sender.clear();
 
     ReviewInput reviewInput =
         ReviewInput.create().removeUserFromAttentionSet(user.email(), "reason");
@@ -728,7 +725,7 @@
     assertThat(attentionSet).hasReasonThat().isEqualTo("reason");
 
     // No emails for removing from attention set were sent.
-    assertThat(email.getMessages()).isEmpty();
+    assertThat(sender.getMessages()).isEmpty();
   }
 
   @Test
@@ -1537,6 +1534,45 @@
   }
 
   @Test
+  public void addToAttentionSetEmail_withTemplateReason() throws Exception {
+    PushOneCommit.Result r = createChange();
+    requestScopeOperations.setApiUser(user.id());
+    String templateReason = "Added by " + AccountTemplateUtil.getAccountTemplate(user.id());
+    int accountId =
+        change(r)
+            .addToAttentionSet(new AttentionSetInput(admin.email(), templateReason))
+            ._accountId;
+
+    assertThat(accountId).isEqualTo(admin.id().get());
+    String emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
+    assertThat(emailBody)
+        .contains(
+            String.format(
+                "%s requires the attention of %s to this change.\n The reason is: Added by %s.",
+                user.fullName(), admin.fullName(), user.getNameEmail()));
+  }
+
+  @Test
+  public void removeFromAttentionSetEmail_withTemplateReason() throws Exception {
+    PushOneCommit.Result r = createChange();
+    // implicitly adds the user to the attention set when adding as reviewer
+    change(r).addReviewer(user.email());
+    sender.clear();
+    requestScopeOperations.setApiUser(user.id());
+
+    String templateReason = "Removed by " + AccountTemplateUtil.getAccountTemplate(user.id());
+    change(r).attention(user.id().toString()).remove(new AttentionSetInput(templateReason));
+
+    String emailBody = Iterables.getOnlyElement(sender.getMessages()).body();
+    assertThat(emailBody)
+        .contains(
+            String.format(
+                "%s removed themselves from the attention set of this change.\n"
+                    + " The reason is: Removed by %s.",
+                user.fullName(), user.getNameEmail()));
+  }
+
+  @Test
   public void attentionSetEmailFooter() throws Exception {
     PushOneCommit.Result r = createChange();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
index 61e5a2e..ae872b2b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -27,6 +27,7 @@
 import static com.google.common.net.HttpHeaders.VARY;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -41,6 +42,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.testing.ConfigSuite;
+import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.util.Locale;
 import java.util.stream.Stream;
@@ -206,6 +208,10 @@
 
   @Test
   public void crossDomainPutTopic() throws Exception {
+    // Setting cookies with HttpOnly requires Servlet API 3+ which not all deployments might have
+    // available.
+    assume().that(cookieHasSetHttpOnlyMethod()).isTrue();
+
     Result change = createChange();
     BasicCookieStore cookies = new BasicCookieStore();
     Executor http = Executor.newInstance().use(cookies);
@@ -327,4 +333,14 @@
       assertWithMessage(ACCESS_CONTROL_ALLOW_HEADERS).that(allowHeaders).isNull();
     }
   }
+
+  private static boolean cookieHasSetHttpOnlyMethod() {
+    Method setHttpOnly = null;
+    try {
+      setHttpOnly = Cookie.class.getMethod("setHttpOnly", boolean.class);
+    } catch (NoSuchMethodException | SecurityException e) {
+      return false;
+    }
+    return setHttpOnly != null;
+  }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index ad06226..b0a14cf 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -54,6 +54,7 @@
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -420,6 +421,69 @@
   }
 
   @Test
+  public void createAuthorAddedAsCcAndNotified() throws Exception {
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.author = new AccountInput();
+    input.author.email = user.email();
+    input.author.name = user.fullName();
+
+    ChangeInfo info = assertCreateSucceeds(input);
+    assertThat(info.reviewers.get(ReviewerState.CC)).hasSize(1);
+    assertThat(Iterables.getOnlyElement(info.reviewers.get(ReviewerState.CC)).email)
+        .isEqualTo(user.email());
+    assertThat(
+            Iterables.getOnlyElement(Iterables.getOnlyElement(sender.getMessages()).rcpt()).email())
+        .isEqualTo(user.email());
+  }
+
+  @Test
+  public void createAuthorAddedAsCcNotNotifiedWithNotifyNone() throws Exception {
+    ChangeInput input = newChangeInput(ChangeStatus.NEW);
+    input.author = new AccountInput();
+    input.author.email = user.email();
+    input.author.name = user.fullName();
+    input.notify = NotifyHandling.NONE;
+
+    ChangeInfo info = assertCreateSucceeds(input);
+    assertThat(info.reviewers.get(ReviewerState.CC)).hasSize(1);
+    assertThat(Iterables.getOnlyElement(info.reviewers.get(ReviewerState.CC)).email)
+        .isEqualTo(user.email());
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void createWithMergeConflictAuthorAddedAsCcNotNotifiedWithNotifyNone() throws Exception {
+    String fileName = "shared.txt";
+    String sourceBranch = "sourceBranch";
+    String sourceSubject = "source change";
+    String sourceContent = "source content";
+    String targetBranch = "targetBranch";
+    String targetSubject = "target change";
+    String targetContent = "target content";
+    changeInTwoBranches(
+        sourceBranch,
+        sourceSubject,
+        fileName,
+        sourceContent,
+        targetBranch,
+        targetSubject,
+        fileName,
+        targetContent);
+    ChangeInput input = newMergeChangeInput(targetBranch, sourceBranch, "", true);
+    input.workInProgress = true;
+    input.author = new AccountInput();
+    input.author.email = user.email();
+    input.author.name = user.fullName();
+    input.notify = NotifyHandling.NONE;
+    ChangeInfo info = assertCreateSucceeds(input);
+
+    assertThat(info.reviewers.get(ReviewerState.CC)).hasSize(1);
+    assertThat(Iterables.getOnlyElement(info.reviewers.get(ReviewerState.CC)).email)
+        .isEqualTo(user.email());
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
   public void createNewWorkInProgressChange() throws Exception {
     ChangeInput input = newChangeInput(ChangeStatus.NEW);
     input.workInProgress = true;
@@ -1117,7 +1181,6 @@
    * @param branchB name of second branch to create
    * @param fileB name of file to commit to branchB
    * @return A {@code Map} of branchName => commit result.
-   * @throws Exception
    */
   private Map<String, Result> changeInTwoBranches(
       String branchA, String fileA, String branchB, String fileB) throws Exception {
@@ -1137,7 +1200,6 @@
    * @param fileB name of file to commit to branchB
    * @param contentB file content to commit to branchB
    * @return A {@code Map} of branchName => commit result.
-   * @throws Exception
    */
   private Map<String, Result> changeInTwoBranches(
       String branchA,
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index daeb032..ef5e7dc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -52,14 +52,14 @@
   }
 
   @Test
-  public void flushAll_Forbidden() throws Exception {
+  public void flushAll_forbidden() throws Exception {
     userRestSession
         .post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL))
         .assertForbidden();
   }
 
   @Test
-  public void flushAll_BadRequest() throws Exception {
+  public void flushAll_badRequest() throws Exception {
     adminRestSession
         .post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")))
         .assertBadRequest();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 02db412..5f60250 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -102,12 +102,12 @@
   }
 
   @Test
-  public void createProjectHttpWhenProjectAlreadyExists_Conflict() throws Exception {
+  public void createProjectHttpWhenProjectAlreadyExists_conflict() throws Exception {
     adminRestSession.put("/projects/" + allProjects.get()).assertConflict();
   }
 
   @Test
-  public void createProjectHttpWhenProjectAlreadyExists_PreconditionFailed() throws Exception {
+  public void createProjectHttpWhenProjectAlreadyExists_preconditionFailed() throws Exception {
     adminRestSession
         .putWithHeaders(
             "/projects/" + allProjects.get(), new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"))
@@ -140,7 +140,7 @@
 
   @Test
   @UseLocalDisk
-  public void createProjectHttpWithUnreasonableName_BadRequest() throws Exception {
+  public void createProjectHttpWithUnreasonableName_badRequest() throws Exception {
     ImmutableList<String> forbiddenStrings =
         ImmutableList.of(
             "/../", "/./", "//", ".git/", "?", "%", "*", ":", "<", ">", "|", "$", "/+", "~");
@@ -153,14 +153,14 @@
   }
 
   @Test
-  public void createProjectHttpWithNameMismatch_BadRequest() throws Exception {
+  public void createProjectHttpWithNameMismatch_badRequest() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("otherName");
     adminRestSession.put("/projects/" + name("someName"), in).assertBadRequest();
   }
 
   @Test
-  public void createProjectHttpWithInvalidRefName_BadRequest() throws Exception {
+  public void createProjectHttpWithInvalidRefName_badRequest() throws Exception {
     ProjectInput in = new ProjectInput();
     in.branches = Collections.singletonList(name("invalid ref name"));
     adminRestSession.put("/projects/" + name("newProject"), in).assertBadRequest();
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
index f98fb45..55735fc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
@@ -29,13 +29,13 @@
 /** Helper to execute REST API calls using the HTTP client. */
 @Ignore
 public class RestApiCallHelper {
-  /** @see #execute(RestSession, List, BeforeRestCall, String...) */
+  /** See {@link #execute(RestSession, List, BeforeRestCall, String...)} */
   public static void execute(RestSession restSession, List<RestCall> restCalls, String... args)
       throws Exception {
     execute(restSession, restCalls, () -> {}, args);
   }
 
-  /** @see #execute(RestSession, List, BeforeRestCall, String...) */
+  /** See {@link #execute(RestSession, RestCall, String...)} */
   public static void execute(
       RestSession restSession,
       List<RestCall> restCalls,
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
index c7beb2d..0cfa0f8 100644
--- a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -160,6 +161,22 @@
   }
 
   @Test
+  @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+  public void byUsernameCaseInsensitive() throws Exception {
+    String existingUsername = "myusername";
+    Account.Id idWithUsername = accountOperations.newAccount().username(existingUsername).create();
+
+    String existingMixedCaseUsername = "MyMixedCaseUsername";
+    Account.Id idWithMixedCaseUsername =
+        accountOperations.newAccount().username(existingMixedCaseUsername).create();
+
+    assertThat(resolve(existingUsername)).containsExactly(idWithUsername);
+    assertThat(resolve(existingMixedCaseUsername)).containsExactly(idWithMixedCaseUsername);
+    assertThat(resolve(existingMixedCaseUsername.toLowerCase()))
+        .containsExactly(idWithMixedCaseUsername);
+  }
+
+  @Test
   public void byNameAndEmail() throws Exception {
     String email = name("user@example.com");
     Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
index 81cb7159..a2765d9 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
@@ -425,7 +425,7 @@
   }
 
   @Test
-  public void commentContextReturnsCorrectContentType_Java() throws Exception {
+  public void commentContextReturnsCorrectContentType_java() throws Exception {
     String javaContent =
         "public class Main {\n"
             + " public static void main(String[]args){\n"
@@ -448,7 +448,7 @@
   }
 
   @Test
-  public void commentContextReturnsCorrectContentType_Cpp() throws Exception {
+  public void commentContextReturnsCorrectContentType_cpp() throws Exception {
     String cppContent =
         "#include <iostream>\n"
             + "\n"
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 89074b7..80cdad8 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -224,12 +224,14 @@
     String ps1 = result.getCommit().name();
 
     CommentInput comment =
-        CommentsUtil.newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+        CommentsUtil.newCommentWithOnlyMandatoryFields(
+            PATCHSET_LEVEL, "The change looks good, LGTM");
     CommentsUtil.addComments(gApi, changeId, ps1, comment);
 
     String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
     assertThat(emailBody).contains("Patchset");
     assertThat(emailBody).doesNotContain("/PATCHSET_LEVEL");
+    assertThat(emailBody).contains("The change looks good, LGTM");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
deleted file mode 100644
index b23f9a3..0000000
--- a/javatests/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ /dev/null
@@ -1,326 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.server.change;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.getChangeId;
-import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.cache.Cache;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.NoHttpd;
-import com.google.gerrit.entities.Patch;
-import com.google.gerrit.entities.Patch.ChangeType;
-import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
-import com.google.gerrit.server.patch.IntraLineDiff;
-import com.google.gerrit.server.patch.IntraLineDiffArgs;
-import com.google.gerrit.server.patch.IntraLineDiffKey;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListCacheImpl;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.Text;
-import com.google.inject.Inject;
-import com.google.inject.name.Named;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.Test;
-
-@NoHttpd
-public class PatchListCacheIT extends AbstractDaemonTest {
-  private static String SUBJECT_1 = "subject 1";
-  private static String SUBJECT_2 = "subject 2";
-  private static String SUBJECT_3 = "subject 3";
-  private static String FILE_A = "a.txt";
-  private static String FILE_B = "b.txt";
-  private static String FILE_C = "c.txt";
-  private static String FILE_D = "d.txt";
-
-  @Inject private PatchListCache patchListCache;
-
-  @Inject
-  @Named("diff")
-  private Cache<PatchListKey, PatchList> abstractPatchListCache;
-
-  @Test
-  public void ensureLegacyBackendIsUsedForFileCacheBackend() throws Exception {
-    Field fileCacheField = patchListCache.getClass().getDeclaredField("fileCache");
-    fileCacheField.setAccessible(true);
-    // Use the reflection to access "localCache" field that is only present in Guava backend.
-    assertThat(
-            Arrays.stream(fileCacheField.get(patchListCache).getClass().getDeclaredFields())
-                .anyMatch(f -> f.getName().equals("localCache")))
-        .isTrue();
-
-    // intraCache (and all other cache backends) should use Caffeine backend.
-    Field intraCacheField = patchListCache.getClass().getDeclaredField("intraCache");
-    intraCacheField.setAccessible(true);
-    assertThat(
-            Arrays.stream(intraCacheField.get(patchListCache).getClass().getDeclaredFields())
-                .noneMatch(f -> f.getName().equals("localCache")))
-        .isTrue();
-  }
-
-  @Test
-  public void listPatchesAgainstBase() throws Exception {
-    commitBuilder().add(FILE_D, "4").message(SUBJECT_1).create();
-    pushHead(testRepo, "refs/heads/master", false);
-
-    // Change 1, 1 (+FILE_A, -FILE_D)
-    RevCommit c =
-        commitBuilder().add(FILE_A, "1").rm(FILE_D).message(SUBJECT_2).insertChangeId().create();
-    String id = getChangeId(testRepo, c).get();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Compare Change 1,1 with Base (+FILE_A, -FILE_D)
-    List<PatchListEntry> entries = getCurrentPatches(id);
-    assertThat(entries).hasSize(3);
-    assertAdded(Patch.COMMIT_MSG, entries.get(0));
-    assertAdded(FILE_A, entries.get(1));
-    assertDeleted(FILE_D, entries.get(2));
-
-    // Change 1,2 (+FILE_A, +FILE_B, -FILE_D)
-    amendBuilder().add(FILE_B, "2").create();
-    pushHead(testRepo, "refs/for/master", false);
-    entries = getCurrentPatches(id);
-
-    // Compare Change 1,2 with Base (+FILE_A, +FILE_B, -FILE_D)
-    assertThat(entries).hasSize(4);
-    assertAdded(Patch.COMMIT_MSG, entries.get(0));
-    assertAdded(FILE_A, entries.get(1));
-    assertAdded(FILE_B, entries.get(2));
-    assertDeleted(FILE_D, entries.get(3));
-  }
-
-  @Test
-  public void listPatchesAgainstBaseWithRebase() throws Exception {
-    commitBuilder().add(FILE_D, "4").message(SUBJECT_1).create();
-    pushHead(testRepo, "refs/heads/master", false);
-
-    // Change 1,1 (+FILE_A, -FILE_D)
-    RevCommit c = commitBuilder().add(FILE_A, "1").rm(FILE_D).message(SUBJECT_2).create();
-    String id = getChangeId(testRepo, c).get();
-    pushHead(testRepo, "refs/for/master", false);
-    List<PatchListEntry> entries = getCurrentPatches(id);
-    assertThat(entries).hasSize(3);
-    assertAdded(Patch.COMMIT_MSG, entries.get(0));
-    assertAdded(FILE_A, entries.get(1));
-    assertDeleted(FILE_D, entries.get(2));
-
-    // Change 2,1 (+FILE_B)
-    testRepo.reset("HEAD~1");
-    commitBuilder().add(FILE_B, "2").message(SUBJECT_3).create();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Change 1,2 (+FILE_A, -FILE_D))
-    testRepo.cherryPick(c);
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Compare Change 1,2 with Base (+FILE_A, -FILE_D))
-    entries = getCurrentPatches(id);
-    assertThat(entries).hasSize(3);
-    assertAdded(Patch.COMMIT_MSG, entries.get(0));
-    assertAdded(FILE_A, entries.get(1));
-    assertDeleted(FILE_D, entries.get(2));
-  }
-
-  @Test
-  public void listPatchesAgainstOtherPatchSet() throws Exception {
-    commitBuilder().add(FILE_D, "4").message(SUBJECT_1).create();
-    pushHead(testRepo, "refs/heads/master", false);
-
-    // Change 1,1 (+FILE_A, +FILE_C, -FILE_D)
-    RevCommit a =
-        commitBuilder().add(FILE_A, "1").add(FILE_C, "3").rm(FILE_D).message(SUBJECT_2).create();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Change 1,2 (+FILE_A, +FILE_B, -FILE_D)
-    RevCommit b = amendBuilder().add(FILE_B, "2").rm(FILE_C).create();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Compare Change 1,1 with Change 1,2 (+FILE_B, -FILE_C)
-    List<PatchListEntry> entries = getPatches(a, b);
-    assertThat(entries).hasSize(3);
-    assertModified(Patch.COMMIT_MSG, entries.get(0));
-    assertAdded(FILE_B, entries.get(1));
-    assertDeleted(FILE_C, entries.get(2));
-
-    // Compare Change 1,2 with Change 1,1 (-FILE_B, +FILE_C)
-    List<PatchListEntry> entriesReverse = getPatches(b, a);
-    assertThat(entriesReverse).hasSize(3);
-    assertModified(Patch.COMMIT_MSG, entriesReverse.get(0));
-    assertDeleted(FILE_B, entriesReverse.get(1));
-    assertAdded(FILE_C, entriesReverse.get(2));
-  }
-
-  @Test
-  public void listPatchesAgainstOtherPatchSetWithRebase() throws Exception {
-    commitBuilder().add(FILE_D, "4").message(SUBJECT_1).create();
-    pushHead(testRepo, "refs/heads/master", false);
-
-    // Change 1,1 (+FILE_A, -FILE_D)
-    RevCommit a = commitBuilder().add(FILE_A, "1").rm(FILE_D).message(SUBJECT_2).create();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Change 2,1 (+FILE_B)
-    testRepo.reset("HEAD~1");
-    commitBuilder().add(FILE_B, "2").message(SUBJECT_3).create();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Change 1,2 (+FILE_A, +FILE_C, -FILE_D)
-    testRepo.cherryPick(a);
-    RevCommit b = amendBuilder().add(FILE_C, "2").create();
-    pushHead(testRepo, "refs/for/master", false);
-
-    // Compare Change 1,1 with Change 1,2 (+FILE_C)
-    List<PatchListEntry> entries = getPatches(a, b);
-    assertThat(entries).hasSize(2);
-    assertModified(Patch.COMMIT_MSG, entries.get(0));
-    assertAdded(FILE_C, entries.get(1));
-
-    // Compare Change 1,2 with Change 1,1 (-FILE_C)
-    List<PatchListEntry> entriesReverse = getPatches(b, a);
-    assertThat(entriesReverse).hasSize(2);
-    assertModified(Patch.COMMIT_MSG, entriesReverse.get(0));
-    assertDeleted(FILE_C, entriesReverse.get(1));
-  }
-
-  @Test
-  public void harmfulMutationsOfEditsAreNotPossibleForPatchListEntry() throws Exception {
-    RevCommit commit =
-        commitBuilder().add("a.txt", "First line\nSecond line\n").message(SUBJECT_1).create();
-    pushHead(testRepo, "refs/heads/master", false);
-
-    PatchListKey diffKey = PatchListKey.againstDefaultBase(commit.copy(), Whitespace.IGNORE_NONE);
-    PatchList patchList = patchListCache.get(diffKey, project);
-
-    PatchListEntry patchListEntry = getEntryFor(patchList, "a.txt");
-    Edit outputEdit = Iterables.getOnlyElement(patchListEntry.getEdits());
-    Edit originalEdit =
-        new Edit(
-            outputEdit.getBeginA(),
-            outputEdit.getEndA(),
-            outputEdit.getBeginB(),
-            outputEdit.getEndB());
-
-    outputEdit.shift(5);
-
-    assertThat(patchListEntry.getEdits()).containsExactly(originalEdit);
-  }
-
-  @Test
-  public void harmfulMutationsOfEditsAreNotPossibleForIntraLineDiffArgsAndCachedValue() {
-    String a = "First line\nSecond line\n";
-    String b = "1st line\n2nd line\n";
-    Text aText = new Text(a.getBytes(UTF_8));
-    Text bText = new Text(b.getBytes(UTF_8));
-    Edit inputEdit = new Edit(0, 2, 0, 2);
-    List<Edit> inputEdits = new ArrayList<>(ImmutableList.of(inputEdit));
-    Set<Edit> inputEditsDueToRebase = new HashSet<>(ImmutableSet.of(inputEdit));
-
-    IntraLineDiffKey diffKey =
-        IntraLineDiffKey.create(ObjectId.zeroId(), ObjectId.zeroId(), Whitespace.IGNORE_NONE);
-    IntraLineDiffArgs diffArgs =
-        IntraLineDiffArgs.create(
-            aText,
-            bText,
-            inputEdits,
-            inputEditsDueToRebase,
-            project,
-            ObjectId.zeroId(),
-            "file.txt");
-    IntraLineDiff intraLineDiff = patchListCache.getIntraLineDiff(diffKey, diffArgs);
-
-    Edit outputEdit = Iterables.getOnlyElement(intraLineDiff.getEdits());
-
-    outputEdit.shift(5);
-    inputEdit.shift(7);
-    inputEdits.add(new Edit(43, 47, 50, 51));
-    inputEditsDueToRebase.add(new Edit(53, 57, 60, 61));
-
-    Edit originalEdit = new Edit(0, 2, 0, 2);
-    assertThat(diffArgs.edits()).containsExactly(originalEdit);
-    assertThat(diffArgs.editsDueToRebase()).containsExactly(originalEdit);
-    assertThat(intraLineDiff.getEdits()).containsExactly(originalEdit);
-  }
-
-  @Test
-  public void largeObjectTombstoneGetsCached() {
-    PatchListKey key = PatchListKey.againstDefaultBase(ObjectId.zeroId(), Whitespace.IGNORE_ALL);
-    PatchListCacheImpl.LargeObjectTombstone tombstone =
-        new PatchListCacheImpl.LargeObjectTombstone();
-    abstractPatchListCache.put(key, tombstone);
-    assertThat(abstractPatchListCache.getIfPresent(key)).isSameInstanceAs(tombstone);
-  }
-
-  private static void assertAdded(String expectedNewName, PatchListEntry e) {
-    assertName(expectedNewName, e);
-    assertThat(e.getChangeType()).isEqualTo(ChangeType.ADDED);
-  }
-
-  private static void assertModified(String expectedNewName, PatchListEntry e) {
-    assertName(expectedNewName, e);
-    assertThat(e.getChangeType()).isEqualTo(ChangeType.MODIFIED);
-  }
-
-  private static void assertDeleted(String expectedNewName, PatchListEntry e) {
-    assertName(expectedNewName, e);
-    assertThat(e.getChangeType()).isEqualTo(ChangeType.DELETED);
-  }
-
-  private static void assertName(String expectedNewName, PatchListEntry e) {
-    assertThat(e.getNewName()).isEqualTo(expectedNewName);
-    assertThat(e.getOldName()).isNull();
-  }
-
-  private List<PatchListEntry> getCurrentPatches(String changeId) throws Exception {
-    return patchListCache.get(getKey(null, getCurrentRevisionId(changeId)), project).getPatches();
-  }
-
-  private List<PatchListEntry> getPatches(ObjectId revisionIdA, ObjectId revisionIdB)
-      throws Exception {
-    return patchListCache.get(getKey(revisionIdA, revisionIdB), project).getPatches();
-  }
-
-  private PatchListKey getKey(ObjectId revisionIdA, ObjectId revisionIdB) {
-    return PatchListKey.againstCommit(revisionIdA, revisionIdB, Whitespace.IGNORE_NONE);
-  }
-
-  private ObjectId getCurrentRevisionId(String changeId) throws Exception {
-    return ObjectId.fromString(gApi.changes().id(changeId).get().currentRevision);
-  }
-
-  private static PatchListEntry getEntryFor(PatchList patchList, String filePath) {
-    Optional<PatchListEntry> patchListEntry =
-        patchList.getPatches().stream()
-            .filter(entry -> entry.getNewName().equals(filePath))
-            .findAny();
-    return patchListEntry.orElseThrow(
-        () -> new IllegalStateException("No PatchListEntry for " + filePath + " exists"));
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
index d767f48..cb1a679 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/AbstractMailIT.java
@@ -93,8 +93,9 @@
    * @param f1 Comment on file one.
    * @return A string with all inline comments and the original quoted email.
    */
-  static String newPlaintextBody(String changeURL, String changeMessage, String c1, String f1) {
-    return (changeMessage == null ? "" : changeMessage + "\n")
+  static String newPlaintextBody(
+      String changeURL, String patchsetLevelComment, String c1, String f1) {
+    return (patchsetLevelComment == null ? "" : patchsetLevelComment + "\n")
         + "> Foo Bar has posted comments on this change. (  \n"
         + "> "
         + changeURL
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 65cb97a..85238f8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -301,6 +301,32 @@
     addReviewerToReviewableChange(batch());
   }
 
+  private void addReviewerToIgnoredChange(Adder adder) throws Exception {
+    StagedChange sc = stageReviewableChange();
+    requestScopeOperations.setApiUser(sc.reviewer.id());
+    gApi.changes().id(sc.changeId).ignore(true);
+    TestAccount addedReviewer = accountCreator.create("added", "added@example.com", "added", null);
+    addReviewer(adder, sc.changeId, sc.owner, addedReviewer.email(), CC_ON_OWN_COMMENTS, null);
+
+    assertThat(sender)
+        .sent("newchange", sc)
+        .to(addedReviewer)
+        .cc(sc.owner)
+        .cc(StagedUsers.REVIEWER_BY_EMAIL, StagedUsers.CC_BY_EMAIL)
+        .noOneElse();
+    assertThat(sender).didNotSend();
+  }
+
+  @Test
+  public void addReviewerToIgnoredChangeSingly() throws Exception {
+    addReviewerToIgnoredChange(singly());
+  }
+
+  @Test
+  public void addReviewerToIgnoredChangeBatch() throws Exception {
+    addReviewerToIgnoredChange(batch());
+  }
+
   private void addReviewerToReviewableChangeByOwnerCcingSelf(Adder adder) throws Exception {
     StagedChange sc = stageReviewableChange();
     TestAccount reviewer = accountCreator.create("added", "added@example.com", "added", null);
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 85c0212..06d9349 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
@@ -97,7 +98,7 @@
   }
 
   @Test
-  public void parseAndPersistChangeMessage() throws Exception {
+  public void parseAndPersistPatchsetLevelComment() throws Exception {
     String changeId = createChangeWithReview();
     ChangeInfo changeInfo = gApi.changes().id(changeId).get();
     String ts =
@@ -114,8 +115,19 @@
 
     Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
     assertThat(messages).hasSize(3);
-    assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1:\n\nTest Message");
+    assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1:\n\n(1 comment)");
     assertThat(Iterables.getLast(messages).tag).isEqualTo("mailMessageId=some id");
+    // Assert comment
+    List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+    assertThat(comments).hasSize(3);
+    assertThat(comments.get(0).path).isEqualTo(PATCHSET_LEVEL);
+    assertThat(comments.get(0).message).isEqualTo("Test Message");
+    assertThat(comments.get(0).tag).isEqualTo("mailMessageId=some id");
+    assertThat(comments.get(0).parent).isNull();
+    assertThat(comments.get(0).side).isNull();
+    assertThat(comments.get(0).line).isNull();
+    assertThat(comments.get(0).parent).isNull();
+    assertThat(comments.get(0).inReplyTo).isNull();
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
index 64ad900..277c0e6 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/ExternalIdNotesUpsertPreprocessorIT.java
@@ -24,9 +24,9 @@
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.server.ServerInitiated;
-import com.google.gerrit.server.account.Accounts;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -50,7 +50,7 @@
   @Inject private Sequences sequences;
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
   @Inject private ExternalIdNotes.Factory extIdNotesFactory;
-  @Inject private Accounts accounts;
+  @Inject private ExternalIdFactory extIdFactory;
 
   public static class Module extends AbstractModule {
     @Override
@@ -72,7 +72,7 @@
   @Test
   public void insertAccount() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId = ExternalId.create("foo", "bar", id);
+    ExternalId extId = extIdFactory.create("foo", "bar", id);
     accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId));
     assertThat(testPreprocessor.upserted).containsExactly(extId);
   }
@@ -80,8 +80,8 @@
   @Test
   public void replaceByKeys() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId1 = ExternalId.create("foo", "bar1", id);
-    ExternalId extId2 = ExternalId.create("foo", "bar2", id);
+    ExternalId extId1 = extIdFactory.create("foo", "bar1", id);
+    ExternalId extId2 = extIdFactory.create("foo", "bar2", id);
     accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
 
     testPreprocessor.reset();
@@ -97,7 +97,7 @@
   @Test
   public void insert() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId = ExternalId.create("foo", "bar", id);
+    ExternalId extId = extIdFactory.create("foo", "bar", id);
 
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
@@ -111,7 +111,7 @@
   @Test
   public void upsert() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId = ExternalId.create("foo", "bar", id);
+    ExternalId extId = extIdFactory.create("foo", "bar", id);
 
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
@@ -125,8 +125,8 @@
   @Test
   public void replace() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId1 = ExternalId.create("foo", "bar1", id);
-    ExternalId extId2 = ExternalId.create("foo", "bar2", id);
+    ExternalId extId1 = extIdFactory.create("foo", "bar1", id);
+    ExternalId extId2 = extIdFactory.create("foo", "bar2", id);
     accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
 
     testPreprocessor.reset();
@@ -142,8 +142,8 @@
   @Test
   public void replace_viaAccountsUpdate() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId1 = ExternalId.create("foo", "bar", id, "email1@foo", "hash");
-    ExternalId extId2 = ExternalId.create("foo", "bar", id, "email2@foo", "hash");
+    ExternalId extId1 = extIdFactory.create("foo", "bar", id, "email1@foo", "hash");
+    ExternalId extId2 = extIdFactory.create("foo", "bar", id, "email2@foo", "hash");
     accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
 
     testPreprocessor.reset();
@@ -154,7 +154,7 @@
   @Test
   public void blockUpsert() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId = ExternalId.create("foo", "bar", id);
+    ExternalId extId = extIdFactory.create("foo", "bar", id);
     testPreprocessor.throwException = true;
     StorageException e =
         assertThrows(
@@ -167,8 +167,8 @@
   @Test
   public void blockUpsert_replace() throws Exception {
     Account.Id id = Account.id(sequences.nextAccountId());
-    ExternalId extId1 = ExternalId.create("foo", "bar", id, "email1@foo", "hash");
-    ExternalId extId2 = ExternalId.create("foo", "bar", id, "email2@foo", "hash");
+    ExternalId extId1 = extIdFactory.create("foo", "bar", id, "email1@foo", "hash");
+    ExternalId extId2 = extIdFactory.create("foo", "bar", id, "email2@foo", "hash");
     accountsUpdateProvider.get().insert("test", id, u -> u.addExternalId(extId1));
 
     assertThat(accounts.get(id).get().externalIds()).containsExactly(extId1);
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
index 9e4907c..46687e3 100644
--- a/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/ExternalUserPermissionIT.java
@@ -44,7 +44,7 @@
 import com.google.gerrit.server.PropertyMap;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -70,6 +70,7 @@
   @Inject private ChangeNotes.Factory changeNotesFactory;
   @Inject private ExternalUser.Factory externalUserFactory;
   @Inject private GroupOperations groupOperations;
+  @Inject private ExternalIdKeyFactory externalIdKeyFactory;
 
   @Before
   public void setUp() {
@@ -295,7 +296,7 @@
   ExternalUser createUserInGroup(String userId, String groupId) {
     return externalUserFactory.create(
         ImmutableSet.of(),
-        ImmutableSet.of(ExternalId.Key.parse("company-auth:" + groupId + "-" + userId)),
+        ImmutableSet.of(externalIdKeyFactory.parse("company-auth:" + groupId + "-" + userId)),
         PropertyMap.EMPTY);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 33276e7..ba86976 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -16,9 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
-import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
 
-import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -32,7 +30,6 @@
 import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
@@ -470,7 +467,7 @@
 
     // ignore the change
     requestScopeOperations.setApiUser(user.id());
-    gApi.accounts().self().setStars(r.getChangeId(), new StarsInput(ImmutableSet.of(IGNORE_LABEL)));
+    gApi.changes().id(r.getChangeId()).ignore(true);
 
     sender.clear();
 
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 9e85558..6e19c39 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -116,7 +116,7 @@
             /* submittabilityExpr= */ "message:\"Fix bug\"",
             /* overrideExpr= */ "");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.NOT_APPLICABLE);
   }
 
@@ -128,7 +128,7 @@
             /* submittabilityExpr= */ "message:\"Fix a bug\"",
             /* overrideExpr= */ "");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
   }
 
@@ -141,7 +141,7 @@
             /* submittabilityExpr= */ "label:\"code-review=+2\"",
             /* overrideExpr= */ "");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
     assertThat(result.submittabilityExpressionResult().failingAtoms())
         .containsExactly("label:\"code-review=+2\"");
@@ -163,7 +163,7 @@
             /* submittabilityExpr= */ "label:\"code-review=+2\"",
             /* overrideExpr= */ "label:\"build-cop-override=+1\"");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.OVERRIDDEN);
   }
 
@@ -178,7 +178,7 @@
             /* submittabilityExpr= */ "label:\"code-review=+2\"",
             /* overrideExpr= */ "label:\"build-cop-override=+1\"");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
     assertThat(result.applicabilityExpressionResult().get().errorMessage().get())
         .isEqualTo("Unsupported operator invalid_field:invalid_value");
@@ -195,7 +195,7 @@
             /* submittabilityExpr= */ "invalid_field:invalid_value",
             /* overrideExpr= */ "label:\"build-cop-override=+1\"");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
     assertThat(result.submittabilityExpressionResult().errorMessage().get())
         .isEqualTo("Unsupported operator invalid_field:invalid_value");
@@ -209,7 +209,7 @@
             /* submittabilityExpr= */ "label:\"code-review=+2\"",
             /* overrideExpr= */ "invalid_field:invalid_value");
 
-    SubmitRequirementResult result = evaluator.evaluate(sr, changeData);
+    SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
     assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.ERROR);
     assertThat(result.overrideExpressionResult().get().errorMessage().get())
         .isEqualTo("Unsupported operator invalid_field:invalid_value");
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
index 9392219..b4d9558 100644
--- a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -269,6 +269,7 @@
             .key(PatchSetApproval.key(psId, approver, LabelId.create("Code-Review")))
             .value(value)
             .build();
-    return ApprovalContext.create(changeNotes, approval, newPsId, changeKind);
+    return ApprovalContext.create(
+        changeNotes, approval, changeNotes.getPatchSets().get(newPsId), changeKind);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
index f866fff..3b38bad 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -38,7 +38,6 @@
 public abstract class AbstractIndexTests extends AbstractDaemonTest {
   @Inject private ExtensionRegistry extensionRegistry;
 
-  /** @param injector injector */
   public void configureIndex(Injector injector) {}
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetAccountIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetAccountIT.java
new file mode 100644
index 0000000..a82876e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/SetAccountIT.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.inject.Inject;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+
+@UseSsh
+@NoHttpd
+public class SetAccountIT extends AbstractDaemonTest {
+  @Inject private ExternalIds externalIds;
+  @Inject private ProjectOperations projectOperations;
+
+  @Test
+  public void setAccount_deleteExternalId_all() throws Exception {
+    TestAccount testAccount = accountCreator.create("user1", "user1@example.com", null, null);
+    adminSshSession.exec("gerrit set-account --delete-external-id ALL user1");
+    adminSshSession.assertSuccess();
+    assertThat(externalIds.byAccount(testAccount.id()).isEmpty()).isTrue();
+  }
+
+  @Test
+  public void setAccount_deleteExternalId_single() throws Exception {
+    TestAccount testAccount = accountCreator.create("user2", "user2@example.com", null, null);
+    List<String> extIdKeys = getExternalIdKeys(testAccount);
+    assertThat(extIdKeys.contains("username:user2")).isTrue();
+    assertThat(extIdKeys.contains("mailto:user2@example.com")).isTrue();
+    adminSshSession.exec("gerrit set-account --delete-external-id username:user2 user2");
+    adminSshSession.assertSuccess();
+    extIdKeys = getExternalIdKeys(testAccount);
+    assertThat(extIdKeys.contains("username:user3")).isFalse();
+    assertThat(extIdKeys.contains("mailto:user3@example.com")).isFalse();
+  }
+
+  @Test
+  public void setAccount_deleteExternalId_multiple() throws Exception {
+    TestAccount testAccount = accountCreator.create("user3", "user3@example.com", null, null);
+    List<String> extIdKeys = getExternalIdKeys(testAccount);
+    assertThat(extIdKeys.contains("username:user3")).isTrue();
+    assertThat(extIdKeys.contains("mailto:user3@example.com")).isTrue();
+    adminSshSession.exec(
+        "gerrit set-account --delete-external-id username:user3 --delete-external-id mailto:user3@example.com user3");
+    adminSshSession.assertSuccess();
+    extIdKeys = getExternalIdKeys(testAccount);
+    assertThat(extIdKeys.contains("username:user3")).isFalse();
+    assertThat(extIdKeys.contains("mailto:user3@example.com")).isFalse();
+  }
+
+  @Test
+  public void setAccount_deleteExternalId_byUser() throws Exception {
+    userSshSession.exec("gerrit set-account --delete-external-id mailto:admin@example.com admin");
+    userSshSession.assertFailure();
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability(GlobalCapability.MODIFY_ACCOUNT).group(REGISTERED_USERS))
+        .update();
+    userSshSession.exec("gerrit set-account --delete-external-id mailto:admin@example.com admin");
+    userSshSession.assertSuccess();
+    userSshSession.exec("gerrit set-account --delete-external-id username:admin admin");
+    userSshSession.assertFailure();
+  }
+
+  private List<String> getExternalIdKeys(TestAccount account) throws Exception {
+    return externalIds.byAccount(account.id()).stream()
+        .map(e -> e.key().get())
+        .collect(Collectors.toList());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
index d08e219..e21cb26 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCancellationIT.java
@@ -122,7 +122,7 @@
   @Test
   public void abortIfClientProvidedDeadlineExceeded() throws Exception {
     adminSshSession.exec("gerrit create-project --deadline 1ms " + name("new"));
-    adminSshSession.assertFailure("Client Provided Deadline Exceeded (timeout=1ms)");
+    adminSshSession.assertFailure("Client Provided Deadline Exceeded (client.timeout=1ms)");
   }
 
   @Test
@@ -149,17 +149,18 @@
     adminSshSession.assertSuccess();
   }
 
+  @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void abortIfServerDeadlineExceeded() throws Exception {
     adminSshSession.exec("gerrit create-project " + name("new"));
-    adminSshSession.assertFailure("Server Deadline Exceeded (timeout=1ms)");
+    adminSshSession.assertFailure("Server Deadline Exceeded (default.timeout=1ms)");
   }
 
   @Test
   @GerritConfig(name = "deadline.default.timeout", value = "1ms")
   public void clientProvidedDeadlineOverridesServerDeadline() throws Exception {
     adminSshSession.exec("gerrit create-project --deadline 2ms " + name("new"));
-    adminSshSession.assertFailure("Client Provided Deadline Exceeded (timeout=2ms)");
+    adminSshSession.assertFailure("Client Provided Deadline Exceeded (client.timeout=2ms)");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/auth/ldap/LdapRealmTest.java b/javatests/com/google/gerrit/auth/ldap/LdapRealmTest.java
index 7a61626..0883033 100644
--- a/javatests/com/google/gerrit/auth/ldap/LdapRealmTest.java
+++ b/javatests/com/google/gerrit/auth/ldap/LdapRealmTest.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
@@ -44,6 +45,7 @@
 
 public final class LdapRealmTest {
   @Inject private LdapRealm ldapRealm = null;
+  @Inject private ExternalIdFactory externalIdFactory;
 
   @Before
   public void setUpInjector() throws Exception {
@@ -67,7 +69,7 @@
   }
 
   private ExternalId id(String scheme, String id) {
-    return ExternalId.create(scheme, id, Account.id(1000));
+    return externalIdFactory.create(scheme, id, Account.id(1000));
   }
 
   private boolean accountBelongsToRealm(ExternalId... ids) {
diff --git a/javatests/com/google/gerrit/auth/oauth/OAuthRealmTest.java b/javatests/com/google/gerrit/auth/oauth/OAuthRealmTest.java
index 1af78e3..3ec6f28 100644
--- a/javatests/com/google/gerrit/auth/oauth/OAuthRealmTest.java
+++ b/javatests/com/google/gerrit/auth/oauth/OAuthRealmTest.java
@@ -24,6 +24,7 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -34,6 +35,7 @@
 
 public final class OAuthRealmTest {
   @Inject private OAuthRealm oauthRealm = null;
+  @Inject private ExternalIdFactory externalIdFactory;
 
   @Before
   public void setUpInjector() throws Exception {
@@ -42,7 +44,7 @@
   }
 
   private ExternalId id(String scheme, String id) {
-    return ExternalId.create(scheme, id, Account.id(1000));
+    return externalIdFactory.create(scheme, id, Account.id(1000));
   }
 
   private boolean accountBelongsToRealm(ExternalId... ids) {
diff --git a/javatests/com/google/gerrit/auth/openid/OpenIdRealmTest.java b/javatests/com/google/gerrit/auth/openid/OpenIdRealmTest.java
index 05b0ec0..f83409b 100644
--- a/javatests/com/google/gerrit/auth/openid/OpenIdRealmTest.java
+++ b/javatests/com/google/gerrit/auth/openid/OpenIdRealmTest.java
@@ -25,6 +25,7 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -35,6 +36,7 @@
 
 public final class OpenIdRealmTest {
   @Inject private OpenIdRealm openidRealm = null;
+  @Inject private ExternalIdFactory extIdFactory;
 
   @Before
   public void setUpInjector() throws Exception {
@@ -43,7 +45,7 @@
   }
 
   private ExternalId id(String scheme, String id) {
-    return ExternalId.create(scheme, id, Account.id(1000));
+    return extIdFactory.create(scheme, id, Account.id(1000));
   }
 
   private boolean accountBelongsToRealm(ExternalId... ids) {
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
index bf39ff8..d332f8a 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
@@ -49,6 +49,7 @@
             .tag("tag-21")
             .realAccountId(Account.id(612))
             .postSubmit(true)
+            .copied(true)
             .build();
 
     Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
@@ -68,6 +69,7 @@
             .setTag("tag-21")
             .setRealAccountId(Entities.Account_Id.newBuilder().setId(612))
             .setPostSubmit(true)
+            .setCopied(true)
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
@@ -99,6 +101,7 @@
             .setGranted(987654L)
             // This value can't be unset when our entity class is given.
             .setPostSubmit(false)
+            .setCopied(false)
             .build();
     assertThat(proto).isEqualTo(expectedProto);
   }
@@ -115,6 +118,7 @@
             .tag("tag-21")
             .realAccountId(Account.id(612))
             .postSubmit(true)
+            .copied(true)
             .build();
 
     PatchSetApproval convertedPatchSetApproval =
@@ -162,6 +166,7 @@
     assertThat(patchSetApproval.value()).isEqualTo(0);
     assertThat(patchSetApproval.granted()).isEqualTo(new Timestamp(0));
     assertThat(patchSetApproval.postSubmit()).isEqualTo(false);
+    assertThat(patchSetApproval.copied()).isEqualTo(false);
   }
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
@@ -176,6 +181,7 @@
                 .put("tag", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("realAccountId", Account.Id.class)
                 .put("postSubmit", boolean.class)
+                .put("copied", boolean.class)
                 .put("toBuilder", PatchSetApproval.Builder.class)
                 .build());
   }
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 4352fe8..024e35e 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -58,6 +58,17 @@
   }
 
   @Test
+  public void getDiff_returnsOldAndNewChangeInfos() {
+    ChangeInfo oldChangeInfo = createChangeInfoWithTopic("topic");
+    ChangeInfo newChangeInfo = createChangeInfoWithTopic(oldChangeInfo.topic);
+
+    ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+    assertThat(diff.oldChangeInfo()).isEqualTo(oldChangeInfo);
+    assertThat(diff.newChangeInfo()).isEqualTo(newChangeInfo);
+  }
+
+  @Test
   public void getDiff_givenUnchangedTopic_returnsNullTopics() {
     ChangeInfo oldChangeInfo = createChangeInfoWithTopic("topic");
     ChangeInfo newChangeInfo = createChangeInfoWithTopic(oldChangeInfo.topic);
diff --git a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 45b3419..05e9808 100644
--- a/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.gpg;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.gpg.GerritPublicKeyChecker.toExtIdKey;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testing.TestTrustKeys.keyA;
@@ -39,6 +38,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.testing.InMemoryModule;
@@ -76,6 +76,10 @@
 
   @Inject private ThreadLocalRequestContext requestContext;
 
+  @Inject private AuthRequest.Factory authRequestFactory;
+
+  @Inject private ExternalIdFactory externalIdFactory;
+
   private LifecycleManager lifecycle;
   private Account.Id userId;
   private IdentifiedUser user;
@@ -101,7 +105,7 @@
     lifecycle.start();
 
     schemaCreator.create();
-    userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+    userId = accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
     // Note: does not match any key in TestKeys.
     accountsUpdateProvider
         .get()
@@ -121,7 +125,7 @@
   }
 
   private IdentifiedUser addUser(String name) throws Exception {
-    AuthRequest req = AuthRequest.forUser(name);
+    AuthRequest req = authRequestFactory.createForUser(name);
     Account.Id id = accountManager.authenticate(req).getAccountId();
     return userFactory.create(id);
   }
@@ -202,16 +206,18 @@
     reloadUser();
 
     TestKey key = validKeyWithSecondUserId();
-    PublicKeyChecker checker = checkerFactory.create(user, store).disableTrust();
+    GerritPublicKeyChecker checker =
+        (GerritPublicKeyChecker) checkerFactory.create(user, store).disableTrust();
     assertProblems(
         checker.check(key.getPublicKey()),
         Status.BAD,
         "No identities found for user; check http://test/settings#Identities");
 
-    checker = checkerFactory.create().setStore(store).disableTrust();
+    checker = (GerritPublicKeyChecker) checkerFactory.create().setStore(store).disableTrust();
     assertProblems(
         checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
-    insertExtId(ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
+    insertExtId(
+        externalIdFactory.create(checker.toExtIdKey(key.getPublicKey()), user.getAccountId()));
     assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user");
   }
 
@@ -362,13 +368,15 @@
   private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
     Account.Id id = user.getAccountId();
     List<ExternalId> newExtIds = new ArrayList<>(2);
-    newExtIds.add(ExternalId.create(toExtIdKey(kr.getPublicKey()), id));
+    GerritPublicKeyChecker checker =
+        (GerritPublicKeyChecker) checkerFactory.create(user, store).disableTrust();
+    newExtIds.add(externalIdFactory.create(checker.toExtIdKey(kr.getPublicKey()), id));
 
     String userId = Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null);
     if (userId != null) {
       String email = PushCertificateIdent.parse(userId).getEmailAddress();
       assertThat(email).contains("@");
-      newExtIds.add(ExternalId.createEmail(id, email));
+      newExtIds.add(externalIdFactory.createEmail(id, email));
     }
 
     store.add(kr);
@@ -401,7 +409,7 @@
   }
 
   private void addExternalId(String scheme, String id, String email) throws Exception {
-    insertExtId(ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
+    insertExtId(externalIdFactory.createWithEmail(scheme, id, user.getAccountId(), email));
   }
 
   private void insertExtId(ExternalId extId) throws Exception {
diff --git a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
index 2f0fafa..da6092b 100644
--- a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -32,8 +32,12 @@
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.PasswordVerifier;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
@@ -62,12 +66,6 @@
   private static final String AUTH_PASSWORD = "jd123";
   private static final String GERRIT_COOKIE_KEY = "GerritAccount";
   private static final String AUTH_COOKIE_VALUE = "gerritcookie";
-  private static final ExternalId AUTH_USER_PASSWORD_EXTERNAL_ID =
-      ExternalId.createWithPassword(
-          ExternalId.Key.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
-          AUTH_ACCOUNT_ID,
-          null,
-          AUTH_PASSWORD);
 
   @Mock private DynamicItem<WebSession> webSessionItem;
 
@@ -93,14 +91,23 @@
   private FakeHttpServletRequest req;
   private HttpServletResponse res;
   private AuthResult authSuccessful;
+  private ExternalIdFactory extIdFactory;
+  private ExternalIdKeyFactory extIdKeyFactory;
+  private PasswordVerifier pwdVerifier;
+  private AuthRequest.Factory authRequestFactory;
 
   @Before
   public void setUp() throws Exception {
     req = new FakeHttpServletRequest("gerrit.example.com", 80, "", "");
     res = new FakeHttpServletResponse();
 
+    extIdKeyFactory = new ExternalIdKeyFactory(new ExternalIdKeyFactory.ConfigImpl(authConfig));
+    extIdFactory = new ExternalIdFactory(extIdKeyFactory);
+    authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
+    pwdVerifier = new PasswordVerifier(extIdKeyFactory);
+
     authSuccessful =
-        new AuthResult(AUTH_ACCOUNT_ID, ExternalId.Key.create("username", AUTH_USER), false);
+        new AuthResult(AUTH_ACCOUNT_ID, extIdKeyFactory.create("username", AUTH_USER), false);
     doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
     doReturn(Optional.of(accountState)).when(accountCache).get(AUTH_ACCOUNT_ID);
     doReturn(account).when(accountState).account();
@@ -121,7 +128,13 @@
     res.setStatus(HttpServletResponse.SC_OK);
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -136,7 +149,13 @@
     res.setStatus(HttpServletResponse.SC_OK);
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -155,7 +174,13 @@
     doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -175,7 +200,13 @@
     res.setStatus(HttpServletResponse.SC_OK);
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -216,7 +247,13 @@
     doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -235,7 +272,13 @@
     doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
 
@@ -255,7 +298,13 @@
     res.setStatus(HttpServletResponse.SC_OK);
 
     ProjectBasicAuthFilter basicAuthFilter =
-        new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
+        new ProjectBasicAuthFilter(
+            webSessionItem,
+            accountCache,
+            accountManager,
+            authConfig,
+            authRequestFactory,
+            pwdVerifier);
 
     basicAuthFilter.doFilter(req, res, chain);
   }
@@ -278,9 +327,13 @@
   }
 
   private void initMockedUsernamePasswordExternalId() {
-    doReturn(ImmutableSet.builder().add(AUTH_USER_PASSWORD_EXTERNAL_ID).build())
-        .when(accountState)
-        .externalIds();
+    ExternalId extId =
+        extIdFactory.createWithPassword(
+            extIdKeyFactory.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
+            AUTH_ACCOUNT_ID,
+            null,
+            AUTH_PASSWORD);
+    doReturn(ImmutableSet.builder().add(extId).build()).when(accountState).externalIds();
   }
 
   private void requestBasicAuth(FakeHttpServletRequest fakeReq) {
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index b22b8ad..a2432a2 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -31,11 +31,11 @@
   protected static final String CHANGE_URL =
       "https://gerrit-review.googlesource.com/c/project/+/123";
 
-  protected static void assertChangeMessage(String message, MailComment comment) {
+  protected static void assertPatchsetComment(String message, MailComment comment) {
     assertThat(comment.fileName).isNull();
     assertThat(comment.message).isEqualTo(message);
     assertThat(comment.inReplyTo).isNull();
-    assertThat(comment.type).isEqualTo(MailComment.CommentType.CHANGE_MESSAGE);
+    assertThat(comment.type).isEqualTo(MailComment.CommentType.PATCHSET_LEVEL);
   }
 
   protected static void assertInlineComment(
diff --git a/javatests/com/google/gerrit/mail/HtmlParserTest.java b/javatests/com/google/gerrit/mail/HtmlParserTest.java
index d661278..bb60fd8 100644
--- a/javatests/com/google/gerrit/mail/HtmlParserTest.java
+++ b/javatests/com/google/gerrit/mail/HtmlParserTest.java
@@ -35,7 +35,7 @@
     List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, "");
 
     assertThat(parsedComments).hasSize(1);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
   }
 
   @Test
@@ -56,7 +56,7 @@
     List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, "");
 
     assertThat(parsedComments).hasSize(1);
-    assertChangeMessage(
+    assertPatchsetComment(
         "Did you consider this: http://gerritcodereview.com", parsedComments.get(0));
   }
 
@@ -77,7 +77,7 @@
     List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertInlineComment("I have a comment on this.", parsedComments.get(1), comments.get(1));
     assertInlineComment("Also have a comment here.", parsedComments.get(2), comments.get(4));
   }
@@ -100,7 +100,7 @@
     List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertInlineComment(
         "How about [1]? This would help IMHO.\n\n[1] http://gerritcodereview.com",
         parsedComments.get(1),
@@ -125,7 +125,7 @@
     List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertFileComment("This is a nice file", parsedComments.get(1), comments.get(1).key.filename);
     assertInlineComment("Also have a comment here.", parsedComments.get(2), comments.get(4));
   }
@@ -168,7 +168,7 @@
     List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage(txtMessage, parsedComments.get(0));
+    assertPatchsetComment(txtMessage, parsedComments.get(0));
     assertFileComment(txtMessage, parsedComments.get(1), comments.get(1).key.filename);
     assertInlineComment(txtMessage, parsedComments.get(2), comments.get(4));
   }
@@ -176,7 +176,6 @@
   /**
    * Create an html message body with the specified comments.
    *
-   * @param changeMessage
    * @param c1 Comment in reply to first comment.
    * @param c2 Comment in reply to second comment.
    * @param c3 Comment in reply to third comment.
diff --git a/javatests/com/google/gerrit/mail/TextParserTest.java b/javatests/com/google/gerrit/mail/TextParserTest.java
index f1d6179..d3e7447 100644
--- a/javatests/com/google/gerrit/mail/TextParserTest.java
+++ b/javatests/com/google/gerrit/mail/TextParserTest.java
@@ -43,7 +43,7 @@
     List<MailComment> parsedComments = TextParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(1);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
   }
 
   @Test
@@ -64,7 +64,7 @@
     List<MailComment> parsedComments = TextParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertInlineComment("I have a comment on this.", parsedComments.get(1), comments.get(1));
     assertInlineComment("Also have a comment here.", parsedComments.get(2), comments.get(3));
   }
@@ -87,7 +87,7 @@
     List<MailComment> parsedComments = TextParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertFileComment("This is a nice file", parsedComments.get(1), comments.get(1).key.filename);
     assertInlineComment("Also have a comment here.", parsedComments.get(2), comments.get(3));
   }
@@ -138,7 +138,7 @@
     List<MailComment> parsedComments = TextParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(3);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertFileComment("This is a nice file", parsedComments.get(1), comments.get(1).key.filename);
     assertInlineComment("Also have a comment here.", parsedComments.get(2), comments.get(3));
   }
@@ -161,7 +161,7 @@
     List<MailComment> parsedComments = TextParser.parse(b.build(), comments, CHANGE_URL);
 
     assertThat(parsedComments).hasSize(2);
-    assertChangeMessage("Looks good to me", parsedComments.get(0));
+    assertPatchsetComment("Looks good to me", parsedComments.get(0));
     assertInlineComment("Comment in reply to file comment", parsedComments.get(1), comments.get(0));
   }
 
@@ -174,14 +174,13 @@
     List<MailComment> parsedComments = TextParser.parse(b.build(), defaultComments(), CHANGE_URL);
 
     assertThat(parsedComments).hasSize(1);
-    assertChangeMessage(
+    assertPatchsetComment(
         "Nice change\n\nMy other comment on the same entity", parsedComments.get(0));
   }
 
   /**
    * Create a plaintext message body with the specified comments.
    *
-   * @param changeMessage
    * @param c1 Comment in reply to first inline comment.
    * @param c2 Comment in reply to second inline comment.
    * @param c3 Comment in reply to third inline comment.
diff --git a/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
index e34b578..fd47567 100644
--- a/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
+++ b/javatests/com/google/gerrit/pgm/init/api/AllProjectsConfigTest.java
@@ -17,6 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.config.AllProjectsConfigProvider;
+import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.securestore.testing.InMemorySecureStore;
 import java.io.File;
@@ -76,8 +78,11 @@
     InitFlags flags = new InitFlags(sitePaths, secureStore, ImmutableList.of(), false);
     Section.Factory sections =
         (name, subsection) -> new Section(flags, sitePaths, secureStore, ui, name, subsection);
+    AllProjectsConfigProvider configProvider = new FileBasedAllProjectsConfigProvider(sitePaths);
+
     allProjectsConfig =
-        new AllProjectsConfig(new AllProjectsNameOnInitProvider(sections), sitePaths, flags);
+        new AllProjectsConfig(
+            new AllProjectsNameOnInitProvider(sections), configProvider, sitePaths, flags);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/CancellationMetricsTest.java b/javatests/com/google/gerrit/server/RequestInfoTest.java
similarity index 93%
rename from javatests/com/google/gerrit/server/CancellationMetricsTest.java
rename to javatests/com/google/gerrit/server/RequestInfoTest.java
index b27d9d2..fafe856 100644
--- a/javatests/com/google/gerrit/server/CancellationMetricsTest.java
+++ b/javatests/com/google/gerrit/server/RequestInfoTest.java
@@ -18,7 +18,7 @@
 
 import org.junit.Test;
 
-public class CancellationMetricsTest {
+public class RequestInfoTest {
   @Test
   public void redactRequestUri() throws Exception {
     // test with valid request URIs
@@ -28,6 +28,7 @@
     assertThat(redact("/changes/123")).isEqualTo("/changes/*");
     assertThat(redact("/changes/123/detail")).isEqualTo("/changes/*/detail");
     assertThat(redact("/changes/123/detail/")).isEqualTo("/changes/*/detail/");
+    assertThat(redact("/accounts/self/capabilities")).isEqualTo("/accounts/*/capabilities");
     assertThat(redact("/foo/123/bar/567")).isEqualTo("/foo/*/bar/*");
     assertThat(redact("/foo/123/bar/567/baz")).isEqualTo("/foo/*/bar/*/baz");
     assertThat(redact("/foo/123/bar/567/baz/")).isEqualTo("/foo/*/bar/*/baz/");
@@ -51,6 +52,6 @@
   }
 
   public static String redact(String uri) {
-    return CancellationMetrics.redactRequestUri(uri);
+    return RequestInfo.redactRequestUri(uri);
   }
 }
diff --git a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
index 814df03..7d9db0b 100644
--- a/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/AllExternalIdsTest.java
@@ -26,11 +26,28 @@
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto.ExternalIdProto;
 import com.google.inject.TypeLiteral;
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
 import org.junit.Test;
 
 public class AllExternalIdsTest {
+  private ExternalIdFactory externalIdFactory;
+
+  @Before
+  public void setUp() throws Exception {
+    externalIdFactory =
+        new ExternalIdFactory(
+            new ExternalIdKeyFactory(
+                new ExternalIdKeyFactory.Config() {
+                  @Override
+                  public boolean isUserNameCaseInsensitive() {
+                    return false;
+                  }
+                }));
+  }
+
   @Test
   public void serializeEmptyExternalIds() throws Exception {
     assertRoundTrip(allExternalIds(), AllExternalIdsProto.getDefaultInstance());
@@ -42,10 +59,10 @@
     Account.Id accountId2 = Account.id(1002);
     assertRoundTrip(
         allExternalIds(
-            ExternalId.create("scheme1", "id1", accountId1),
-            ExternalId.create("scheme2", "id2", accountId1),
-            ExternalId.create("scheme2", "id3", accountId2),
-            ExternalId.create("scheme3", "id4", accountId2)),
+            externalIdFactory.create("scheme1", "id1", accountId1),
+            externalIdFactory.create("scheme2", "id2", accountId1),
+            externalIdFactory.create("scheme2", "id3", accountId2),
+            externalIdFactory.create("scheme3", "id4", accountId2)),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
                 ExternalIdProto.newBuilder().setKey("scheme1:id1").setAccountId(1001).build())
@@ -61,7 +78,7 @@
   @Test
   public void serializeExternalIdWithEmail() throws Exception {
     assertRoundTrip(
-        allExternalIds(ExternalId.createEmail(Account.id(1001), "foo@example.com")),
+        allExternalIds(externalIdFactory.createEmail(Account.id(1001), "foo@example.com")),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
                 ExternalIdProto.newBuilder()
@@ -75,7 +92,7 @@
   public void serializeExternalIdWithPassword() throws Exception {
     assertRoundTrip(
         allExternalIds(
-            ExternalId.create("scheme", "id", Account.id(1001), null, "hashed password")),
+            externalIdFactory.create("scheme", "id", Account.id(1001), null, "hashed password")),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
                 ExternalIdProto.newBuilder()
@@ -89,8 +106,8 @@
   public void serializeExternalIdWithBlobId() throws Exception {
     assertRoundTrip(
         allExternalIds(
-            ExternalId.create(
-                ExternalId.create("scheme", "id", Account.id(1001)),
+            externalIdFactory.create(
+                externalIdFactory.create("scheme", "id", Account.id(1001)),
                 ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))),
         AllExternalIdsProto.newBuilder()
             .addExternalId(
@@ -121,12 +138,14 @@
   public void externalIdMethods() {
     assertThatSerializedClass(ExternalId.class)
         .hasAutoValueMethods(
-            ImmutableMap.of(
-                "key", ExternalId.Key.class,
-                "accountId", Account.Id.class,
-                "email", String.class,
-                "password", String.class,
-                "blobId", ObjectId.class));
+            ImmutableMap.<String, Type>builder()
+                .put("key", ExternalId.Key.class)
+                .put("accountId", Account.Id.class)
+                .put("isCaseInsensitive", boolean.class)
+                .put("email", String.class)
+                .put("password", String.class)
+                .put("blobId", ObjectId.class)
+                .build());
   }
 
   private static AllExternalIds allExternalIds(ExternalId... externalIds) {
diff --git a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
index 5717e78..4f8c559 100644
--- a/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/ExternalIDCacheLoaderTest.java
@@ -57,11 +57,23 @@
   private ExternalIdReader externalIdReader;
   private ExternalIdReader externalIdReaderSpy;
 
+  private ExternalIdFactory externalIdFactory;
+
   @Before
   public void setUp() throws Exception {
+    externalIdFactory =
+        new ExternalIdFactory(
+            new ExternalIdKeyFactory(
+                new ExternalIdKeyFactory.Config() {
+                  @Override
+                  public boolean isUserNameCaseInsensitive() {
+                    return false;
+                  }
+                }));
     externalIdCache = CacheBuilder.newBuilder().build();
     repoManager.createRepository(ALL_USERS).close();
-    externalIdReader = new ExternalIdReader(repoManager, ALL_USERS, new DisabledMetricMaker());
+    externalIdReader =
+        new ExternalIdReader(repoManager, ALL_USERS, new DisabledMetricMaker(), externalIdFactory);
     externalIdReaderSpy = Mockito.spy(externalIdReader);
     loader = createLoader(true);
   }
@@ -151,7 +163,8 @@
     ObjectId head =
         modifyExternalId(
             externalId(1, 1),
-            ExternalId.create("fooschema", "bar1", Account.id(1), "foo@bar.com", "password"));
+            externalIdFactory.create(
+                "fooschema", "bar1", Account.id(1), "foo@bar.com", "password"));
     assertThat(allFromGit(head).byAccount().size()).isEqualTo(1);
     externalIdCache.put(firstState, allFromGit(firstState));
 
@@ -212,7 +225,8 @@
         externalIdReaderSpy,
         Providers.of(externalIdCache),
         new DisabledMetricMaker(),
-        cfg);
+        cfg,
+        externalIdFactory);
   }
 
   private AllExternalIds allFromGit(ObjectId revision) throws Exception {
@@ -256,13 +270,14 @@
   }
 
   private ExternalId externalId(int key, int accountId) {
-    return ExternalId.create("fooschema", "bar" + key, Account.id(accountId));
+    return externalIdFactory.create("fooschema", "bar" + key, Account.id(accountId));
   }
 
   private ObjectId performExternalIdUpdate(Consumer<ExternalIdNotes> update) throws Exception {
     try (Repository repo = repoManager.openRepository(ALL_USERS)) {
       PersonIdent updater = new PersonIdent("Foo bar", "foo@bar.com");
-      ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(ALL_USERS, repo);
+      ExternalIdNotes extIdNotes =
+          ExternalIdNotes.loadNoCacheUpdate(ALL_USERS, repo, externalIdFactory);
       update.accept(extIdNotes);
       try (MetaDataUpdate metaDataUpdate =
           new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
diff --git a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
index 19c479d..b69a894 100644
--- a/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/javatests/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -17,9 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.entities.RefNames.REFS_TAGS;
 
+import com.google.common.truth.Correspondence;
+import com.google.gerrit.truth.NullAwareCorrespondence;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -112,8 +116,12 @@
     IncludedInResolver.Result detail = resolve(commit_v2_5);
 
     // Check that only tags and branches which refer the tip are returned
-    assertThat(detail.tags()).containsExactly(TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
-    assertThat(detail.branches()).containsExactly(BRANCH_2_5);
+    assertThat(detail.tags())
+        .comparingElementsUsing(hasShortName())
+        .containsExactly(TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
+    assertThat(detail.branches())
+        .comparingElementsUsing(hasShortName())
+        .containsExactly(BRANCH_2_5);
   }
 
   @Test
@@ -123,6 +131,7 @@
 
     // Check whether all tags and branches are returned
     assertThat(detail.tags())
+        .comparingElementsUsing(hasShortName())
         .containsExactly(
             TAG_1_0,
             TAG_1_0_1,
@@ -133,6 +142,7 @@
             TAG_2_5_ANNOTATED,
             TAG_2_5_ANNOTATED_TWICE);
     assertThat(detail.branches())
+        .comparingElementsUsing(hasShortName())
         .containsExactly(BRANCH_MASTER, BRANCH_1_0, BRANCH_1_3, BRANCH_2_0, BRANCH_2_5);
   }
 
@@ -143,8 +153,11 @@
 
     // Check whether all succeeding tags and branches are returned
     assertThat(detail.tags())
+        .comparingElementsUsing(hasShortName())
         .containsExactly(TAG_1_3, TAG_2_5, TAG_2_5_ANNOTATED, TAG_2_5_ANNOTATED_TWICE);
-    assertThat(detail.branches()).containsExactly(BRANCH_1_3, BRANCH_2_5);
+    assertThat(detail.branches())
+        .comparingElementsUsing(hasShortName())
+        .containsExactly(BRANCH_1_3, BRANCH_2_5);
   }
 
   private IncludedInResolver.Result resolve(RevCommit commit) throws Exception {
@@ -154,4 +167,9 @@
   private RevTag tag(String name, RevObject dest) throws Exception {
     return tr.update(REFS_TAGS + name, tr.tag(name, dest));
   }
+
+  private static Correspondence<Ref, String> hasShortName() {
+    return NullAwareCorrespondence.transforming(
+        ref -> Repository.shortenRefName(ref.getName()), "has short name");
+  }
 }
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index cd28ac4..5e3be9a 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -71,6 +71,7 @@
   @Inject private ProjectConfig.Factory projectConfigFactory;
   @Inject private GerritApi gApi;
   @Inject private ProjectOperations projectOperations;
+  @Inject private AuthRequest.Factory authRequestFactory;
 
   private LifecycleManager lifecycle;
   private Account.Id userId;
@@ -87,7 +88,7 @@
     lifecycle.start();
 
     schemaCreator.create();
-    userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+    userId = accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
     user = userFactory.create(userId);
 
     requestContext.setContext(() -> user);
diff --git a/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
index 7832bec..6b8177e 100644
--- a/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
@@ -36,6 +36,12 @@
   }
 
   private static class TestGitRepositoryManager implements GitRepositoryManager {
+
+    @Override
+    public Status getRepositoryStatus(NameKey name) {
+      throw new UnsupportedOperationException("Not implemented");
+    }
+
     @Override
     public Repository openRepository(NameKey name) {
       throw new UnsupportedOperationException("Not implemented");
diff --git a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
index febb142..12130ea 100644
--- a/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -20,6 +20,7 @@
 
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager.Status;
 import com.google.gerrit.server.ioutil.HostPlatform;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -67,6 +68,7 @@
     try (Repository repo = repoManager.openRepository(projectA)) {
       assertThat(repo).isNotNull();
     }
+    assertThat(repoManager.getRepositoryStatus(projectA)).isEqualTo(Status.ACTIVE);
     assertThat(repoManager.list()).containsExactly(projectA);
   }
 
@@ -199,7 +201,7 @@
   public void testProjectRecreation() throws Exception {
     repoManager.createRepository(Project.nameKey("a"));
     assertThrows(
-        IllegalStateException.class, () -> repoManager.createRepository(Project.nameKey("a")));
+        RepositoryExistsException.class, () -> repoManager.createRepository(Project.nameKey("a")));
   }
 
   @Test
@@ -207,7 +209,8 @@
     repoManager.createRepository(Project.nameKey("a"));
     LocalDiskRepositoryManager newRepoManager = new LocalDiskRepositoryManager(site, cfg);
     assertThrows(
-        IllegalStateException.class, () -> newRepoManager.createRepository(Project.nameKey("a")));
+        RepositoryExistsException.class,
+        () -> newRepoManager.createRepository(Project.nameKey("a")));
   }
 
   @Test
@@ -221,6 +224,19 @@
   }
 
   @Test
+  public void testGetRepositoryStatusNameCaseMismatch() throws Exception {
+    assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
+    repoManager.createRepository(Project.nameKey("a"));
+    assertThat(repoManager.getRepositoryStatus(Project.nameKey("A"))).isEqualTo(Status.ACTIVE);
+  }
+
+  @Test
+  public void testGetRepositoryStatusNonExistent() throws Exception {
+    assertThat(repoManager.getRepositoryStatus(Project.nameKey("non-existent")))
+        .isEqualTo(Status.NON_EXISTENT);
+  }
+
+  @Test
   public void testNameCaseMismatch() throws Exception {
     assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue();
     repoManager.createRepository(Project.nameKey("a"));
@@ -272,6 +288,12 @@
   }
 
   @Test
+  public void testGetRepoStatusInvalidName() throws Exception {
+    assertThat(repoManager.getRepositoryStatus(Project.nameKey("project%?|<>A")))
+        .isEqualTo(Status.NON_EXISTENT);
+  }
+
+  @Test
   public void list() throws Exception {
     Project.NameKey projectA = Project.nameKey("projectA");
     createRepository(repoManager.getBasePath(projectA), projectA.get());
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index 92a5fbe..d16efc3 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -56,25 +56,33 @@
             .build();
     ExternalId extId1 =
         ExternalId.create(
-            ExternalId.Key.create(ExternalId.SCHEME_MAILTO, "foo.bar@example.com"),
+            ExternalId.Key.create(ExternalId.SCHEME_MAILTO, "foo.bar@example.com", false),
             id,
             "foo.bar@example.com",
             null,
             ObjectId.fromString("1b9a0cf038ea38a0ab08617c39aa8e28413a27ca"));
     ExternalId extId2 =
         ExternalId.create(
-            ExternalId.Key.create(ExternalId.SCHEME_USERNAME, "foo"),
+            ExternalId.Key.create(ExternalId.SCHEME_USERNAME, "foo", false),
             id,
             null,
             "secret",
             ObjectId.fromString("5b3a73dc9a668a5b89b5f049225261e3e3291d1a"));
+    ExternalId extId3 =
+        ExternalId.create(
+            ExternalId.Key.create(ExternalId.SCHEME_USERNAME, "Bar", true),
+            id,
+            null,
+            "secret",
+            ObjectId.fromString("483ea804e84282e15ddcdd1d15a797eb4796a760"));
     List<String> values =
         toStrings(
             AccountField.EXTERNAL_ID_STATE.get(
-                AccountState.forAccount(account, ImmutableSet.of(extId1, extId2))));
+                AccountState.forAccount(account, ImmutableSet.of(extId1, extId2, extId3))));
     String expectedValue1 = extId1.key().sha1().name() + ":" + extId1.blobId().name();
     String expectedValue2 = extId2.key().sha1().name() + ":" + extId2.blobId().name();
-    assertThat(values).containsExactly(expectedValue1, expectedValue2);
+    String expectedValue3 = extId3.key().sha1().name() + ":" + extId3.blobId().name();
+    assertThat(values).containsExactly(expectedValue1, expectedValue2, expectedValue3);
   }
 
   private List<String> toStrings(Iterable<byte[]> values) {
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 521af2f..66a98e8 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -55,6 +55,7 @@
             null,
             new Config(),
             null,
+            null,
             null));
   }
 
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index ed4325d..fefa066 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -34,6 +34,7 @@
 import com.google.inject.Injector;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 import org.eclipse.jgit.lib.Config;
 import org.junit.After;
 import org.junit.Before;
@@ -110,9 +111,10 @@
 
   @Test
   public void
-      traceTimersInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers() {
+      traceTimersInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers()
+          throws Exception {
     // Remove test performance logger so that there are no registered performance loggers.
-    performanceLoggerRegistrationHandle.remove();
+    removeAllPerformanceLoggers();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
     assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
@@ -277,9 +279,10 @@
 
   @Test
   public void
-      timerMetricssInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers() {
-    // Remove test performance logger so that there are no registered performance loggers.
-    performanceLoggerRegistrationHandle.remove();
+      timerMetricssInsidePerformanceLogContextDoNotCreatePerformanceLogIfNoPerformanceLoggers()
+          throws Exception {
+    // Remove all performance loggers so that there are no registered performance loggers.
+    removeAllPerformanceLoggers();
 
     assertThat(LoggingContext.getInstance().isPerformanceLogging()).isFalse();
     assertThat(LoggingContext.getInstance().getPerformanceLogRecords()).isEmpty();
@@ -369,6 +372,12 @@
     }
   }
 
+  private void removeAllPerformanceLoggers() throws Exception {
+    java.lang.reflect.Field itemsField = DynamicSet.class.getDeclaredField("items");
+    itemsField.setAccessible(true);
+    ((CopyOnWriteArrayList<?>) itemsField.get(performanceLoggers)).clear();
+  }
+
   @AutoValue
   abstract static class PerformanceLogEntry {
     static PerformanceLogEntry create(String operation, Metadata metadata) {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 6a32fa1..dc9b9cd 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -153,6 +153,43 @@
   }
 
   @Test
+  public void parseCopiedApproval() throws Exception {
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 1\n"
+            + "Copied-Label: Label1=+1 Account <1@gerrit>,Other Account <2@Gerrit>\n"
+            + "Copied-Label: Label2=+1 Account <1@gerrit>\n"
+            + "Copied-Label: Label3=+1 Account <1@gerrit>,Other Account <2@Gerrit> :\"tag\"\n"
+            + "Copied-Label: Label4=+1 Account <1@Gerrit> :\"tag with characters %^#@^( *::!\"\n"
+            + "Subject: This is a test change\n");
+    assertParseSucceeds(
+        "Update change\n"
+            + "\n"
+            + "Branch: refs/heads/master\n"
+            + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+            + "Patch-set: 1\n"
+            + "Label: -Label1\n"
+            + "Label: -Label4 Account <1@gerrit>\n"
+            + "Subject: This is a test change\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=X\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1 = 1\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: X+Y\n");
+    assertParseFails(
+        "Update change\n\nPatch-set: 1\nCopied-Label: Label1 Other Account <2@gerrit>\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: -Label!1\n");
+    assertParseFails(
+        "Update change\n\nPatch-set: 1\nCopied-Label: -Label!1 Other Account <2@gerrit>\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: -Label1\n");
+    assertParseFails(
+        "Update change\n\nPatch-set: 1\nCopied-Label: Label1 Other Account <2@gerrit>,Other "
+            + "Account <2@gerrit>,Other Account <2@gerrit> \n");
+    assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1 non-user\n");
+  }
+
+  @Test
   public void parseSubmitRecords() throws Exception {
     assertParseSucceeds(
         "Update change\n"
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 209d880..52eaf11 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -375,6 +375,7 @@
                 PatchSetApproval.key(
                     PatchSet.id(ID, 1), Account.id(2001), LabelId.create(LabelId.CODE_REVIEW)))
             .value(1)
+            .tag("tag")
             .granted(new Timestamp(1212L))
             .build();
     Entities.PatchSetApproval psa1 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a1);
@@ -386,11 +387,13 @@
                 PatchSetApproval.key(
                     PatchSet.id(ID, 1), Account.id(2002), LabelId.create(LabelId.VERIFIED)))
             .value(-1)
+            .tag("tag")
+            .copied(true)
             .granted(new Timestamp(3434L))
             .build();
     Entities.PatchSetApproval psa2 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a2);
     ByteString a2Bytes = Protos.toByteString(psa2);
-    assertThat(a2Bytes.size()).isEqualTo(49);
+    assertThat(a2Bytes.size()).isEqualTo(56);
     assertThat(a2Bytes).isNotEqualTo(a1Bytes);
 
     assertRoundTrip(
@@ -684,6 +687,7 @@
             .submitRequirementsResult(
                 ImmutableList.of(
                     SubmitRequirementResult.builder()
+                        .legacy(true)
                         .patchSetCommitId(
                             ObjectId.fromString("26e50c7d315a33a13e5cc00902781fa876bc36cd"))
                         .submitRequirement(
@@ -713,6 +717,7 @@
         newProtoBuilder()
             .addSubmitRequirementResult(
                 SubmitRequirementResultProto.newBuilder()
+                    .setLegacy(true)
                     .setCommit(
                         ObjectIdConverter.create()
                             .toByteString(
@@ -978,6 +983,7 @@
                 .put("tag", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("realAccountId", Account.Id.class)
                 .put("postSubmit", boolean.class)
+                .put("copied", boolean.class)
                 .put("toBuilder", PatchSetApproval.Builder.class)
                 .build());
   }
@@ -1035,6 +1041,8 @@
     assertThatSerializedClass(SubmitRecord.class)
         .hasFields(
             ImmutableMap.of(
+                "ruleName",
+                new TypeLiteral<String>() {}.getType(),
                 "status",
                 SubmitRecord.Status.class,
                 "labels",
@@ -1065,7 +1073,7 @@
             .setChangeId(ID.get())
             .setMergedOnMillis(234567L)
             .setHasMergedOn(true)
-            .setColumns(colsProto.toBuilder())
+            .setColumns(colsProto)
             .build());
   }
 
@@ -1128,7 +1136,7 @@
             .setChangeId(ID.get())
             .setServerId(DEFAULT_SERVER_ID)
             .setHasServerId(true)
-            .setColumns(colsProto.toBuilder())
+            .setColumns(colsProto)
             .build());
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 9c2e9a9..c524c94 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -514,6 +514,184 @@
   }
 
   @Test
+  public void copiedApprovals() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putCopiedApproval(
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    c.currentPatchSetId(),
+                    changeOwner.getAccountId(),
+                    LabelId.create(LabelId.CODE_REVIEW)))
+            .value(1)
+            .copied(true)
+            .granted(TimeUtil.nowTs())
+            .tag("tag")
+            .realAccountId(otherUserId)
+            .build());
+    update.putApprovalFor(otherUserId, LabelId.CODE_REVIEW, (short) -1);
+    update.commit();
+
+    // Only the non copied approval is reachable by getApprovals.
+    ChangeNotes notes = newNotes(c);
+    PatchSetApproval approval =
+        Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+    assertThat(approval.accountId()).isEqualTo(otherUser.getAccountId());
+    assertThat(approval.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(approval.value()).isEqualTo((short) -1);
+    assertThat(approval.copied()).isFalse();
+
+    // Get approvals with copied gets all of the approvals (including copied).
+    ImmutableList<PatchSetApproval> approvals =
+        notes.getApprovalsWithCopied().get(c.currentPatchSetId()).stream()
+            .sorted(comparing(a -> a.accountId().get()))
+            .collect(toImmutableList());
+    assertThat(approvals).hasSize(2);
+
+    PatchSetApproval copied = approvals.get(0);
+    assertThat(copied.accountId()).isEqualTo(changeOwner.getAccountId());
+    assertThat(copied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(copied.value()).isEqualTo((short) 1);
+    assertThat(copied.tag()).hasValue("tag");
+    assertThat(copied.accountId()).isEqualTo(changeOwner.getAccountId());
+    assertThat(copied.realAccountId()).isEqualTo(otherUserId);
+    assertThat(copied.copied()).isTrue();
+
+    PatchSetApproval nonCopied = approvals.get(1);
+    assertThat(nonCopied.accountId()).isEqualTo(otherUserId);
+    assertThat(nonCopied.realAccountId()).isEqualTo(otherUserId);
+    assertThat(nonCopied.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(nonCopied.value()).isEqualTo((short) -1);
+  }
+
+  @Test
+  public void copiedApprovalsCanBeRemoved() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putCopiedApproval(
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    c.currentPatchSetId(),
+                    changeOwner.getAccountId(),
+                    LabelId.create(LabelId.CODE_REVIEW)))
+            .value(1)
+            .copied(true)
+            .granted(TimeUtil.nowTs())
+            .build());
+    update.commit();
+
+    update.removeApproval(LabelId.CODE_REVIEW);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    PatchSetApproval approval =
+        Iterables.getOnlyElement(notes.getApprovalsWithCopied().get(c.currentPatchSetId()));
+    assertThat(approval.accountId()).isEqualTo(changeOwner.getAccountId());
+    assertThat(approval.label()).isEqualTo(LabelId.CODE_REVIEW);
+    // The vote got removed since the latest patch-set only has one vote and it's "0". The copied
+    // approval will never have a "0" vote, but it can be overridden by a "0" vote of a
+    // non-copied approval.
+    assertThat(approval.value()).isEqualTo((short) 0);
+    assertThat(approval.copied()).isFalse();
+  }
+
+  @Test
+  public void copiedApprovalsWithStrangeTags() throws Exception {
+    String strangeTag = "!@#$%^\0&*):\" \n: \r\"#$@,. :";
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putCopiedApproval(
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    c.currentPatchSetId(),
+                    changeOwner.getAccountId(),
+                    LabelId.create(LabelId.CODE_REVIEW)))
+            .value(1)
+            .copied(true)
+            .granted(TimeUtil.nowTs())
+            .tag(strangeTag)
+            .build());
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    PatchSetApproval approval =
+        Iterables.getOnlyElement(notes.getApprovalsWithCopied().get(c.currentPatchSetId()));
+    assertThat(approval.accountId()).isEqualTo(changeOwner.getAccountId());
+    assertThat(approval.label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(approval.value()).isEqualTo((short) 1);
+    assertThat(approval.tag()).hasValue(NoteDbUtil.sanitizeFooter(strangeTag));
+    assertThat(approval.copied()).isTrue();
+  }
+
+  @Test
+  public void copiedApprovalsPostSubmit() throws Exception {
+    Change c = newChange();
+    SubmissionId submissionId = new SubmissionId(c);
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putCopiedApproval(
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    c.currentPatchSetId(),
+                    changeOwner.getAccountId(),
+                    LabelId.create(LabelId.CODE_REVIEW)))
+            .value(1)
+            .copied(true)
+            .granted(TimeUtil.nowTs())
+            .build());
+    update.putCopiedApproval(
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    c.currentPatchSetId(),
+                    changeOwner.getAccountId(),
+                    LabelId.create(LabelId.VERIFIED)))
+            .value(1)
+            .copied(true)
+            .granted(TimeUtil.nowTs())
+            .build());
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.merge(
+        submissionId,
+        ImmutableList.of(
+            submitRecord(
+                "NOT_READY",
+                null,
+                submitLabel(LabelId.VERIFIED, "OK", changeOwner.getAccountId()),
+                submitLabel(LabelId.CODE_REVIEW, "NEED", null))));
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.putCopiedApproval(
+        PatchSetApproval.builder()
+            .key(
+                PatchSetApproval.key(
+                    c.currentPatchSetId(),
+                    changeOwner.getAccountId(),
+                    LabelId.create(LabelId.CODE_REVIEW)))
+            .value(2)
+            .copied(true)
+            .granted(TimeUtil.nowTs())
+            .build());
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    List<PatchSetApproval> approvals = Lists.newArrayList(notes.getApprovalsWithCopied().values());
+    assertThat(approvals).hasSize(2);
+    assertThat(approvals.get(0).label()).isEqualTo(LabelId.VERIFIED);
+    assertThat(approvals.get(0).value()).isEqualTo((short) 1);
+    assertThat(approvals.get(0).postSubmit()).isFalse();
+    assertThat(approvals.get(1).label()).isEqualTo(LabelId.CODE_REVIEW);
+    assertThat(approvals.get(1).value()).isEqualTo((short) 2);
+    assertThat(approvals.get(1).postSubmit()).isTrue();
+  }
+
+  @Test
   public void multipleReviewers() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 77db31f..26e1881 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -34,11 +34,11 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.entities.SubmitRecord;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.notedb.ChangeNoteUtil.AttentionStatusInNoteDb;
 import com.google.gerrit.server.notedb.CommitRewriter.BackfillResult;
+import com.google.gerrit.server.notedb.CommitRewriter.CommitDiff;
 import com.google.gerrit.server.notedb.CommitRewriter.RunOptions;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -124,6 +124,38 @@
   }
 
   @Test
+  public void outputDiffOff_refsReported() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Change has been successfully merged by " + changeOwner.getName());
+    ObjectId commitToFix = update.commit();
+
+    ChangeUpdate updateWithSubject = newUpdate(c, changeOwner);
+    updateWithSubject.setSubjectForCommit("Update with subject");
+    updateWithSubject.commit();
+
+    Ref metaRefBefore = repo.exactRef(RefNames.changeMetaRef(c.getId()));
+    RunOptions options = new RunOptions();
+    options.dryRun = false;
+    options.outputDiff = false;
+    options.verifyCommits = false;
+    BackfillResult backfillResult = rewriter.backfillProject(project, repo, options);
+    assertThat(backfillResult.fixedRefDiff.keySet())
+        .containsExactly(RefNames.changeMetaRef(c.getId()));
+    Ref metaRefAfter = repo.exactRef(RefNames.changeMetaRef(c.getId()));
+
+    assertThat(metaRefBefore.getObjectId()).isNotEqualTo(metaRefAfter.getObjectId());
+
+    assertFixedCommits(ImmutableList.of(commitToFix), backfillResult, c.getId());
+
+    List<String> commitHistoryDiff = commitHistoryDiff(backfillResult, c.getId());
+    assertThat(commitHistoryDiff).containsExactly("");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
+  }
+
+  @Test
   public void fixAuthorIdent() throws Exception {
     Change c = newChange();
     Timestamp when = TimeUtil.nowTs();
@@ -164,6 +196,8 @@
 
     assertValidCommits(
         commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
+    assertFixedCommits(ImmutableList.of(invalidUpdateCommit.getId()), result, c.getId());
+
     RevCommit fixedUpdateCommit = commitsAfterRewrite.get(invalidCommitIndex);
     PersonIdent originalAuthorIdent = invalidUpdateCommit.getAuthorIdent();
     PersonIdent fixedAuthorIdent = fixedUpdateCommit.getAuthorIdent();
@@ -177,10 +211,13 @@
     assertThat(invalidUpdateCommit.getCommitterIdent())
         .isEqualTo(fixedUpdateCommit.getCommitterIdent());
     assertThat(fixedUpdateCommit.getFullMessage()).doesNotContain(changeOwner.getName());
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff).hasSize(1);
     assertThat(commitHistoryDiff.get(0)).contains("-author Change Owner <1@gerrit>");
     assertThat(commitHistoryDiff.get(0)).contains("+author Gerrit User 1 <1@gerrit>");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -188,7 +225,7 @@
     Change c = newChange();
 
     String realUserIdentToFix = getAccountIdentToFix(otherUser.getAccount());
-    RevCommit invalidUpdateCommit =
+    ObjectId invalidUpdateCommit =
         writeUpdate(
             RefNames.changeMetaRef(c.getId()),
             getChangeUpdateBody(c, "Comment on behalf of user", "Real-user: " + realUserIdentToFix),
@@ -233,31 +270,42 @@
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(
         commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
+    assertFixedCommits(ImmutableList.of(invalidUpdateCommit), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -9 +9 @@\n"
                 + "-Real-user: Other Account <2@gerrit>\n"
                 + "+Real-user: Gerrit User 2 <2@gerrit>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
   public void fixReviewerFooterIdent() throws Exception {
     Change c = newChange();
     String reviewerIdentToFix = getAccountIdentToFix(otherUser.getAccount());
-    ImmutableList<RevCommit> commitsToFix =
-        new ImmutableList.Builder<RevCommit>()
+    ImmutableList<ObjectId> commitsToFix =
+        new ImmutableList.Builder<ObjectId>()
             .add(
                 writeUpdate(
                     RefNames.changeMetaRef(c.getId()),
+                    // valid change message that should not be overwritten
                     getChangeUpdateBody(
-                        c, /*changeMessage=*/ null, "Reviewer: " + reviewerIdentToFix),
+                        c,
+                        "Removed reviewer <GERRIT_ACCOUNT_1>.",
+                        "Reviewer: " + reviewerIdentToFix),
                     getAuthorIdent(changeOwner.getAccount())))
             .add(
                 writeUpdate(
                     RefNames.changeMetaRef(c.getId()),
-                    getChangeUpdateBody(c, /*changeMessage=*/ null, "CC: " + reviewerIdentToFix),
+                    // valid change message that should not be overwritten
+                    getChangeUpdateBody(
+                        c,
+                        "Removed cc <GERRIT_ACCOUNT_2> with the following votes:\n\n * Code-Review+2 by <GERRIT_ACCOUNT_2>",
+                        "CC: " + reviewerIdentToFix),
                     getAuthorIdent(otherUser.getAccount())))
             .add(
                 writeUpdate(
@@ -299,23 +347,29 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix, result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
-            "@@ -7 +7 @@\n"
+            "@@ -9 +9 @@\n"
                 + "-Reviewer: Other Account <2@gerrit>\n"
                 + "+Reviewer: Gerrit User 2 <2@gerrit>\n",
-            "@@ -7 +7 @@\n" + "-CC: Other Account <2@gerrit>\n" + "+CC: Gerrit User 2 <2@gerrit>\n",
+            "@@ -11 +11 @@\n"
+                + "-CC: Other Account <2@gerrit>\n"
+                + "+CC: Gerrit User 2 <2@gerrit>\n",
             "@@ -9 +9 @@\n"
                 + "-Removed: Other Account <2@gerrit>\n"
                 + "+Removed: Gerrit User 2 <2@gerrit>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
   public void fixReviewerMessage() throws Exception {
     Change c = newChange();
-    ImmutableList.Builder<RevCommit> commitsToFix = new ImmutableList.Builder<>();
+    ImmutableList.Builder<ObjectId> commitsToFix = new ImmutableList.Builder<>();
     ChangeUpdate addReviewerUpdate = newUpdate(c, changeOwner);
     addReviewerUpdate.putReviewer(otherUserId, REVIEWER);
     addReviewerUpdate.commit();
@@ -325,7 +379,7 @@
             RefNames.changeMetaRef(c.getId()),
             getChangeUpdateBody(
                 c,
-                "Removed reviewer " + otherUser.getAccount().fullName(),
+                String.format("Removed reviewer %s.", otherUser.getAccount().fullName()),
                 "Removed: " + getValidIdentAsString(otherUser.getAccount())),
             getAuthorIdent(changeOwner.getAccount())));
 
@@ -338,7 +392,9 @@
             RefNames.changeMetaRef(c.getId()),
             getChangeUpdateBody(
                 c,
-                "Removed cc " + otherUser.getAccount().fullName(),
+                String.format(
+                    "Removed cc %s with the following votes:\n\n * Code-Review+2",
+                    otherUser.getAccount().fullName()),
                 "Removed: " + getValidIdentAsString(otherUser.getAccount())),
             getAuthorIdent(changeOwner.getAccount())));
 
@@ -378,7 +434,9 @@
 
     assertThat(notesBeforeRewrite.getReviewerUpdates()).isEqualTo(expectedReviewerUpdates);
     assertThat(changeMessages(notesBeforeRewrite))
-        .containsExactly("Removed reviewer Other Account", "Removed cc Other Account");
+        .containsExactly(
+            "Removed reviewer Other Account.",
+            "Removed cc Other Account with the following votes:\n\n * Code-Review+2");
     assertThat(notesAfterRewrite.getReviewerUpdates()).isEqualTo(expectedReviewerUpdates);
     assertThat(changeMessages(notesAfterRewrite)).containsExactly("Removed reviewer", "Removed cc");
 
@@ -387,12 +445,58 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix.build(), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
-            "@@ -6 +6 @@\n" + "-Removed reviewer Other Account\n" + "+Removed reviewer\n",
-            "@@ -6 +6 @@\n" + "-Removed cc Other Account\n" + "+Removed cc\n");
+            "@@ -6 +6 @@\n" + "-Removed reviewer Other Account.\n" + "+Removed reviewer\n",
+            "@@ -6,3 +6 @@\n"
+                + "-Removed cc Other Account with the following votes:\n"
+                + "-\n"
+                + "- * Code-Review+2\n"
+                + "+Removed cc\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
+  }
+
+  @Test
+  public void fixReviewerMessageNoReviewerFooter() throws Exception {
+    Change c = newChange();
+
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c, String.format("Removed reviewer %s.", otherUser.getAccount().fullName())),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c,
+            String.format(
+                "Removed cc %s with the following votes:\n\n * Code-Review+2",
+                otherUser.getAccount().fullName())),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    RunOptions options = new RunOptions();
+    options.dryRun = false;
+    BackfillResult result = rewriter.backfillProject(project, repo, options);
+    assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+    assertThat(commitHistoryDiff)
+        .containsExactly(
+            "@@ -6 +6 @@\n" + "-Removed reviewer Other Account.\n" + "+Removed reviewer\n",
+            "@@ -6,3 +6 @@\n"
+                + "-Removed cc Other Account with the following votes:\n"
+                + "-\n"
+                + "- * Code-Review+2\n"
+                + "+Removed cc\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -404,8 +508,8 @@
     approvalUpdateByOtherUser.putApproval(VERIFIED, (short) -1);
     approvalUpdateByOtherUser.commit();
 
-    ImmutableList<RevCommit> commitsToFix =
-        new ImmutableList.Builder<RevCommit>()
+    ImmutableList<ObjectId> commitsToFix =
+        new ImmutableList.Builder<ObjectId>()
             .add(
                 writeUpdate(
                     RefNames.changeMetaRef(c.getId()),
@@ -506,8 +610,9 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix, result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -7,2 +7,2 @@\n"
@@ -523,6 +628,9 @@
             "@@ -7 +7 @@\n"
                 + "-Label: -Verified Change Owner <1@gerrit>\n"
                 + "+Label: -Verified Gerrit User 1 <1@gerrit>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -535,8 +643,8 @@
     approvalUpdateByOtherUser.putApprovalFor(changeOwner.getAccountId(), VERIFIED, (short) -1);
     approvalUpdateByOtherUser.commit();
 
-    ImmutableList<RevCommit> commitsToFix =
-        new ImmutableList.Builder<RevCommit>()
+    ImmutableList<ObjectId> commitsToFix =
+        new ImmutableList.Builder<ObjectId>()
             .add(
                 writeUpdate(
                     RefNames.changeMetaRef(c.getId()),
@@ -626,8 +734,9 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix, result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -6 +6 @@\n"
@@ -642,6 +751,9 @@
             "@@ -6 +6 @@\n"
                 + "-Removed Verified+2 by Change Owner <change@owner.com>\n"
                 + "+Removed Verified+2 by <GERRIT_ACCOUNT_1>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -657,7 +769,7 @@
         RefNames.changeMetaRef(c.getId()),
         getChangeUpdateBody(
             c,
-            /*changeMessage=*/ "Removed Verified+2 by " + changeOwner.getNameEmail(),
+            /*changeMessage=*/ "Removed Verified+2 by " + otherUser.getNameEmail(),
             "Label: -Verified"),
         invalidAuthorIdent);
 
@@ -666,12 +778,123 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+    // Other Account does not applier in any change updates, replaced with default
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -6 +6 @@\n"
-                + "-Removed Verified+2 by Change Owner <change@owner.com>\n"
+                + "-Removed Verified+2 by Other Account <other@account.com>\n"
                 + "+Removed Verified+2 by Gerrit Account\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
+  }
+
+  @Test
+  public void fixRemoveVoteChangeMessageWithNoFooterLabel() throws Exception {
+    Change c = newChange();
+    ChangeUpdate approvalUpdate = newUpdate(c, changeOwner);
+    approvalUpdate.putApproval(VERIFIED, (short) +2);
+
+    approvalUpdate.putApprovalFor(otherUserId, VERIFIED, (short) -1);
+    approvalUpdate.commit();
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+
+        // Even though footer is missing, accounts are matched among the account in change updates.
+        getChangeUpdateBody(c, /*changeMessage=*/ "Removed Verified-1 by Other Account (0002)"),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c, /*changeMessage=*/ "Removed Verified+2 by " + changeOwner.getNameEmail()),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    // No rewrite for default
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(c, /*changeMessage=*/ "Removed Verified+2 by Gerrit Account"),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    RunOptions options = new RunOptions();
+    options.dryRun = false;
+    BackfillResult result = rewriter.backfillProject(project, repo, options);
+    assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+    assertThat(commitHistoryDiff)
+        .containsExactly(
+            "@@ -6 +6 @@\n"
+                + "-Removed Verified-1 by Other Account (0002)\n"
+                + "+Removed Verified-1 by <GERRIT_ACCOUNT_2>\n",
+            "@@ -6 +6 @@\n"
+                + "-Removed Verified+2 by Change Owner <change@owner.com>\n"
+                + "+Removed Verified+2 by <GERRIT_ACCOUNT_1>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
+  }
+
+  @Test
+  public void fixRemoveVotesChangeMessage() throws Exception {
+    Change c = newChange();
+    ChangeUpdate approvalUpdate = newUpdate(c, changeOwner);
+    approvalUpdate.putApproval(VERIFIED, (short) +2);
+
+    approvalUpdate.putApprovalFor(otherUserId, VERIFIED, (short) -1);
+    approvalUpdate.commit();
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+
+        // Even though footer is missing, accounts are matched among the account in change updates.
+        getChangeUpdateBody(
+            c,
+            /*changeMessage=*/ "Removed the following votes:\n"
+                + String.format("* Verified-1 by %s\n", otherUser.getNameEmail())),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c,
+            /*changeMessage=*/ "Removed the following votes:\n"
+                + String.format("* Verified+2 by %s\n", changeOwner.getNameEmail())
+                + String.format("* Verified-1 by %s\n", changeOwner.getNameEmail())
+                + String.format("* Code-Review by %s\n", otherUser.getNameEmail())),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    // No rewrite for default
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c,
+            /*changeMessage=*/ "Removed the following votes:\n"
+                + "* Verified+2 by Gerrit Account\n"
+                + "* Verified-1 by <GERRIT_ACCOUNT_2>\n"),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    RunOptions options = new RunOptions();
+    options.dryRun = false;
+    BackfillResult result = rewriter.backfillProject(project, repo, options);
+    assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+    assertThat(commitHistoryDiff)
+        .containsExactly(
+            "@@ -7 +7 @@\n"
+                + "-* Verified-1 by Other Account <other@account.com>\n"
+                + "+* Verified-1 by <GERRIT_ACCOUNT_2>\n",
+            "@@ -7,3 +7,3 @@\n"
+                + "-* Verified+2 by Change Owner <change@owner.com>\n"
+                + "-* Verified-1 by Change Owner <change@owner.com>\n"
+                + "-* Code-Review by Other Account <other@account.com>\n"
+                + "+* Verified+2 by <GERRIT_ACCOUNT_1>\n"
+                + "+* Verified-1 by <GERRIT_ACCOUNT_1>\n"
+                + "+* Code-Review by <GERRIT_ACCOUNT_2>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -726,8 +949,19 @@
     ChangeUpdate validAttentionSetUpdate = newUpdate(c, changeOwner);
     validAttentionSetUpdate.addToPlannedAttentionSetUpdates(
         AttentionSetUpdate.createForWrite(otherUserId, Operation.REMOVE, "Removed by someone"));
+    validAttentionSetUpdate.addToPlannedAttentionSetUpdates(
+        AttentionSetUpdate.createForWrite(
+            changeOwner.getAccountId(), Operation.ADD, "Added by someone"));
     validAttentionSetUpdate.commit();
 
+    ChangeUpdate invalidRemovedByClickUpdate = newUpdate(c, changeOwner);
+    invalidRemovedByClickUpdate.addToPlannedAttentionSetUpdates(
+        AttentionSetUpdate.createForWrite(
+            changeOwner.getAccountId(),
+            Operation.REMOVE,
+            String.format("Removed by %s by clicking the attention icon", otherUser.getName())));
+    commitsToFix.add(invalidRemovedByClickUpdate.commit());
+
     Ref metaRefBeforeRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
 
     ImmutableList<RevCommit> commitsBeforeRewrite = logMetaRef(repo, metaRefBeforeRewrite);
@@ -747,6 +981,16 @@
     ImmutableList<AttentionSetUpdate> attentionSetUpdatesBeforeRewrite =
         ImmutableList.of(
             AttentionSetUpdate.createFromRead(
+                invalidRemovedByClickUpdate.getWhen().toInstant(),
+                changeOwner.getAccountId(),
+                Operation.REMOVE,
+                String.format("Removed by %s by clicking the attention icon", otherUser.getName())),
+            AttentionSetUpdate.createFromRead(
+                validAttentionSetUpdate.getWhen().toInstant(),
+                changeOwner.getAccountId(),
+                Operation.ADD,
+                "Added by someone"),
+            AttentionSetUpdate.createFromRead(
                 validAttentionSetUpdate.getWhen().toInstant(),
                 otherUserId,
                 Operation.REMOVE,
@@ -780,6 +1024,16 @@
     ImmutableList<AttentionSetUpdate> attentionSetUpdatesAfterRewrite =
         ImmutableList.of(
             AttentionSetUpdate.createFromRead(
+                invalidRemovedByClickUpdate.getWhen().toInstant(),
+                changeOwner.getAccountId(),
+                Operation.REMOVE,
+                "Removed by someone by clicking the attention icon"),
+            AttentionSetUpdate.createFromRead(
+                validAttentionSetUpdate.getWhen().toInstant(),
+                changeOwner.getAccountId(),
+                Operation.ADD,
+                "Added by someone"),
+            AttentionSetUpdate.createFromRead(
                 validAttentionSetUpdate.getWhen().toInstant(),
                 otherUserId,
                 Operation.REMOVE,
@@ -822,9 +1076,10 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix.build(), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
-    assertThat(commitHistoryDiff).hasSize(3);
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+    assertThat(commitHistoryDiff).hasSize(4);
     assertThat(commitHistoryDiff.get(0))
         .isEqualTo(
             "@@ -8 +8 @@\n"
@@ -844,6 +1099,14 @@
             "-Attention: {\"person_ident\":\"Change Owner \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Other Account replied on the change\"}",
             "+Attention: {\"person_ident\":\"Gerrit User 2 \\u003c2@gerrit\\u003e\",\"operation\":\"ADD\",\"reason\":\"Added by someone using the hovercard menu\"}",
             "+Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Someone replied on the change\"}");
+    assertThat(commitHistoryDiff.get(3))
+        .isEqualTo(
+            "@@ -7 +7 @@\n"
+                + "-Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by Other Account by clicking the attention icon\"}\n"
+                + "+Attention: {\"person_ident\":\"Gerrit User 1 \\u003c1@gerrit\\u003e\",\"operation\":\"REMOVE\",\"reason\":\"Removed by someone by clicking the attention icon\"}\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -882,16 +1145,19 @@
               Operation.REMOVE,
               String.format("Removed by %s using the hovercard menu", okAccountName)));
       secondAttentionSetUpdate.commit();
-      ChangeUpdate clearAttentionSetUpdate = newUpdate(c, changeOwner);
-      clearAttentionSetUpdate.addToPlannedAttentionSetUpdates(
-          AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.REMOVE, "Clear"));
-      clearAttentionSetUpdate.commit();
-      attentionSetUpdatesBeforeRewrite.add(
-          AttentionSetUpdate.createFromRead(
-              clearAttentionSetUpdate.getWhen().toInstant(),
+      ChangeUpdate thirdAttentionSetUpdate = newUpdate(c, changeOwner);
+      thirdAttentionSetUpdate.addToPlannedAttentionSetUpdates(
+          AttentionSetUpdate.createForWrite(
               changeOwner.getAccountId(),
               Operation.REMOVE,
-              "Clear"),
+              String.format("Removed by %s by clicking the attention icon", okAccountName)));
+      thirdAttentionSetUpdate.commit();
+      attentionSetUpdatesBeforeRewrite.add(
+          AttentionSetUpdate.createFromRead(
+              thirdAttentionSetUpdate.getWhen().toInstant(),
+              changeOwner.getAccountId(),
+              Operation.REMOVE,
+              String.format("Removed by %s by clicking the attention icon", okAccountName)),
           AttentionSetUpdate.createFromRead(
               secondAttentionSetUpdate.getWhen().toInstant(),
               otherUserId,
@@ -907,7 +1173,6 @@
               otherUserId,
               Operation.ADD,
               String.format("Added by %s using the hovercard menu", okAccountName)));
-      clearAttentionSetUpdate.getNotes();
     }
 
     ChangeNotes notesBeforeRewrite = newNotes(c);
@@ -924,6 +1189,9 @@
     assertThat(notesBeforeRewrite.getMetaId()).isEqualTo(notesAfterRewrite.getMetaId());
     assertThat(metaRefBefore.getObjectId()).isEqualTo(metaRefAfter.getObjectId());
     assertThat(backfillResult.fixedRefDiff).isEmpty();
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -934,19 +1202,19 @@
     invalidMergedMessageUpdate.setChangeMessage(
         "Change has been successfully merged by " + changeOwner.getName());
     invalidMergedMessageUpdate.setTopic("");
-    invalidMergedMessageUpdate.setTag(ChangeMessagesUtil.TAG_MERGED);
+
     commitsToFix.add(invalidMergedMessageUpdate.commit());
     ChangeUpdate invalidCherryPickedMessageUpdate = newUpdate(c, changeOwner);
     invalidCherryPickedMessageUpdate.setChangeMessage(
         "Change has been successfully cherry-picked as e40dc1a50dc7f457a37579e2755374f3e1a5413b by "
             + changeOwner.getName());
-    invalidCherryPickedMessageUpdate.setTag(ChangeMessagesUtil.TAG_MERGED);
+
     commitsToFix.add(invalidCherryPickedMessageUpdate.commit());
     ChangeUpdate invalidRebasedMessageUpdate = newUpdate(c, changeOwner);
     invalidRebasedMessageUpdate.setChangeMessage(
         "Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b by "
             + changeOwner.getName());
-    invalidRebasedMessageUpdate.setTag(ChangeMessagesUtil.TAG_MERGED);
+
     commitsToFix.add(invalidRebasedMessageUpdate.commit());
     ChangeUpdate validSubmitMessageUpdate = newUpdate(c, changeOwner);
     validSubmitMessageUpdate.setChangeMessage(
@@ -988,8 +1256,9 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix.build(), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -6 +6 @@\n"
@@ -1001,6 +1270,9 @@
             "@@ -6 +6 @@\n"
                 + "-Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b by Change Owner\n"
                 + "+Change has been successfully rebased and submitted as e40dc1a50dc7f457a37579e2755374f3e1a5413b\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1032,7 +1304,7 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -1 +1 @@\n"
@@ -1047,6 +1319,9 @@
                 + "@@ -15 +15 @@\n"
                 + "-Submitted-with: OK: Code-Review: Change Owner <1@gerrit>\n"
                 + "+Submitted-with: OK: Code-Review: Gerrit User 1 <1@gerrit>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1108,8 +1383,9 @@
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(
         commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
+    assertFixedCommits(ImmutableList.of(invalidUpdateCommit.getId()), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -10 +10 @@\n"
@@ -1118,6 +1394,9 @@
                 + "@@ -12 +12 @@\n"
                 + "-Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
                 + "+Submitted-with: OK: Verified: Gerrit User 1 <1@gerrit>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1179,8 +1458,9 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix.build(), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -6 +6 @@\n"
@@ -1189,6 +1469,9 @@
             "@@ -6 +6 @@\n"
                 + "-Change message removed by: Change Owner\n"
                 + "+Change message removed\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1288,8 +1571,9 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix.build(), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -6 +6 @@\n"
@@ -1307,6 +1591,9 @@
             "@@ -6 +6 @@\n"
                 + "-Reviewer User who was added as reviewer owns the following files:\n"
                 + "+Gerrit Account, who was added as reviewer owns the following files:\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1315,30 +1602,51 @@
     Change c = newChange();
     ImmutableList.Builder<ObjectId> commitsToFix = new ImmutableList.Builder<>();
 
-    ChangeUpdate invalidOnApprovalUpdate = newUpdate(c, changeOwner);
-    invalidOnApprovalUpdate.setChangeMessage(
-        "Patch Set 1: Code-Review+2\n\n"
+    ChangeUpdate invalidOnReviewUpdate = newUpdate(c, changeOwner);
+    invalidOnReviewUpdate.setChangeMessage(
+        "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
+            + "By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
+            + "   * file1.java\n"
+            + "   * file2.ts\n"
+            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
+            + "By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n");
+    commitsToFix.add(invalidOnReviewUpdate.commit());
+
+    ChangeUpdate invalidOnReviewUpdateAnyOrder = newUpdate(c, changeOwner);
+    invalidOnReviewUpdateAnyOrder.setChangeMessage(
+        "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
+            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
+            + "By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n"
             + "By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
             + "   * file1.java\n"
             + "   * file2.ts\n");
-    commitsToFix.add(invalidOnApprovalUpdate.commit());
-    ChangeUpdate invalidOnApprovalMultipleByUpdate = newUpdate(c, otherUser);
-    invalidOnApprovalMultipleByUpdate.setChangeMessage(
+    commitsToFix.add(invalidOnReviewUpdateAnyOrder.commit());
+    ChangeUpdate invalidOnApprovalUpdate = newUpdate(c, otherUser);
+    invalidOnApprovalUpdate.setChangeMessage(
         "Patch Set 1: -Code-Review\n\n"
             + "By removing the Code-Review+2 vote the following files are no longer explicitly code-owner approved by Other Account:\n"
             + "   * file1.java\n"
             + "   * file2.ts\n"
             + "\nThe listed files are still implicitly approved by Other Account.\n");
-    commitsToFix.add(invalidOnApprovalMultipleByUpdate.commit());
+    commitsToFix.add(invalidOnApprovalUpdate.commit());
 
     ChangeUpdate invalidOnOverrideUpdate = newUpdate(c, changeOwner);
     invalidOnOverrideUpdate.setChangeMessage(
         "Patch Set 1: -Owners-Override\n\n"
             + "(1 comment)\n\n"
-            + "By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by Change Owner:\n"
-            + "   * file3.java\n");
+            + "By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by Change Owner\n");
+
     commitsToFix.add(invalidOnOverrideUpdate.commit());
 
+    ChangeUpdate partiallyValidOnReviewUpdate = newUpdate(c, changeOwner);
+    partiallyValidOnReviewUpdate.setChangeMessage(
+        "Patch Set 1: Any-Label+2 Code-Review+2\n\n"
+            + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+            + "   * file1.java\n"
+            + "   * file2.ts\n"
+            + "By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n");
+    commitsToFix.add(partiallyValidOnReviewUpdate.commit());
+
     ChangeUpdate validOnApprovalUpdate = newUpdate(c, changeOwner);
     validOnApprovalUpdate.setChangeMessage(
         "Patch Set 1: Code-Review-2\n\n"
@@ -1349,8 +1657,7 @@
     ChangeUpdate validOnOverrideUpdate = newUpdate(c, changeOwner);
     validOnOverrideUpdate.setChangeMessage(
         "Patch Set 1: Owners-Override+1\n\n"
-            + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>:\n"
-            + "   * file1.java\n");
+            + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n");
     validOnOverrideUpdate.commit();
 
     Ref metaRefBeforeRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
@@ -1370,11 +1677,18 @@
 
     ChangeNotes notesAfterRewrite = newNotes(c);
 
-    assertThat(changeMessages(notesBeforeRewrite)).hasSize(5);
+    assertThat(changeMessages(notesBeforeRewrite)).hasSize(7);
     assertThat(changeMessages(notesAfterRewrite))
         .containsExactly(
-            "Patch Set 1: Code-Review+2\n"
-                + "\n"
+            "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
+                + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "   * file1.java\n"
+                + "   * file2.ts\n"
+                + "By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
+                + "By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n",
+            "Patch Set 1: Any-Label+2 Other-Label+2 Code-Review+2\n\n"
+                + "By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
+                + "By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n"
                 + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
                 + "   * file1.java\n"
                 + "   * file2.ts\n",
@@ -1388,27 +1702,43 @@
                 + "\n"
                 + "(1 comment)\n"
                 + "\n"
-                + "By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by <GERRIT_ACCOUNT_1>:\n"
-                + "   * file3.java\n",
+                + "By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by <GERRIT_ACCOUNT_1>\n",
+            "Patch Set 1: Any-Label+2 Code-Review+2\n\n"
+                + "By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "   * file1.java\n"
+                + "   * file2.ts\n"
+                + "By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n",
             "Patch Set 1: Code-Review-2\n\n"
                 + "By voting Code-Review-2 the following files are no longer explicitly code-owner approved by <GERRIT_ACCOUNT_1>:\n"
                 + "   * file4.java\n",
             "Patch Set 1: Owners-Override+1\n"
                 + "\n"
-                + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>:\n"
-                + "   * file1.java\n");
+                + "By voting Owners-Override+1 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n");
 
     Ref metaRefAfterRewrite = repo.exactRef(RefNames.changeMetaRef(c.getId()));
     assertThat(metaRefAfterRewrite.getObjectId()).isNotEqualTo(metaRefBeforeRewrite.getObjectId());
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
+    assertFixedCommits(commitsToFix.build(), result, c.getId());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -8 +8 @@\n"
                 + "-By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
+                + "+By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n"
+                + "@@ -11,2 +11,2 @@\n"
+                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
+                + "-By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n"
+                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
+                + "+By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n",
+            "@@ -8,3 +8,3 @@\n"
+                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
+                + "-By voting Other-Label+2 the code-owners submit requirement is still overridden by Change Owner\n"
+                + "-By voting Code-Review+2 the following files are now code-owner approved by Change Owner:\n"
+                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n"
+                + "+By voting Other-Label+2 the code-owners submit requirement is still overridden by <GERRIT_ACCOUNT_1>\n"
                 + "+By voting Code-Review+2 the following files are now code-owner approved by <GERRIT_ACCOUNT_1>:\n",
             "@@ -8 +8 @@\n"
                 + "-By removing the Code-Review+2 vote the following files are no longer explicitly code-owner approved by Other Account:\n"
@@ -1417,8 +1747,14 @@
                 + "-The listed files are still implicitly approved by Other Account.\n"
                 + "+The listed files are still implicitly approved by <GERRIT_ACCOUNT_2>.\n",
             "@@ -10 +10 @@\n"
-                + "-By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by Change Owner:\n"
-                + "+By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by <GERRIT_ACCOUNT_1>:\n");
+                + "-By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by Change Owner\n"
+                + "+By removing the Owners-Override+1 vote the code-owners submit requirement is no longer overridden by <GERRIT_ACCOUNT_1>\n",
+            "@@ -11 +11 @@\n"
+                + "-By voting Any-Label+2 the code-owners submit requirement is overridden by Change Owner\n"
+                + "+By voting Any-Label+2 the code-owners submit requirement is overridden by <GERRIT_ACCOUNT_1>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1466,6 +1802,7 @@
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(
         commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
+    assertFixedCommits(ImmutableList.of(invalidUpdateCommit.getId()), result, c.getId());
 
     RevCommit fixedUpdateCommit = commitsAfterRewrite.get(invalidCommitIndex);
     assertThat(invalidUpdateCommit.getAuthorIdent()).isEqualTo(fixedUpdateCommit.getAuthorIdent());
@@ -1478,20 +1815,23 @@
     String expectedFixedIdent = getValidIdentAsString(changeOwner.getAccount());
     assertThat(fixedUpdateCommit.getFullMessage()).contains(expectedFixedIdent);
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -9 +9 @@\n"
                 + "-Assignee: Change Owner <1@gerrit>\n"
                 + "+Assignee: Gerrit User 1 <1@gerrit>\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
   public void fixAssigneeChangeMessage() throws Exception {
     Change c = newChange();
 
-    ImmutableList<RevCommit> commitsToFix =
-        new ImmutableList.Builder<RevCommit>()
+    ImmutableList<ObjectId> commitsToFix =
+        new ImmutableList.Builder<ObjectId>()
             .add(
                 writeUpdate(
                     RefNames.changeMetaRef(c.getId()),
@@ -1561,7 +1901,9 @@
 
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(commitsBeforeRewrite, commitsAfterRewrite, invalidCommits);
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    assertFixedCommits(commitsToFix, result, c.getId());
+
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff)
         .containsExactly(
             "@@ -6 +6 @@\n"
@@ -1577,6 +1919,70 @@
                 + "@@ -9 +9 @@\n"
                 + "-Assignee:\n"
                 + "+Assignee: \n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
+  }
+
+  @Test
+  public void fixAssigneeChangeMessageNoAssigneeFooter() throws Exception {
+    Change c = newChange();
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(c, "Assignee added: " + changeOwner.getName()),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(
+            c,
+            String.format(
+                "Assignee changed from: %s to: %s", changeOwner.getName(), otherUser.getName())),
+        getAuthorIdent(otherUser.getAccount()));
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(c, "Assignee deleted: " + otherUser.getName()),
+        getAuthorIdent(changeOwner.getAccount()));
+    Account reviewer =
+        Account.builder(Account.id(3), TimeUtil.nowTs())
+            .setFullName("Reviewer User")
+            .setPreferredEmail("reviewer@account.com")
+            .build();
+    accountCache.put(reviewer);
+    // Even though account is present in the cache, it won't be used because it does not appear in
+    // the history of this change.
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(c, "Assignee added: " + reviewer.getName()),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    writeUpdate(
+        RefNames.changeMetaRef(c.getId()),
+        getChangeUpdateBody(c, "Assignee deleted: Gerrit Account"),
+        getAuthorIdent(changeOwner.getAccount()));
+
+    RunOptions options = new RunOptions();
+    options.dryRun = false;
+    BackfillResult result = rewriter.backfillProject(project, repo, options);
+    assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
+    assertThat(commitHistoryDiff)
+        .containsExactly(
+            "@@ -6 +6 @@\n"
+                + "-Assignee added: Change Owner\n"
+                + "+Assignee added: <GERRIT_ACCOUNT_1>\n",
+            "@@ -6 +6 @@\n"
+                + "-Assignee changed from: Change Owner to: Other Account\n"
+                + "+Assignee changed from: <GERRIT_ACCOUNT_1> to: <GERRIT_ACCOUNT_2>\n",
+            "@@ -6 +6 @@\n"
+                + "-Assignee deleted: Other Account\n"
+                + "+Assignee deleted: <GERRIT_ACCOUNT_2>\n",
+            "@@ -6 +6 @@\n"
+                + "-Assignee added: Reviewer User\n"
+                + "+Assignee added: Gerrit Account\n");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   @Test
@@ -1621,6 +2027,7 @@
     ImmutableList<RevCommit> commitsAfterRewrite = logMetaRef(repo, metaRefAfterRewrite);
     assertValidCommits(
         commitsBeforeRewrite, commitsAfterRewrite, ImmutableList.of(invalidCommitIndex));
+    assertFixedCommits(ImmutableList.of(invalidUpdateCommit.getId()), result, c.getId());
 
     RevCommit fixedUpdateCommit = commitsAfterRewrite.get(invalidCommitIndex);
     assertThat(invalidUpdateCommit.getAuthorIdent())
@@ -1629,7 +2036,7 @@
     assertThat(fixedUpdateCommit.getFullMessage()).doesNotContain(changeOwner.getName());
     assertThat(fixedUpdateCommit.getFullMessage()).doesNotContain(otherUser.getName());
 
-    List<String> commitHistoryDiff = result.fixedRefDiff.get(RefNames.changeMetaRef(c.getId()));
+    List<String> commitHistoryDiff = commitHistoryDiff(result, c.getId());
     assertThat(commitHistoryDiff).hasSize(1);
     assertThat(commitHistoryDiff.get(0)).contains("-author Change Owner <1@gerrit>");
     assertThat(commitHistoryDiff.get(0)).contains("+author Gerrit User 1 <1@gerrit>");
@@ -1641,6 +2048,9 @@
                 + "@@ -9 +9 @@\n"
                 + "-Assignee: Other Account <2@gerrit>\n"
                 + "+Assignee: Gerrit User 2 <2@gerrit>");
+    BackfillResult secondRunResult = rewriter.backfillProject(project, repo, options);
+    assertThat(secondRunResult.fixedRefDiff.keySet()).isEmpty();
+    assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
   private RevCommit writeUpdate(String metaRef, String body, PersonIdent author) throws Exception {
@@ -1702,6 +2112,15 @@
     }
   }
 
+  private void assertFixedCommits(
+      ImmutableList<ObjectId> expectedFixedCommits, BackfillResult result, Change.Id changeId) {
+    assertThat(expectedFixedCommits)
+        .containsExactlyElementsIn(
+            result.fixedRefDiff.get(RefNames.changeMetaRef(changeId)).stream()
+                .map(CommitDiff::oldSha1)
+                .collect(toImmutableList()));
+  }
+
   private String getAccountIdentToFix(Account account) {
     return String.format("%s <%s>", account.getName(), account.id().get() + "@" + serverId);
   }
@@ -1718,6 +2137,12 @@
         .collect(toImmutableList());
   }
 
+  private ImmutableList<String> commitHistoryDiff(BackfillResult result, Change.Id changeId) {
+    return result.fixedRefDiff.get(RefNames.changeMetaRef(changeId)).stream()
+        .map(CommitDiff::diff)
+        .collect(toImmutableList());
+  }
+
   private PersonIdent getAuthorIdent(Account account) {
     Timestamp when = TimeUtil.nowTs();
     return changeNoteUtil.newAccountIdIdent(account.id(), when, serverIdent);
diff --git a/javatests/com/google/gerrit/server/permissions/SectionSortCacheTest.java b/javatests/com/google/gerrit/server/permissions/SectionSortCacheTest.java
index 9ec1625..0ba3b56 100644
--- a/javatests/com/google/gerrit/server/permissions/SectionSortCacheTest.java
+++ b/javatests/com/google/gerrit/server/permissions/SectionSortCacheTest.java
@@ -78,7 +78,7 @@
     // Cache preserves relative order (reference equality) for identical elements
     AccessSection[] expected = {sectionBClone, sectionB, sectionA, sectionAClone, sectionA};
     for (int i = 0; i < sorted.size(); i++) {
-      assert (sorted.get(i) == expected[i]);
+      assertThat(sorted.get(i)).isSameInstanceAs(expected[i]);
     }
   }
 }
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index 1035fe7..5daf68e 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -64,6 +64,7 @@
   @Inject protected AllProjectsName allProjects;
   @Inject private CommitsCollection commits;
   @Inject private ProjectOperations projectOperations;
+  @Inject private AuthRequest.Factory authRequestFactory;
 
   private TestRepository<InMemoryRepository> repo;
   private Project.NameKey project;
@@ -72,7 +73,8 @@
   public void setUp() throws Exception {
     setUpPermissions();
 
-    Account.Id user = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+    Account.Id user =
+        accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
     testEnvironment.setApiUser(user);
     project = projectOperations.newProject().create();
     repo = new TestRepository<>(repoManager.openRepository(project));
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 9130d3e..7f0b685 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -68,7 +69,10 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
+@RunWith(JUnit4.class)
 public class ProjectConfigTest {
   private static final String LABEL_SCORES_CONFIG =
       "  copyAnyScore = "
@@ -113,7 +117,8 @@
   public void setUp() throws Exception {
     sitePaths = new SitePaths(temporaryFolder.newFolder().toPath());
     Files.createDirectories(sitePaths.etc_dir);
-    factory = new ProjectConfig.Factory(sitePaths, ALL_PROJECTS);
+    factory =
+        new ProjectConfig.Factory(ALL_PROJECTS, new FileBasedAllProjectsConfigProvider(sitePaths));
     db = new InMemoryRepository(new DfsRepositoryDescription("repo"));
     tr = new TestRepository<>(db);
   }
@@ -327,8 +332,8 @@
     assertThat(cfg.getValidationErrors()).hasSize(1);
     assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
         .isEqualTo(
-            "project.config: Submit requirement \"code-review\" does not define a submittability expression."
-                + " Skipping this requirement.");
+            "project.config: Submit requirement \"code-review\" does not define a submittability"
+                + " expression. Skipping this requirement.");
   }
 
   @Test
@@ -866,7 +871,9 @@
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
         .isEqualTo(
-            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+            "[commentlink \"bugzilla\"]\n"
+                + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
   }
 
   @Test
@@ -891,7 +898,9 @@
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
         .isEqualTo(
-            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+            "[commentlink \"bugzilla\"]\n"
+                + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
   }
 
   @Test
@@ -913,7 +922,9 @@
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
         .isEqualTo(
-            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+            "[commentlink \"bugzilla\"]\n"
+                + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
   }
 
   @Test
@@ -978,7 +989,9 @@
     rev = commit(cfg);
     assertThat(text(rev, "project.config"))
         .isEqualTo(
-            "[commentlink \"bugzilla\"]\n\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
+            "[commentlink \"bugzilla\"]\n"
+                + "\tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n"
+                + "\tlink = http://bugs.example.com/show_bug.cgi?id=$2\n");
   }
 
   @Test
@@ -987,8 +1000,11 @@
     Path tmp = Files.createTempFile("gerrit_test_", "_site");
     Files.deleteIfExists(tmp);
     SitePaths sitePaths = new SitePaths(tmp);
+    FileBasedAllProjectsConfigProvider allProjectsConfigProvider =
+        new FileBasedAllProjectsConfigProvider(sitePaths);
     byte[] hashedContents =
-        ProjectCacheImpl.allProjectsFileProjectConfigHash(ALL_PROJECTS, sitePaths);
+        ProjectCacheImpl.allProjectsFileProjectConfigHash(
+            allProjectsConfigProvider.get(ALL_PROJECTS));
     assertThat(hashedContents).isEqualTo(new byte[16]); // Empty/absent config
     FileBasedConfig fileBasedConfig =
         new FileBasedConfig(
@@ -1000,7 +1016,9 @@
             FS.DETECTED);
     fileBasedConfig.setString("plugin", "my-plugin", "key", "value");
     fileBasedConfig.save();
-    hashedContents = ProjectCacheImpl.allProjectsFileProjectConfigHash(ALL_PROJECTS, sitePaths);
+    hashedContents =
+        ProjectCacheImpl.allProjectsFileProjectConfigHash(
+            allProjectsConfigProvider.get(ALL_PROJECTS));
     assertThat(hashedContents)
         .isEqualTo(
             new byte[] {
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
new file mode 100644
index 0000000..05eb6e0
--- /dev/null
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
@@ -0,0 +1,317 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.LabelFunction;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRecord.Label;
+import com.google.gerrit.entities.SubmitRecord.Status;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import java.util.Arrays;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SubmitRequirementsAdapterTest {
+  private List<LabelType> labelTypes;
+
+  private static final ObjectId psCommitId = ObjectId.zeroId();
+
+  @Before
+  public void setup() {
+    LabelType codeReview =
+        LabelType.builder(
+                "Code-Review",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Looks good to me"),
+                    LabelValue.create((short) 0, "No score"),
+                    LabelValue.create((short) -1, "I would prefer this is not merged as is")))
+            .setFunction(LabelFunction.MAX_WITH_BLOCK)
+            .build();
+
+    LabelType verified =
+        LabelType.builder(
+                "Verified",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Looks good to me"),
+                    LabelValue.create((short) 0, "No score"),
+                    LabelValue.create((short) -1, "I would prefer this is not merged as is")))
+            .setFunction(LabelFunction.MAX_NO_BLOCK)
+            .build();
+
+    LabelType codeStyle =
+        LabelType.builder(
+                "Code-Style",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Looks good to me"),
+                    LabelValue.create((short) 0, "No score"),
+                    LabelValue.create((short) -1, "I would prefer this is not merged as is")))
+            .setFunction(LabelFunction.ANY_WITH_BLOCK)
+            .build();
+
+    LabelType ignoreSelfApprovalLabel =
+        LabelType.builder(
+                "ISA-Label",
+                ImmutableList.of(
+                    LabelValue.create((short) 1, "Looks good to me"),
+                    LabelValue.create((short) 0, "No score"),
+                    LabelValue.create((short) -1, "I would prefer this is not merged as is")))
+            .setFunction(LabelFunction.MAX_WITH_BLOCK)
+            .setIgnoreSelfApproval(true)
+            .build();
+
+    labelTypes = Arrays.asList(codeReview, verified, codeStyle, ignoreSelfApprovalLabel);
+  }
+
+  @Test
+  public void defaultSubmitRule_withLabelsAllPass() {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~DefaultSubmitRule",
+            Status.OK,
+            Arrays.asList(
+                createLabel("Code-Review", Label.Status.OK),
+                createLabel("Verified", Label.Status.OK)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(2);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "Code-Review",
+        /* submitExpression= */ "label:Code-Review=MAX -label:Code-Review=MIN",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+    assertResult(
+        requirements.get(1),
+        /* reqName= */ "Verified",
+        /* submitExpression= */ "label:Verified=MAX",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+  }
+
+  @Test
+  public void defaultSubmitRule_withLabelsAllNeed() {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~DefaultSubmitRule",
+            Status.OK,
+            Arrays.asList(
+                createLabel("Code-Review", Label.Status.NEED),
+                createLabel("Verified", Label.Status.NEED)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(2);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "Code-Review",
+        /* submitExpression= */ "label:Code-Review=MAX -label:Code-Review=MIN",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+    assertResult(
+        requirements.get(1),
+        /* reqName= */ "Verified",
+        /* submitExpression= */ "label:Verified=MAX",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+  }
+
+  @Test
+  public void defaultSubmitRule_withLabelStatusNeed_labelHasIgnoreSelfApproval() throws Exception {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~DefaultSubmitRule",
+            Status.NOT_READY,
+            Arrays.asList(createLabel("ISA-Label", Label.Status.NEED)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "ISA-Label",
+        /* submitExpression= */ "label:ISA-Label=MAX,user=non_uploader -label:ISA-Label=MIN",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+  }
+
+  @Test
+  public void defaultSubmitRule_withLabelStatusOk_labelHasIgnoreSelfApproval() throws Exception {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~DefaultSubmitRule",
+            Status.OK,
+            Arrays.asList(createLabel("ISA-Label", Label.Status.OK)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "ISA-Label",
+        /* submitExpression= */ "label:ISA-Label=MAX,user=non_uploader -label:ISA-Label=MIN",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+  }
+
+  @Test
+  public void customSubmitRule_noLabels_withStatusOk() {
+    SubmitRecord submitRecord =
+        createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, Arrays.asList());
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "gerrit~IgnoreSelfApprovalRule",
+        /* submitExpression= */ "rule:gerrit~IgnoreSelfApprovalRule",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+  }
+
+  @Test
+  public void customSubmitRule_nullLabels_withStatusOk() {
+    SubmitRecord submitRecord =
+        createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, /* labels= */ null);
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "gerrit~IgnoreSelfApprovalRule",
+        /* submitExpression= */ "rule:gerrit~IgnoreSelfApprovalRule",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+  }
+
+  @Test
+  public void customSubmitRule_noLabels_withStatusNotReady() {
+    SubmitRecord submitRecord =
+        createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.NOT_READY, Arrays.asList());
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "gerrit~IgnoreSelfApprovalRule",
+        /* submitExpression= */ "rule:gerrit~IgnoreSelfApprovalRule",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+  }
+
+  @Test
+  public void customSubmitRule_withLabels() {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~PrologRule",
+            Status.NOT_READY,
+            Arrays.asList(
+                createLabel("custom-label-1", Label.Status.NEED),
+                createLabel("custom-label-2", Label.Status.REJECT)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(2);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "custom-label-1",
+        /* submitExpression= */ "label:custom-label-1=gerrit~PrologRule",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+    assertResult(
+        requirements.get(1),
+        /* reqName= */ "custom-label-2",
+        /* submitExpression= */ "label:custom-label-2=gerrit~PrologRule",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+  }
+
+  @Test
+  public void customSubmitRule_withMixOfPassingAndFailingLabels() {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~PrologRule",
+            Status.NOT_READY,
+            Arrays.asList(
+                createLabel("custom-label-1", Label.Status.OK),
+                createLabel("custom-label-2", Label.Status.REJECT)));
+
+    List<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+
+    assertThat(requirements).hasSize(2);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "custom-label-1",
+        /* submitExpression= */ "label:custom-label-1=gerrit~PrologRule",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+    assertResult(
+        requirements.get(1),
+        /* reqName= */ "custom-label-2",
+        /* submitExpression= */ "label:custom-label-2=gerrit~PrologRule",
+        SubmitRequirementResult.Status.UNSATISFIED,
+        SubmitRequirementExpressionResult.Status.FAIL);
+  }
+
+  private void assertResult(
+      SubmitRequirementResult r,
+      String reqName,
+      String submitExpression,
+      SubmitRequirementResult.Status status,
+      SubmitRequirementExpressionResult.Status expressionStatus) {
+    assertThat(r.submitRequirement().name()).isEqualTo(reqName);
+    assertThat(r.submitRequirement().submittabilityExpression().expressionString())
+        .isEqualTo(submitExpression);
+    assertThat(r.status()).isEqualTo(status);
+    assertThat(r.submittabilityExpressionResult().status()).isEqualTo(expressionStatus);
+  }
+
+  private SubmitRecord createSubmitRecord(
+      String ruleName, SubmitRecord.Status status, @Nullable List<Label> labels) {
+    SubmitRecord record = new SubmitRecord();
+    record.ruleName = ruleName;
+    record.status = status;
+    record.labels = labels;
+    return record;
+  }
+
+  private Label createLabel(String name, Label.Status status) {
+    Label label = new Label();
+    label.label = name;
+    label.status = status;
+    return label;
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index b7be40b..16f7199 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -65,6 +65,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -139,6 +140,10 @@
 
   @Inject protected ExternalIds externalIds;
 
+  @Inject private ExternalIdKeyFactory externalIdKeyFactory;
+
+  @Inject protected AuthRequest.Factory authRequestFactory;
+
   protected LifecycleManager lifecycle;
   protected Injector injector;
   protected AccountInfo currentUserInfo;
@@ -659,7 +664,7 @@
     List<AccountExternalIdInfo> externalIdInfos = gApi.accounts().self().getExternalIds();
     List<ByteArrayWrapper> blobs = new ArrayList<>();
     for (AccountExternalIdInfo info : externalIdInfos) {
-      Optional<ExternalId> extId = externalIds.get(ExternalId.Key.parse(info.identity));
+      Optional<ExternalId> extId = externalIds.get(externalIdKeyFactory.parse(info.identity));
       assertThat(extId).isPresent();
       blobs.add(new ByteArrayWrapper(extId.get().toByteArray()));
     }
@@ -772,9 +777,10 @@
   private Account.Id createAccount(String username, String fullName, String email, boolean active)
       throws Exception {
     try (ManualRequestContext ctx = oneOffRequestContext.open()) {
-      Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+      Account.Id id =
+          accountManager.authenticate(authRequestFactory.createForUser(username)).getAccountId();
       if (email != null) {
-        accountManager.link(id, AuthRequest.forEmail(email));
+        accountManager.link(id, authRequestFactory.createForEmail(email));
       }
       accountsUpdate
           .get()
@@ -791,7 +797,7 @@
   private void addEmails(AccountInfo account, String... emails) throws Exception {
     Account.Id id = Account.id(account._accountId);
     for (String email : emails) {
-      accountManager.link(id, AuthRequest.forEmail(email));
+      accountManager.link(id, authRequestFactory.createForEmail(email));
     }
     accountIndexer.index(id);
   }
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1f29f45..9ebee9c 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -42,6 +42,9 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
 import com.google.common.truth.ThrowableSubject;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
+import com.google.gerrit.acceptance.FakeSubmitRule;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
@@ -68,7 +71,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
-import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -103,7 +105,7 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.VersionedAccountQueries;
-import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -137,7 +139,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -174,6 +175,7 @@
   @Inject protected IdentifiedUser.GenericFactory userFactory;
   @Inject protected ChangeIndexCollection indexes;
   @Inject protected ChangeIndexer indexer;
+  @Inject protected ExtensionRegistry extensionRegistry;
   @Inject protected IndexConfig indexConfig;
   @Inject protected InMemoryRepositoryManager repoManager;
   @Inject protected Provider<AnonymousUser> anonymousUserProvider;
@@ -190,6 +192,8 @@
   @Inject protected ProjectCache projectCache;
   @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
   @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
+  @Inject protected AuthRequest.Factory authRequestFactory;
+  @Inject protected ExternalIdFactory externalIdFactory;
 
   @Inject private ProjectConfig.Factory projectConfigFactory;
   @Inject private ProjectOperations projectOperations;
@@ -224,14 +228,16 @@
   protected void setUpDatabase() throws Exception {
     schemaCreator.create();
 
-    userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+    userId = accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
     String email = "user@example.com";
     accountsUpdate
         .get()
         .update(
             "Add Email",
             userId,
-            u -> u.addExternalId(ExternalId.createEmail(userId, email)).setPreferredEmail(email));
+            u ->
+                u.addExternalId(externalIdFactory.createEmail(userId, email))
+                    .setPreferredEmail(email));
     resetUser();
   }
 
@@ -415,7 +421,7 @@
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo), userId);
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     Change change2 = insert(repo, newChange(repo), user2);
 
     // No private changes.
@@ -583,7 +589,7 @@
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo), userId);
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     Change change2 = insert(repo, newChange(repo), user2);
 
     assertQuery("is:owner", change1);
@@ -621,9 +627,14 @@
     PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
     PersonIdent john = new PersonIdent("John", "john@example.com");
     PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
+    Account ua = user.asIdentifiedUser().getAccount();
+    PersonIdent myself = new PersonIdent("I Am", ua.preferredEmail());
+    PersonIdent selfName = new PersonIdent("My Self", "my.self@example.com");
+
     Change change1 = createChange(repo, johnDoe);
     Change change2 = createChange(repo, john);
     Change change3 = createChange(repo, doeSmith);
+    createChange(repo, selfName);
 
     // Only email address.
     assertQuery(searchOperator + "john.doe@example.com", change1);
@@ -639,6 +650,18 @@
     assertQuery(searchOperator + "\"John <john.doe@example.com>\"");
     assertQuery(searchOperator + "\"Doe John <john@example.com>\"");
     assertQuery(searchOperator + "\"Doe John <doe_smith@example.com>\"");
+
+    // Partial name
+    assertQuery(searchOperator + "ohn");
+    assertQuery(searchOperator + "smith", change3);
+
+    // The string 'self' in the name should not be matched
+    assertQuery(searchOperator + "self");
+
+    // ':self' matches a change created with the current user's email address
+    Change change5 = createChange(repo, myself);
+    assertQuery(searchOperator + "me", change5);
+    assertQuery(searchOperator + "self", change5);
   }
 
   private void byAuthorOrCommitterFullText(String searchOperator) throws Exception {
@@ -680,7 +703,7 @@
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo), userId);
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     Change change2 = insert(repo, newChange(repo), user2);
     Change change3 = insert(repo, newChange(repo), user2);
     gApi.changes().id(change3.getId().get()).current().review(ReviewInput.approve());
@@ -970,7 +993,7 @@
 
   @Test
   public void byLabel() throws Exception {
-    accountManager.authenticate(AuthRequest.forUser("anotheruser"));
+    accountManager.authenticate(authRequestFactory.createForUser("anotheruser"));
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo);
     ChangeInserter ins2 = newChange(repo);
@@ -1143,6 +1166,26 @@
     assertQuery("label:Code-Review=+1,owner");
   }
 
+  @Test
+  public void byLabelNonUploader() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    ChangeInserter ins = newChange(repo);
+    Account.Id user1 = createAccount("user1");
+
+    // create a change with "user"
+    Change reviewPlus1Change = insert(repo, ins);
+
+    // add a +1 vote with "user". Query doesn't match since voter is the uploader.
+    gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+    assertQuery("label:Code-Review=+1,user=non_uploader");
+
+    // add a +1 vote with "user1". Query will match since voter is a non-uploader.
+    requestContext.setContext(newRequestContext(user1));
+    gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+    assertQuery("label:Code-Review=+1,user=non_uploader", reviewPlus1Change);
+    assertQuery("label:Code-Review=+1,non_uploader", reviewPlus1Change);
+  }
+
   private Change[] codeReviewInRange(Map<Integer, Change> changes, int start, int end) {
     int size = 0;
     Change[] range = new Change[end - start + 1];
@@ -1164,7 +1207,7 @@
   }
 
   private Account.Id createAccount(String name) throws Exception {
-    return accountManager.authenticate(AuthRequest.forUser(name)).getAccountId();
+    return accountManager.authenticate(authRequestFactory.createForUser(name)).getAccountId();
   }
 
   @Test
@@ -1324,7 +1367,7 @@
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo), userId);
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     for (int i = 0; i < 5; i++) {
       insert(repo, newChange(repo), user2);
     }
@@ -1337,7 +1380,7 @@
   public void filterOutAllResults() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     for (int i = 0; i < 5; i++) {
       insert(repo, newChange(repo), user2);
     }
@@ -2123,11 +2166,11 @@
     Account.Id user3 = createAccount("user3");
 
     // Explicitly authenticate user2 and user3 so that display name gets set
-    AuthRequest authRequest = AuthRequest.forUser("user2");
+    AuthRequest authRequest = authRequestFactory.createForUser("user2");
     authRequest.setDisplayName("Another User");
     authRequest.setEmailAddress("user2@example.com");
     accountManager.authenticate(authRequest);
-    authRequest = AuthRequest.forUser("user3");
+    authRequest = authRequestFactory.createForUser("user3");
     authRequest.setDisplayName("Another User");
     authRequest.setEmailAddress("user3@example.com");
     accountManager.authenticate(authRequest);
@@ -2198,7 +2241,10 @@
     Change change2 = insert(repo, newChange(repo));
 
     int user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId().get();
+        accountManager
+            .authenticate(authRequestFactory.createForUser("anotheruser"))
+            .getAccountId()
+            .get();
 
     ReviewInput input = new ReviewInput();
     input.message = "toplevel";
@@ -2217,7 +2263,49 @@
   }
 
   @Test
-  public void byDraftBy() throws Exception {
+  public void bySubmitRuleResult() throws Exception {
+    if (getSchemaVersion() < 68) {
+      assertMissingField(ChangeField.SUBMIT_RULE_RESULT);
+      return;
+    }
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
+      TestRepository<Repo> repo = createProject("repo");
+      Change change = insert(repo, newChange(repo));
+      assertQuery("rule:gerrit~FakeSubmitRule");
+
+      // FakeSubmitRule returns true if change has one or more hashtags.
+      HashtagsInput hashtag = new HashtagsInput();
+      hashtag.add = ImmutableSet.of("Tag1");
+      gApi.changes().id(change.getId().get()).setHashtags(hashtag);
+      assertQuery("rule:gerrit~FakeSubmitRule", change);
+      assertQuery("rule:gerrit~FakeSubmitRule=OK", change);
+      assertQuery("rule:gerrit~FakeSubmitRule=NOT_READY");
+
+      // The 'gerrit~' prefix can be omitted for core submit rules
+      assertQuery("rule:FakeSubmitRule", change);
+    }
+  }
+
+  @Test
+  public void byNonExistingSubmitRule_returnsEmpty() throws Exception {
+    // Some submit rules could be removed from the gerrit.config but there can be records for
+    // merged changes in NoteDb for these rules. We allow querying for non-existent rules to handle
+    // this case.
+    if (getSchemaVersion() < 68) {
+      assertMissingField(ChangeField.SUBMIT_RULE_RESULT);
+      return;
+    }
+    try (Registration registration =
+        extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
+      TestRepository<Repo> repo = createProject("repo");
+      insert(repo, newChange(repo));
+      assertQuery("rule:non-existent-rule");
+    }
+  }
+
+  @Test
+  public void byHasDraft() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChange(repo));
@@ -2236,16 +2324,17 @@
     in.path = Patch.COMMIT_MSG;
     gApi.changes().id(change2.getId().get()).current().createDraft(in);
 
-    int user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId().get();
+    Account.Id user2 =
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
 
     assertQuery("has:draft", change2, change1);
-    assertQuery("draftby:" + userId.get(), change2, change1);
-    assertQuery("draftby:" + user2);
+
+    requestContext.setContext(newRequestContext(user2));
+    assertQuery("has:draft");
   }
 
   @Test
-  public void byDraftByExcludesZombieDrafts() throws Exception {
+  public void byHasDraftExcludesZombieDrafts() throws Exception {
     Project.NameKey project = Project.nameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     Change change = insert(repo, newChange(repo));
@@ -2257,7 +2346,7 @@
     in.path = Patch.COMMIT_MSG;
     gApi.changes().id(id.get()).current().createDraft(in);
 
-    assertQuery("draftby:" + userId, change);
+    assertQuery("has:draft", change);
     assertQuery("commentby:" + userId);
 
     try (TestRepository<Repo> allUsers =
@@ -2269,7 +2358,7 @@
       rin.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
       gApi.changes().id(id.get()).current().review(rin);
 
-      assertQuery("draftby:" + userId);
+      assertQuery("has:draft");
       assertQuery("commentby:" + userId, change);
       assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNull();
 
@@ -2279,7 +2368,7 @@
     }
 
     indexer.index(project, id);
-    assertQuery("draftby:" + userId);
+    assertQuery("has:draft");
   }
 
   @Test
@@ -2292,69 +2381,62 @@
     gApi.accounts().self().starChange(change1.getId().toString());
     gApi.accounts().self().starChange(change2.getId().toString());
 
-    int user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId().get();
+    Account.Id user2 =
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
 
-    assertQuery("starredby:self", change2, change1);
-    assertQuery("starredby:" + user2);
+    assertQuery("has:star", change2, change1);
+    assertQuery("star:star", change2, change1);
+
+    requestContext.setContext(newRequestContext(user2));
+    assertQuery("has:star");
+    assertQuery("star:star");
   }
 
   @Test
   public void byStar() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = insert(repo, newChange(repo));
+    Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
     Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
-    Change change3 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
-    Change change4 = insert(repo, newChange(repo));
+    Change change3 = insert(repo, newChange(repo));
 
-    gApi.accounts()
-        .self()
-        .setStars(
-            change1.getId().toString(),
-            new StarsInput(new HashSet<>(Arrays.asList("red", "blue"))));
-    gApi.accounts()
-        .self()
-        .setStars(
-            change2.getId().toString(),
-            new StarsInput(
-                new HashSet<>(Arrays.asList(StarredChangesUtil.DEFAULT_LABEL, "green", "blue"))));
+    Account.Id user2 =
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+    requestContext.setContext(newRequestContext(user2));
 
-    gApi.accounts()
-        .self()
-        .setStars(
-            change4.getId().toString(), new StarsInput(new HashSet<>(Arrays.asList("ignore"))));
-
-    // check labeled stars
-    assertQuery("star:red", change1);
-    assertQuery("star:blue", change2, change1);
-    assertQuery("has:stars", change4, change2, change1);
+    gApi.accounts().self().starChange(change1.getId().toString());
+    gApi.changes().id(change3.getChangeId()).ignore(true);
 
     // check default star
-    assertQuery("has:star", change2);
-    assertQuery("is:starred", change2);
-    assertQuery("starredby:self", change2);
-    assertQuery("star:" + StarredChangesUtil.DEFAULT_LABEL, change2);
+    assertQuery("has:star", change1);
+    assertQuery("is:starred", change1);
+    assertQuery("star:" + StarredChangesUtil.DEFAULT_LABEL, change1);
 
     // check ignored
-    assertQuery("is:ignored", change4);
-    assertQuery("-is:ignored", change3, change2, change1);
+    assertQuery("is:ignored", change3);
+    assertQuery("-is:ignored", change2, change1);
+    assertQuery("star:ignore", change3);
+    assertQuery("-star:ignore", change2, change1);
   }
 
   @Test
   public void byIgnore() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     Change change1 = insert(repo, newChange(repo), user2);
     Change change2 = insert(repo, newChange(repo), user2);
 
     gApi.changes().id(change1.getId().toString()).ignore(true);
     assertQuery("is:ignored", change1);
     assertQuery("-is:ignored", change2);
+    assertQuery("star:ignore", change1);
+    assertQuery("-star:ignore", change2);
 
     gApi.changes().id(change1.getId().toString()).ignore(false);
     assertQuery("is:ignored");
     assertQuery("-is:ignored", change2, change1);
+    assertQuery("star:ignore");
+    assertQuery("-star:ignore", change2, change1);
   }
 
   @Test
@@ -2363,7 +2445,7 @@
     Change change1 = insert(repo, newChange(repo));
 
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     Change change2 = insert(repo, newChange(repo), user2);
 
     ReviewInput input = new ReviewInput();
@@ -2434,6 +2516,17 @@
   }
 
   @Test
+  public void cherrypick() throws Exception {
+    assume().that(getSchema().hasField(ChangeField.CHERRY_PICK)).isTrue();
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newCherryPickChange(repo, "foo", change1.currentPatchSetId()));
+
+    assertQuery("is:cherrypick", change2);
+    assertQuery("-is:cherrypick", change1);
+  }
+
+  @Test
   public void merge() throws Exception {
     assume().that(getSchema().hasField(ChangeField.MERGE)).isTrue();
     TestRepository<Repo> repo = createProject("repo");
@@ -2470,7 +2563,7 @@
     gApi.changes().id(change1.getId().get()).current().review(new ReviewInput().message("comment"));
 
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     requestContext.setContext(newRequestContext(user2));
 
     gApi.changes().id(change2.getId().get()).current().review(new ReviewInput().message("comment"));
@@ -2536,7 +2629,7 @@
   public void byReviewed() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Account.Id otherUser =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChange(repo));
 
@@ -2556,9 +2649,12 @@
 
   @Test
   public void reviewerin() throws Exception {
-    Account.Id user1 = accountManager.authenticate(AuthRequest.forUser("user1")).getAccountId();
-    Account.Id user2 = accountManager.authenticate(AuthRequest.forUser("user2")).getAccountId();
-    Account.Id user3 = accountManager.authenticate(AuthRequest.forUser("user3")).getAccountId();
+    Account.Id user1 =
+        accountManager.authenticate(authRequestFactory.createForUser("user1")).getAccountId();
+    Account.Id user2 =
+        accountManager.authenticate(authRequestFactory.createForUser("user2")).getAccountId();
+    Account.Id user3 =
+        accountManager.authenticate(authRequestFactory.createForUser("user3")).getAccountId();
     TestRepository<Repo> repo = createProject("repo");
 
     Change change1 = insert(repo, newChange(repo));
@@ -2835,7 +2931,6 @@
     insert(repo2, ins2);
 
     assertQuery("is:watched");
-    assertQuery("watchedby:self");
 
     List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
     ProjectWatchInfo pwi = new ProjectWatchInfo();
@@ -2849,7 +2944,6 @@
     resetUser();
 
     assertQuery("is:watched", change1);
-    assertQuery("watchedby:self", change1);
   }
 
   @Test
@@ -2875,19 +2969,6 @@
   }
 
   @Test
-  public void selfAndMe() throws Exception {
-    TestRepository<Repo> repo = createProject("repo");
-    Change change1 = insert(repo, newChange(repo));
-    Change change2 = insert(repo, newChange(repo), userId);
-    insert(repo, newChange(repo));
-    gApi.accounts().self().starChange(change1.getId().toString());
-    gApi.accounts().self().starChange(change2.getId().toString());
-
-    assertQuery("starredby:self", change2, change1);
-    assertQuery("starredby:me", change2, change1);
-  }
-
-  @Test
   public void defaultFieldWithManyUsers() throws Exception {
     for (int i = 0; i < ChangeQueryBuilder.MAX_ACCOUNTS_PER_DEFAULT_FIELD * 2; i++) {
       createAccount("user" + i, "User " + i, "user" + i + "@example.com", true);
@@ -3021,8 +3102,7 @@
       }
       for (Account.Id ignorerId : ignoredBy) {
         requestContext.setContext(newRequestContext(ignorerId));
-        StarsInput in = new StarsInput(new HashSet<>(Arrays.asList("ignore")));
-        gApi.accounts().self().setStars("" + id, in);
+        gApi.changes().id(change.getChangeId()).ignore(true);
       }
       DraftInput in = new DraftInput();
       in.path = Patch.COMMIT_MSG;
@@ -3352,6 +3432,7 @@
   @Test
   public void attentionSetIndexed() throws Exception {
     assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue();
+    assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS_COUNT)).isTrue();
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
     Change change2 = insert(repo, newChange(repo));
@@ -3359,8 +3440,18 @@
     AttentionSetInput input = new AttentionSetInput(userId.toString(), "some reason");
     gApi.changes().id(change1.getChangeId()).addToAttentionSet(input);
 
+    assertQuery("is:attention", change1);
+    assertQuery("-is:attention", change2);
+    assertQuery("has:attention", change1);
+    assertQuery("-has:attention", change2);
     assertQuery("attention:" + user.getUserName().get(), change1);
     assertQuery("-attention:" + userId.toString(), change2);
+
+    gApi.changes()
+        .id(change1.getChangeId())
+        .attention(userId.toString())
+        .remove(new AttentionSetInput("removed again"));
+    assertQuery("-is:attention", change1, change2);
   }
 
   @Test
@@ -3372,7 +3463,7 @@
     AttentionSetInput input = new AttentionSetInput(userId.toString(), "reason 1");
     gApi.changes().id(change.getChangeId()).addToAttentionSet(input);
     Account.Id user2Id =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
 
     // Add the second user as cc to ensure that user took part of the change and can be added to the
     // attention set.
@@ -3424,7 +3515,7 @@
         .isEqualTo("Unknown named destination: foo");
 
     Account.Id anotherUserId =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     String destination1 = "refs/heads/master\trepo1";
     String destination2 = "refs/heads/master\trepo2";
     String destination3 = "refs/heads/master\trepo1\nrefs/heads/master\trepo2";
@@ -3501,7 +3592,7 @@
     Change change2 = insert(repo, newChangeForBranch(repo, "stable"));
 
     Account.Id anotherUserId =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
     String queryListText =
         "query1\tproject:repo\n"
             + "query2\tproject:repo status:open\n"
@@ -3585,7 +3676,7 @@
 
   @Test
   public void selfFailsForAnonymousUser() throws Exception {
-    for (String query : ImmutableList.of("assignee:self", "starredby:self", "is:starred")) {
+    for (String query : ImmutableList.of("assignee:self", "has:star", "is:starred", "star:star")) {
       assertQuery(query);
       RequestContext oldContext = requestContext.setContext(anonymousUserProvider::get);
 
@@ -3603,22 +3694,20 @@
   @Test
   public void selfSucceedsForInactiveAccount() throws Exception {
     Account.Id user2 =
-        accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+        accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
 
     TestRepository<Repo> repo = createProject("repo");
     Change change = insert(repo, newChange(repo));
-    AssigneeInput ain = new AssigneeInput();
-    ain.assignee = user2.toString();
-    gApi.changes().id(change.getId().get()).setAssignee(ain);
+    gApi.changes().id(change.getId().get()).addReviewer(user2.toString());
 
     RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
-    assertQuery("assignee:self", change);
+    assertQuery("reviewer:self", change);
 
     requestContext.setContext(adminContext);
     gApi.accounts().id(user2.get()).setActive(false);
 
     requestContext.setContext(newRequestContext(user2));
-    assertQuery("assignee:self", change);
+    assertQuery("reviewer:self", change);
   }
 
   @Test
@@ -3657,12 +3746,12 @@
   }
 
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
-    return newChange(repo, null, null, null, null, false, false);
+    return newChange(repo, null, null, null, null, null, false, false);
   }
 
   protected ChangeInserter newChangeForCommit(TestRepository<Repo> repo, RevCommit commit)
       throws Exception {
-    return newChange(repo, commit, null, null, null, false, false);
+    return newChange(repo, commit, null, null, null, null, false, false);
   }
 
   protected ChangeInserter newChangeWithFiles(TestRepository<Repo> repo, String... paths)
@@ -3676,25 +3765,30 @@
 
   protected ChangeInserter newChangeForBranch(TestRepository<Repo> repo, String branch)
       throws Exception {
-    return newChange(repo, null, branch, null, null, false, false);
+    return newChange(repo, null, branch, null, null, null, false, false);
   }
 
   protected ChangeInserter newChangeWithStatus(TestRepository<Repo> repo, Change.Status status)
       throws Exception {
-    return newChange(repo, null, null, status, null, false, false);
+    return newChange(repo, null, null, status, null, null, false, false);
   }
 
   protected ChangeInserter newChangeWithTopic(TestRepository<Repo> repo, String topic)
       throws Exception {
-    return newChange(repo, null, null, null, topic, false, false);
+    return newChange(repo, null, null, null, topic, null, false, false);
   }
 
   protected ChangeInserter newChangeWorkInProgress(TestRepository<Repo> repo) throws Exception {
-    return newChange(repo, null, null, null, null, true, false);
+    return newChange(repo, null, null, null, null, null, true, false);
   }
 
   protected ChangeInserter newChangePrivate(TestRepository<Repo> repo) throws Exception {
-    return newChange(repo, null, null, null, null, false, true);
+    return newChange(repo, null, null, null, null, null, false, true);
+  }
+
+  protected ChangeInserter newCherryPickChange(
+      TestRepository<Repo> repo, String branch, PatchSet.Id cherryPickOf) throws Exception {
+    return newChange(repo, null, branch, null, null, cherryPickOf, false, true);
   }
 
   protected ChangeInserter newChange(
@@ -3703,6 +3797,7 @@
       @Nullable String branch,
       @Nullable Change.Status status,
       @Nullable String topic,
+      @Nullable PatchSet.Id cherryPickOf,
       boolean workInProgress,
       boolean isPrivate)
       throws Exception {
@@ -3723,7 +3818,8 @@
             .setStatus(status)
             .setTopic(topic)
             .setWorkInProgress(workInProgress)
-            .setPrivate(isPrivate);
+            .setPrivate(isPrivate)
+            .setCherryPickOf(cherryPickOf);
     return ins;
   }
 
@@ -3976,9 +4072,10 @@
   private Account.Id createAccount(String username, String fullName, String email, boolean active)
       throws Exception {
     try (ManualRequestContext ctx = oneOffRequestContext.open()) {
-      Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+      Account.Id id =
+          accountManager.authenticate(authRequestFactory.createForUser(username)).getAccountId();
       if (email != null) {
-        accountManager.link(id, AuthRequest.forEmail(email));
+        accountManager.link(id, authRequestFactory.createForEmail(email));
       }
       accountsUpdate
           .get()
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 0f102a8..08456d1 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -17,6 +17,7 @@
         "//prolog:gerrit-prolog-common",
     ],
     deps = [
+        "//java/com/google/gerrit/acceptance:lib",
         "//java/com/google/gerrit/acceptance/config",
         "//java/com/google/gerrit/acceptance/testsuite/project",
         "//java/com/google/gerrit/common:annotations",
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index f392747..568b5a0 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -106,6 +106,8 @@
 
   @Inject protected GroupIndexCollection indexes;
 
+  @Inject protected AuthRequest.Factory authRequestFactory;
+
   @Inject private GroupIndexCollection groupIndexes;
 
   protected LifecycleManager lifecycle;
@@ -397,9 +399,10 @@
   private Account.Id createAccountOutsideRequestContext(
       String username, String fullName, String email, boolean active) throws Exception {
     try (ManualRequestContext ctx = oneOffRequestContext.open()) {
-      Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+      Account.Id id =
+          accountManager.authenticate(authRequestFactory.createForUser(username)).getAccountId();
       if (email != null) {
-        accountManager.link(id, AuthRequest.forEmail(email));
+        accountManager.link(id, authRequestFactory.createForEmail(email));
       }
       accountsUpdate
           .get()
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 2317c7e..60d1655 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -104,6 +104,8 @@
 
   @Inject protected AllUsersName allUsers;
 
+  @Inject protected AuthRequest.Factory authRequestFactory;
+
   protected LifecycleManager lifecycle;
   protected Injector injector;
   protected AccountInfo currentUserInfo;
@@ -309,9 +311,10 @@
   private Account.Id createAccount(String username, String fullName, String email, boolean active)
       throws Exception {
     try (ManualRequestContext ctx = oneOffRequestContext.open()) {
-      Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+      Account.Id id =
+          accountManager.authenticate(authRequestFactory.createForUser(username)).getAccountId();
       if (email != null) {
-        accountManager.link(id, AuthRequest.forEmail(email));
+        accountManager.link(id, authRequestFactory.createForEmail(email));
       }
       accountsUpdate
           .get()
diff --git a/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
index da22f76..e0ba666 100644
--- a/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
+++ b/javatests/com/google/gerrit/server/schema/ProjectConfigSchemaUpdateTest.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -36,7 +37,10 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
+@RunWith(JUnit4.class)
 public class ProjectConfigSchemaUpdateTest {
   private static final String ALL_PROJECTS = "All-The-Projects";
 
@@ -64,8 +68,11 @@
     try (Repository repo = new FileRepository(allProjectsRepoFile)) {
       repo.create(true);
     }
+    FileBasedAllProjectsConfigProvider configProvider =
+        new FileBasedAllProjectsConfigProvider(sitePaths);
 
-    factory = new ProjectConfigSchemaUpdate.Factory(sitePaths, new AllProjectsName(ALL_PROJECTS));
+    factory =
+        new ProjectConfigSchemaUpdate.Factory(new AllProjectsName(ALL_PROJECTS), configProvider);
   }
 
   @Test
diff --git a/package.json b/package.json
index 9df72da..a47ba9f 100644
--- a/package.json
+++ b/package.json
@@ -6,19 +6,20 @@
     "@bazel/rollup": "^3.5.0",
     "@bazel/terser": "^3.5.0",
     "@bazel/typescript": "^3.5.0",
-    "twinkie": "^1.1.2"
+    "twinkie": "^1.1.3"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^4.25.0",
+    "@typescript-eslint/eslint-plugin": "^4.29.0",
     "eslint": "^7.24.0",
     "eslint-config-google": "^0.14.0",
     "eslint-plugin-html": "^6.1.2",
     "eslint-plugin-import": "^2.22.1",
     "eslint-plugin-jsdoc": "^32.3.0",
+    "eslint-plugin-lit": "^1.5.1",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-prettier": "^3.4.0",
+    "eslint-plugin-regex": "^1.8.0",
     "gts": "^3.1.0",
-    "polymer-cli": "^1.9.11",
     "prettier": "2.3.1",
     "rollup": "^2.45.2",
     "terser": "^5.6.1",
@@ -33,17 +34,19 @@
     "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
-    "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
-    "test:debug": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
-    "test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles",
-    "postinstall": "(git apply --reverse --ignore-whitespace twinkie.patch || true) && git apply --ignore-whitespace twinkie.patch",
-    "polytest": "npm run safe_bazelisk test //polygerrit-ui/app:validate_polymer_templates",
-    "polytest:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
+    "test:debug": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --browsers ChromeDev --no-single-run --test-files",
+    "test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --test-files",
+    "polylint": "npm run safe_bazelisk test //polygerrit-ui/app:polylint_test",
+    "polylint:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
   },
   "repository": {
     "type": "git",
     "url": "https://gerrit.googlesource.com/gerrit"
   },
+  "resolutions": {
+    "lodash": "4.17.21",
+    "twinkie/typescript": "4.3.2"
+  },
   "author": "",
   "license": "Apache-2.0"
 }
diff --git a/plugins/delete-project b/plugins/delete-project
index 7dce6f7..8fe544a 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 7dce6f70611cd8dbf1d38628698155258ee8ef82
+Subproject commit 8fe544ac569efa357ee054257143d8e1d4aa6afd
diff --git a/plugins/package.json b/plugins/package.json
index f761be9..4e3c376 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -5,7 +5,7 @@
     "dependencies": {
       "@polymer/decorators": "^3.0.0",
       "@polymer/polymer": "^3.4.1",
-      "@gerritcodereview/typescript-api": "3.4.2",
+      "@gerritcodereview/typescript-api": "3.4.4",
       "lit": "2.0.0-rc.3"
     },
     "license": "Apache-2.0",
diff --git a/plugins/replication b/plugins/replication
index 46cfb7d..cd17fe7 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 46cfb7dd5b6891f991cfe66e72c08953487c1c81
+Subproject commit cd17fe7f90e5a36ab84b9b7ce0aab22e60e48a70
diff --git a/plugins/tsconfig-plugins-base.json b/plugins/tsconfig-plugins-base.json
index b9d14e1..b7e9d52 100644
--- a/plugins/tsconfig-plugins-base.json
+++ b/plugins/tsconfig-plugins-base.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
     /* Basic Options */
-    "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
     "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "inlineSourceMap": true, /* Generates corresponding '.map' file. */
     "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
@@ -20,6 +20,7 @@
     "noUnusedLocals": true, /* Report errors on unused locals. */
     "noUnusedParameters": true, /* Report errors on unused parameters. */
     "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+    "noImplicitOverride": true,
     "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
 
     "skipLibCheck": true, /* Do not check node_modules */
@@ -34,6 +35,15 @@
     "incremental": true,
     "experimentalDecorators": true,
 
-    "allowUmdGlobalAccess": true
+    "allowUmdGlobalAccess": true,
+
+    "typeRoots": [
+      /* typeRoots for Bazel */
+      "../external/ui_dev_npm/node_modules/@types",
+      "../external/plugins_npm/node_modules/@types",
+      /* typeRoots for IDE */
+      "../polygerrit-ui/node_modules/@types",
+      "../plugins/node_modules/@types"
+    ]
   },
 }
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 1faa71a..3ff1cc4 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -2,10 +2,10 @@
 # yarn lockfile v1
 
 
-"@gerritcodereview/typescript-api@3.4.2":
-  version "3.4.2"
-  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.4.2.tgz#79e8ff336608cbf18e651bfa9541d7bdead5e1f9"
-  integrity sha512-iqHd6G43pV4Wk5iNw95AQmWUBuIrY+dvQ1Ne8ZYkOwRhdruh4BAPhMtsmqWDlcVQbfcwZD5F2zFkGB4J4htggw==
+"@gerritcodereview/typescript-api@3.4.4":
+  version "3.4.4"
+  resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.4.4.tgz#9f09687038088dd7edd3b4e30d249502eb21bfbc"
+  integrity sha512-MAiQwntcQ59b92yYDsVIXj3oBbAB4C7HELkLFFbYs4ZjzC43XqqtR9VF0dh5OUC8wzFZttgUiOmGehk9edpPuw==
 
 "@lit/reactive-element@^1.0.0-rc.2":
   version "1.0.0-rc.2"
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 7bca96d..62d1d92 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -1,5 +1,6 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary")
 load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/bzl:js.bzl", "karma_test")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -33,8 +34,6 @@
     ],
 )
 
-# Define a karma+plugins binary to run karma-mocha tests.
-# Can be reused multiple time, if there are multiple karma test rules
 sh_binary(
     name = "karma_bin",
     srcs = ["@ui_dev_npm//:node_modules/karma/bin/karma"],
@@ -49,26 +48,8 @@
     ],
 )
 
-# Run all tests in one.
-# TODO(dmfilippov): allow parallel tests for karma - either on the bazel level
-# or on the karma level. For now single sh_test is enough.
-sh_test(
+karma_test(
     name = "karma_test",
-    size = "enormous",
     srcs = ["karma_test.sh"],
-    args = [
-        "$(location :karma_bin)",
-        "$(location karma.conf.js)",
-    ],
-    data = [
-        "karma.conf.js",
-        ":karma_bin",
-        "//polygerrit-ui/app:test-srcs-fg",
-    ],
-    # Should not run sandboxed.
-    tags = [
-        "karma",
-        "local",
-        "manual",
-    ],
+    data = ["//polygerrit-ui/app:test-srcs-fg"],
 )
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index faf126c..14f9e8c 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -277,6 +277,18 @@
       },
     },
     {
+      files: ['**/api/*.ts'],
+      rules: {
+        'regex/invalid': [
+          'error', [{
+            regex: 'export interface',
+            message: 'All interfaces in the api/ dir must have "declare"',
+            replacement: 'export declare interface',
+          }],
+        ],
+      },
+    },
+    {
       files: ['**/*.ts'],
       extends: [require.resolve('gts/.eslintrc.json')],
       rules: {
@@ -400,12 +412,32 @@
         }],
       },
     },
+    {
+      files: ['*.ts'],
+      excludedFiles: '*_html.ts',
+      rules: {
+        'lit/attribute-value-entities': 'error',
+        'lit/binding-positions': 'error',
+        'lit/no-duplicate-template-bindings': 'error',
+        'lit/no-invalid-html': 'error',
+        'lit/no-legacy-template-syntax': 'error',
+        'lit/no-property-change-update': 'error',
+        'lit/no-invalid-escape-sequences': 'error',
+        'lit/no-legacy-imports': 'error',
+        'lit/no-private-properties': 'error',
+        'lit/no-useless-template-literals': 'error',
+        'lit/no-value-attribute': 'error',
+        'lit/prefer-static-styles': 'error',
+      },
+    },
   ],
   plugins: [
     'html',
     'jsdoc',
     'import',
+    'lit',
     'prettier',
+    'regex',
   ],
   settings: {
     'html/report-bad-indent': 'error',
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 4a186c1..0552d45 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -45,8 +45,6 @@
     ),
     allow_js = True,
     incremental = True,
-    # The same outdir also appears in the following files:
-    # polylint_test.sh
     out_dir = "_pg_ts_out",
     tsc = "//tools/node_tools:tsc-bin",
     tsconfig = ":ts_config_bazel",
@@ -70,7 +68,6 @@
         [
             "**/*.js",
             "**/*.ts",
-            "test/@types/*.d.ts",
         ],
         exclude = [
             "node_modules/**",
@@ -97,9 +94,7 @@
 # so template tests pass.
 # TODO: fix problems reported by template checker in these files.
 ignore_templates_list = [
-    "elements/admin/gr-access-section/gr-access-section_html.ts",
     "elements/admin/gr-admin-view/gr-admin-view_html.ts",
-    "elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts",
     "elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
     "elements/admin/gr-group-members/gr-group-members_html.ts",
     "elements/admin/gr-group/gr-group_html.ts",
@@ -110,7 +105,6 @@
     "elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts",
     "elements/admin/gr-repo/gr-repo_html.ts",
     "elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
-    "elements/change-list/gr-change-list-item/gr-change-list-item_html.ts",
     "elements/change-list/gr-change-list-view/gr-change-list-view_html.ts",
     "elements/change-list/gr-change-list/gr-change-list_html.ts",
     "elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts",
@@ -118,9 +112,6 @@
     "elements/change/gr-change-metadata/gr-change-metadata_html.ts",
     "elements/change/gr-change-requirements/gr-change-requirements_html.ts",
     "elements/change/gr-change-view/gr-change-view_html.ts",
-    "elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts",
-    "elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts",
-    "elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts",
     "elements/change/gr-file-list-header/gr-file-list-header_html.ts",
     "elements/change/gr-file-list/gr-file-list_html.ts",
     "elements/change/gr-label-score-row/gr-label-score-row_html.ts",
@@ -146,17 +137,19 @@
     "elements/shared/gr-list-view/gr-list-view_html.ts",
 ]
 
+sources_for_template_checking = glob(
+    [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
+        ".ts",
+    ]],
+    exclude = [
+        "**/*_test.ts",
+    ] + ignore_templates_list,
+)
+
 # Transform templates into a .ts files.
 templates_srcs = transform_polymer_templates(
     name = "template_test",
-    srcs = glob(
-        [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
-            ".ts",
-        ]],
-        exclude = [
-            "**/*_test.ts",
-        ] + ignore_templates_list,
-    ),
+    srcs = sources_for_template_checking,
     out_tsconfig = "tsconfig_template_test.json",
     tsconfig = "tsconfig_bazel.json",
     deps = [
@@ -166,50 +159,34 @@
     ],
 )
 
-# Compile transformed templates together with the polygerrit source. If
-# templates don't have problem, then the compilation ends without error.
-# Otherwise, the typescript compiler reports the error.
-# Note, that the compile_ts macro creates build rules. If the build succeed,
-# the macro creates the file compile_template_test.success. The
-# 'validate_polymer_templates' rule tests existence of the file.
-#
-# TODO: Re-instantiate this rule. It broke when switching to ts_project with
-# ERROR: //polygerrit-ui/app:compile_template_test srcs cannot be a mix of
-#        generated files and source files since this would prevent giving a
-#        single rootDir to the TypeScript compiler
-# Also, the emitJS feature of compile_ts has to be re-created in some form.
-#ts_project(
-#    name = "compile_template_test",
-#    srcs = templates_srcs + glob(
-#        [src_dir + "/**/*" + ext for src_dir in src_dirs for ext in [
-#            ".ts",
-#        ]],
-#        exclude = [
-#            "**/*_test.ts",
-#        ] + ignore_templates_list,
-#    ),
-#    allow_js = True,
-#    out_dir = "_pg_template_test_out",
-#    # Should not run sandboxed.
-#    tags = [
-#        "local",
-#        "manual",
-#    ],
-#    tsc = "//tools/node_tools:tsc-bin",
-#    tsconfig = "tsconfig_template_test.json",
-#)
-#
-# This rule allows to run polymer template checker with bazel test command.
-# For details - see compile_template_test rule.
-#
-# TODO: Re-instantiate this test. It broke when switching
-#       'compile_template_test'to ts_project, see above. ts_project does not
-#       create '.success' files.
-#sh_test(
-#    name = "validate_polymer_templates",
-#    srcs = [":empty_test.sh"],
-#    data = ["compile_template_test.success"],
-#)
+# After templates are converted into a typescript code, the TS compiler should check that the
+# converted code doesn't have the error (i.e. templates don't have problems).
+# The input to the compiler is: the converted (i.e. autogenerated) code + original polygerrit code;
+# the output (i.e. js code) is not needed (we only care wheather the code has error or not).
+# The existing ts_project rule can't compile a mix of a generated and a non-generated code, so it
+# can't be used for the purpose of template checking.
+# Because the output of TS compiler is not needed, the simplest workaround is to run typescript
+# compiler from command line using the sh_test rule. The compiler exits with non-zero return code if
+# errors found and sh_test fails.
+sh_test(
+    name = "polylint_test",
+    srcs = [":compile_generated_templates.sh"],
+    args = [
+        "$(location //tools/node_tools:tsc-bin)",
+        "$(location tsconfig_template_test.json)",
+    ],
+    data = [
+        "tsconfig_template_test.json",
+        "tsconfig_bazel.json",
+        "tsconfig.json",
+        "//tools/node_tools:tsc-bin",
+        "@ui_npm//:node_modules",
+    ] + templates_srcs + sources_for_template_checking,
+    tags = [
+        "local",
+        "manual",
+    ],
+)
 
 polygerrit_bundle(
     name = "polygerrit_ui",
@@ -291,33 +268,3 @@
         "@npm//gts",
     ],
 )
-
-filegroup(
-    name = "polylint-fg",
-    srcs = [
-        # Workaround for https://github.com/bazelbuild/bazel/issues/1305
-        "@ui_npm//:node_modules",
-        # Polylinter can't check .ts files, run it on compiled srcs
-        ":compile_pg",
-    ],
-)
-
-sh_test(
-    name = "polylint_test",
-    size = "large",
-    srcs = ["polylint_test.sh"],
-    args = [
-        "$(location @tools_npm//polymer-cli/bin:polymer)",
-        "$(location polymer.json)",
-    ],
-    data = [
-        "polymer.json",
-        ":polylint-fg",
-        "@tools_npm//polymer-cli/bin:polymer",
-    ],
-    # Should not run sandboxed.
-    tags = [
-        "local",
-        "manual",
-    ],
-)
diff --git a/polygerrit-ui/app/api/BUILD_for_publishing_api_only b/polygerrit-ui/app/api/BUILD_for_publishing_api_only
new file mode 100644
index 0000000..9d3029b
--- /dev/null
+++ b/polygerrit-ui/app/api/BUILD_for_publishing_api_only
@@ -0,0 +1,50 @@
+# This BUILD file is only for publishing the
+# "Gerrit Frontend Plugin TypeScript API" as an npm package.
+#
+# Publishing procedure:
+# - Execute the `publish.sh` script from the Gerrit root dir.
+# - Verify that the contents look good.
+# - Increment the version in package.json.
+# - Execute `publish.sh --upload`.
+#
+# NB: Renaming to 'BUILD' breaks the app/BUILD, because then the api/ sources
+# are not visible anymore to the parent BUILD. And if ts_projects depend on each
+# other, then the api/ files would have to be imported with their full package
+# names.
+load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm")
+load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
+
+filegroup(
+    name = "js_plugin_api_srcs",
+    srcs = glob(["**/*.ts"]),
+)
+
+ts_config(
+    name = "ts_config",
+    src = "tsconfig.json",
+    deps = [
+        "//plugins:tsconfig-plugins-base.json",
+    ],
+)
+
+ts_project(
+    name = "js_plugin_api_compiled",
+    srcs = glob(["**/*.ts"]),
+    incremental = True,
+    tsc = "//tools/node_tools:tsc-bin",
+    tsconfig = ":ts_config",
+)
+
+# Use this rule for publishing the js plugin api as a package to the npm repo.
+pkg_npm(
+    name = "js_plugin_api_npm_package",
+    srcs = glob(
+        ["**/*"],
+        exclude = [
+            "BUILD",
+            "tsconfig.json",
+            "publish.sh",
+        ],
+    ),
+    deps = [":js_plugin_api_compiled"],
+)
diff --git a/polygerrit-ui/app/api/README.md b/polygerrit-ui/app/api/README.md
index 550063f..b5710bf 100644
--- a/polygerrit-ui/app/api/README.md
+++ b/polygerrit-ui/app/api/README.md
@@ -1,23 +1,25 @@
-# API
+# Gerrit TypeScript Plugin API
 
-In this folder, we declare the API of various parts of the Gerrit webclient.
-There are two primary use cases for this:
+This package contains the types for developing browser plugins for the
+Gerrit Code Review web application. General documentation for plugin
+developers can be found at
+[gerrit-review.googlesource.com](https://gerrit-review.googlesource.com/Documentation/pg-plugin-dev.html).
 
-* apps that embed our diff viewer, gr-diff
-* Gerrit plugins that need to access some part of Gerrit to extend it
+The `.ts` files only contain types, interfaces and enums, and thus the compiled
+`.js` files only contain the enums. For JavaScript plugins this package is not
+really useful or necessary, but it also serves as the source of truth for
+what plugin APIs are actually supported.
 
-Both may be built as a separate bundle, but would like to type check against
-the same types the Gerrit/gr-diff bundle uses. For this reason, this folder
-should contain only types, with the exception of enums, where having the
-value side is deemed an acceptable duplication.
+Versioning of this API matches the MAJOR and MINOR versions of the general
+Gerrit releases, but the PATCH version is independent. When you are building
+a plugin for Gerrit x.y.z, then you should use the API package x.y.n, where
+n is the highest available patch version of the API. Patch versions will only
+contain additions and fixes, minor versions may include API removals.
 
 All types in here should use the `declare` keyword to prevent bundlers from
 renaming fields, which would break communication across separately built
-bundles. Again enums are the exception, because their keys are not referenced
+bundles. enums are the exception, because their keys are not referenced
 across bundles, and values will not be renamed by bundlers as they are strings.
 
-This API is used by other apps embedding gr-diff and any breaking changes
+This API is also used by other apps embedding gr-diff and any breaking changes
 should be discussed with the Gerrit core team and properly versioned.
-
-Gerrit types should either directly use or extend these types, so that
-breaking changes to the implementation require changes to these files.
diff --git a/polygerrit-ui/app/api/change-actions.ts b/polygerrit-ui/app/api/change-actions.ts
index 2ce697a..4380195 100644
--- a/polygerrit-ui/app/api/change-actions.ts
+++ b/polygerrit-ui/app/api/change-actions.ts
@@ -72,6 +72,9 @@
 export type PrimaryActionKey = ChangeActions | RevisionActions;
 
 export declare interface ChangeActionsPluginApi {
+  // Deprecated. This API method will be removed.
+  ensureEl(): Element;
+
   addPrimaryActionKey(key: PrimaryActionKey): void;
 
   removePrimaryActionKey(key: string): void;
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index b64cd91..d52a555 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -144,8 +144,8 @@
    * attempt. Every run has its own attempt numbering, so attempt 3 of run A is
    * not directly related to attempt 3 of run B.
    *
-   * RUNNABLE runs must use `undefined` as attempt.
-   * COMPLETED and RUNNING runs must use an attempt number >=0.
+   * The attempt number must be >=0. Only if you have just one RUNNABLE attempt,
+   * then you can leave it undefined.
    *
    * TBD: Optionally providing aggregate information about former attempts will
    * probably be a useful feature, but we are deferring the exact data modeling
@@ -184,7 +184,8 @@
 
   /**
    * RUNNABLE:  Not run (yet). Mostly useful for runs that the user can trigger
-   *            (see actions). Cannot contain results.
+   *            (see actions) and for indicating that a check was not run at a
+   *            later attempt. Cannot contain results.
    * RUNNING:   Subsumes "scheduled".
    * COMPLETED: The attempt of the run has finished. Does not indicate at all
    *            whether the run was successful or not. Outcomes can and should
@@ -249,6 +250,12 @@
    */
   primary?: boolean;
   /**
+   * Summary actions will get an even more prominent treatment in the UI. They
+   * will show up in the checks summary right below the commit message. This
+   * only affects top-level actions (i.e. actions in FetchResponse).
+   */
+  summary?: boolean;
+  /**
    * Renders the action button in a disabled state. That can be useful for
    * actions that are present most of the time, but sometimes don't apply. Then
    * a grayed out button with a tooltip makes it easier for the user to
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index ba7eb70..ee579ff 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -53,30 +53,30 @@
 }
 
 /**
+ * Represents a "generic" text range in the code (e.g. text selection)
+ */
+interface TextRange {
+  /** first line of the range (1-based inclusive). */
+  start_line: number;
+  /** first column of the range (in the first line) (1-based inclusive). */
+  start_column: number;
+  /** last line of the range (1-based inclusive). */
+  end_line: number;
+  /** last column of the range (in the end line) (1-based inclusive). */
+  end_column: number;
+}
+
+/**
  * Represents a syntax block in a code (e.g. method, function, class, if-else).
  */
 export declare interface SyntaxBlock {
   /** Name of the block (e.g. name of the method/class)*/
   name: string;
-  /** Where does this block syntatically starts and ends (line number and column).*/
-  range: {
-    /** first line of the block (1-based inclusive). */
-    start_line: number;
-    /**
-     * column of the range start inside the first line (e.g. "{" character ending a function/method)
-     * (1-based inclusive).
-     */
-    start_column: number;
-    /**
-     * last line of the block (1-based inclusive).
-     */
-    end_line: number;
-    /**
-     * column of the block end inside the end line (e.g. "}" character ending a function/method)
-     * (1-based inclusive).
-     */
-    end_column: number;
-  };
+  /**
+   * Where does this block syntatically starts and ends (line number and
+   * column).
+   */
+  range: TextRange;
   /** Sub-blocks of the current syntax block (e.g. methods of a class) */
   children: SyntaxBlock[];
 }
@@ -209,6 +209,25 @@
   line_wrapping?: boolean;
 }
 
+/**
+ * Event details when a token is highlighted.
+ */
+export declare interface TokenHighlightEventDetails {
+  token: string;
+  element: Element;
+  side: Side;
+  range: TextRange;
+}
+
+/**
+ * Listens to changes in token highlighting - when a new token starts or stopped
+ * being highlighted. undefined is sent if the event is about a clear in
+ * highlighting.
+ */
+export type TokenHighlightListener = (
+  tokenHighlightEvent?: TokenHighlightEventDetails
+) => void;
+
 export declare interface ImageDiffPreferences {
   automatic_blink?: boolean;
 }
@@ -374,6 +393,7 @@
    * @param textElement The rendered text of one side of the diff.
    * @param lineNumberElement The rendered line number of one side of the diff.
    * @param line Describes the line that should be annotated.
+   * @param side Which side of the diff is being annotated.
    */
   annotate(
     textElement: HTMLElement,
diff --git a/polygerrit-ui/app/api/embed.ts b/polygerrit-ui/app/api/embed.ts
index b1b7f34..520aeec 100644
--- a/polygerrit-ui/app/api/embed.ts
+++ b/polygerrit-ui/app/api/embed.ts
@@ -20,14 +20,24 @@
  * limitations under the License.
  */
 
-import {DiffLayer, GrAnnotation, GrDiffCursor} from './diff';
+import {
+  DiffLayer,
+  GrAnnotation,
+  GrDiffCursor,
+  TokenHighlightListener,
+} from './diff';
 
 declare global {
   interface Window {
     grdiff: {
       GrAnnotation: GrAnnotation;
       GrDiffCursor: {new (): GrDiffCursor};
-      TokenHighlightLayer: {new (): DiffLayer};
+      TokenHighlightLayer: {
+        new (
+          container?: HTMLElement,
+          listener?: TokenHighlightListener
+        ): DiffLayer;
+      };
     };
   }
 }
diff --git a/polygerrit-ui/app/api/gerrit.ts b/polygerrit-ui/app/api/gerrit.ts
index 8488961..2091eea 100644
--- a/polygerrit-ui/app/api/gerrit.ts
+++ b/polygerrit-ui/app/api/gerrit.ts
@@ -25,7 +25,7 @@
   }
 }
 
-export interface Gerrit {
+export declare interface Gerrit {
   install(
     callback: (plugin: PluginApi) => void,
     opt_version?: string,
diff --git a/polygerrit-ui/app/api/hook.ts b/polygerrit-ui/app/api/hook.ts
index f8a6cc1..8cbb9d0 100644
--- a/polygerrit-ui/app/api/hook.ts
+++ b/polygerrit-ui/app/api/hook.ts
@@ -16,7 +16,7 @@
  */
 import {ChangeInfo, ConfigInfo, RevisionInfo} from './rest-api';
 
-export interface GerritElementExtensions {
+export declare interface GerritElementExtensions {
   content?: HTMLElement & {hidden?: boolean};
   change?: ChangeInfo;
   revision?: RevisionInfo;
diff --git a/polygerrit-ui/app/api/package.json b/polygerrit-ui/app/api/package.json
new file mode 100644
index 0000000..8af6832
--- /dev/null
+++ b/polygerrit-ui/app/api/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "@gerritcodereview/typescript-api",
+  "version": "3.4.4",
+  "description": "Gerrit Code Review - TypeScript API",
+  "homepage": "https://www.gerritcodereview.com/",
+  "browser": true,
+  "dependencies": {},
+  "license": "Apache-2.0"
+}
diff --git a/polygerrit-ui/app/api/popup.ts b/polygerrit-ui/app/api/popup.ts
index 8d81831..d265ee6 100644
--- a/polygerrit-ui/app/api/popup.ts
+++ b/polygerrit-ui/app/api/popup.ts
@@ -17,9 +17,10 @@
 
 export declare interface PopupPluginApi {
   /**
-   * Opens the popup, inserts it into DOM over current UI.
-   * Creates the popup if not previously created. Creates popup content element,
-   * if it was provided with constructor.
+   * Opens the popup, inserts it into the DOM over current UI.
+   * Creates the popup if not previously created. Creates and inserts the popup
+   * content element, if a `moduleName` was provided in the constructor.
+   * Otherwise you have to call `appendContent()` when the promise resolves.
    */
   open(): Promise<PopupPluginApi>;
 
@@ -27,4 +28,10 @@
    * Hides the popup.
    */
   close(): void;
+
+  /**
+   * Appends the given element as a child to the popup. Only call this method
+   * when you have called `popup()` without a `moduleName`.
+   */
+  appendContent(el: HTMLElement): void;
 }
diff --git a/polygerrit-ui/app/api/publish.sh b/polygerrit-ui/app/api/publish.sh
new file mode 100755
index 0000000..16de4c9
--- /dev/null
+++ b/polygerrit-ui/app/api/publish.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+# Should be executed from the root of the Gerrit repo:
+# polygerrit-ui/app/api/publish.sh
+#
+# Builds the npm package @gerritcodereview/typescript-api
+#
+# Adding the `--upload` argument will also publish the package.
+
+bazel_bin=$(which bazelisk 2>/dev/null)
+if [[ -z "$bazel_bin" ]]; then
+    echo "Warning: bazelisk is not installed; falling back to bazel."
+    bazel_bin=bazel
+fi
+api_path=polygerrit-ui/app/api
+
+function cleanup() {
+  echo "Cleaning up ..."
+  rm -f ${api_path}/BUILD
+}
+trap cleanup EXIT
+cp ${api_path}/BUILD_for_publishing_api_only ${api_path}/BUILD
+
+${bazel_bin} build //${api_path}:js_plugin_api_npm_package
+
+if [ "$1" == "--upload" ]; then
+  echo 'Uploading npm package @gerritcodereview/typescript-api'
+  ${bazel_bin} run //${api_path}:js_plugin_api_npm_package.publish
+fi
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index f3bb39b..f86e825 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -30,10 +30,6 @@
  * enums =======================================================================
  */
 
-export enum AccountTag {
-  SERVICE_USER = 'SERVICE_USER',
-}
-
 /**
  * The authentication type that is configured on the server.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#auth-info
@@ -237,7 +233,15 @@
   _more_accounts?: boolean; // not set if false
   status?: string; // status message of the account
   inactive?: boolean; // not set if false
-  tags?: AccountTag[];
+  tags?: string[];
+}
+
+/**
+ * The AccountDetailInfo entity contains detailed information about an account.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-detail-info
+ */
+export declare interface AccountDetailInfo extends AccountInfo {
+  registered_on: Timestamp;
 }
 
 /**
@@ -245,7 +249,7 @@
  * from the accounts section.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#accounts-config-info
  */
-export interface AccountsConfigInfo {
+export declare interface AccountsConfigInfo {
   visibility: string;
   default_display_name: DefaultDisplayNameConfig;
 }
@@ -316,7 +320,7 @@
  * configuration of the Gerrit server.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#auth-info
  */
-export interface AuthInfo {
+export declare interface AuthInfo {
   auth_type: AuthType; // docs incorrectly names it 'type'
   use_contributor_agreements?: boolean;
   contributor_agreements?: ContributorAgreementInfo[];
@@ -352,7 +356,7 @@
  * from the change section.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#change-config-info
  */
-export interface ChangeConfigInfo {
+export declare interface ChangeConfigInfo {
   allow_blame?: boolean;
   large_change: number;
   update_delay: number;
@@ -460,14 +464,14 @@
  * The CommentLinkInfo entity describes acommentlink.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#commentlink-info
  */
-export interface CommentLinkInfo {
+export declare interface CommentLinkInfo {
   match: string;
   link?: string;
   enabled?: boolean;
   html?: string;
 }
 
-export interface CommentLinks {
+export declare interface CommentLinks {
   [name: string]: CommentLinkInfo;
 }
 
@@ -488,7 +492,8 @@
   resolve_conflicts_web_links?: WebLinkInfo[];
 }
 
-export interface ConfigArrayParameterInfo extends ConfigParameterInfoBase {
+export declare interface ConfigArrayParameterInfo
+  extends ConfigParameterInfoBase {
   type: ConfigParameterInfoType.ARRAY;
   values: string[];
 }
@@ -498,7 +503,7 @@
  * project configuration.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-info
  */
-export interface ConfigInfo {
+export declare interface ConfigInfo {
   description?: string;
   use_contributor_agreements?: InheritedBooleanInfo;
   use_content_merge?: InheritedBooleanInfo;
@@ -521,7 +526,8 @@
   reject_empty_commit?: InheritedBooleanInfo;
 }
 
-export interface ConfigListParameterInfo extends ConfigParameterInfoBase {
+export declare interface ConfigListParameterInfo
+  extends ConfigParameterInfoBase {
   type: ConfigParameterInfoType.LIST;
   permitted_values?: string[];
 }
@@ -535,7 +541,7 @@
  * The ConfigParameterInfo entity describes a project configurationparameter.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-parameter-info
  */
-export interface ConfigParameterInfoBase {
+export declare interface ConfigParameterInfoBase {
   display_name?: string;
   description?: string;
   warning?: string;
@@ -550,7 +556,7 @@
 }
 
 // https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#contributor-agreement-info
-export interface ContributorAgreementInfo {
+export declare interface ContributorAgreementInfo {
   name: string;
   description: string;
   url: string;
@@ -580,7 +586,7 @@
  * options.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#download-info
  */
-export interface DownloadInfo {
+export declare interface DownloadInfo {
   schemes: SchemesInfoMap;
   archives: string[];
 }
@@ -590,7 +596,7 @@
  * scheme and its commands.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html
  */
-export interface DownloadSchemeInfo {
+export declare interface DownloadSchemeInfo {
   url: string;
   is_auth_required: boolean;
   is_auth_supported: boolean;
@@ -630,7 +636,7 @@
  * the gerrit section.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#gerrit-info
  */
-export interface GerritInfo {
+export declare interface GerritInfo {
   all_projects: string; // Doc contains incorrect name
   all_users: string; // Doc contains incorrect name
   doc_search: boolean;
@@ -681,7 +687,7 @@
  * Gerrit internal group, or an external group that is known to Gerrit.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html#group-info
  */
-export interface GroupInfo {
+export declare interface GroupInfo {
   id: GroupId;
   name?: GroupName;
   url?: string;
@@ -702,7 +708,7 @@
  * Options of the group.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-groups.html
  */
-export interface GroupOptionsInfo {
+export declare interface GroupOptionsInfo {
   visible_to_all: boolean;
 }
 
@@ -714,7 +720,7 @@
  * A boolean value that can also be inherited.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#inherited-boolean-info
  */
-export interface InheritedBooleanInfo {
+export declare interface InheritedBooleanInfo {
   value: boolean;
   configured_value: InheritedBooleanInfoConfiguredValue;
   inherited_value?: boolean;
@@ -746,7 +752,7 @@
  * has.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#label-type-info
  */
-export interface LabelTypeInfo {
+export declare interface LabelTypeInfo {
   values: LabelTypeInfoValues;
   default_value: number;
 }
@@ -761,7 +767,7 @@
  * size limit of a project.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#max-object-size-limit-info
  */
-export interface MaxObjectSizeLimitInfo {
+export declare interface MaxObjectSizeLimitInfo {
   value?: string;
   configured_value?: string;
   summary?: string;
@@ -789,7 +795,7 @@
  * plugins.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#plugin-config-info
  */
-export interface PluginConfigInfo {
+export declare interface PluginConfigInfo {
   has_avatars: boolean;
   // Exists in Java class, but not mentioned in docs.
   js_resource_paths: string[];
@@ -824,7 +830,7 @@
  * The ProjectInfo entity contains information about a project
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#project-info
  */
-export interface ProjectInfo {
+export declare interface ProjectInfo {
   id: UrlEncodedRepoName;
   // name is not set if returned in a map where the project name is used as
   // map key
@@ -841,7 +847,7 @@
   web_links?: WebLinkInfo[];
 }
 
-export interface ProjectInfoWithName extends ProjectInfo {
+export declare interface ProjectInfoWithName extends ProjectInfo {
   name: RepoName;
 }
 
@@ -887,7 +893,7 @@
  * git-receive-pack behavior on the server.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#receive-info
  */
-export interface ReceiveInfo {
+export declare interface ReceiveInfo {
   enable_signed_push?: string;
 }
 
@@ -960,7 +966,7 @@
  * Gerrit server.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#server-info
  */
-export interface ServerInfo {
+export declare interface ServerInfo {
   accounts: AccountsConfigInfo;
   auth: AuthInfo;
   change: ChangeConfigInfo;
@@ -995,7 +1001,7 @@
  * project inheritance.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-type-info
  */
-export interface SubmitTypeInfo {
+export declare interface SubmitTypeInfo {
   value: Exclude<SubmitType, SubmitType.INHERIT>;
   configured_value: SubmitType;
   inherited_value: Exclude<SubmitType, SubmitType.INHERIT>;
@@ -1006,7 +1012,7 @@
  * the suggest section.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#suggest-info
  */
-export interface SuggestInfo {
+export declare interface SuggestInfo {
   from: number;
 }
 
@@ -1034,7 +1040,7 @@
  * from the user section.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-config.html#user-config-info
  */
-export interface UserConfigInfo {
+export declare interface UserConfigInfo {
   anonymous_coward_name: string;
 }
 
diff --git a/polygerrit-ui/app/api/rest.ts b/polygerrit-ui/app/api/rest.ts
index 46dad3d..86f33a9 100644
--- a/polygerrit-ui/app/api/rest.ts
+++ b/polygerrit-ui/app/api/rest.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {ProjectInfoWithName, ServerInfo} from './rest-api';
+import {AccountDetailInfo, ProjectInfoWithName, ServerInfo} from './rest-api';
 
 export type RequestPayload = string | object;
 
@@ -37,6 +37,8 @@
 
   invalidateReposCache(): void;
 
+  getAccount(): Promise<AccountDetailInfo | undefined>;
+
   getRepos(
     filter: string,
     reposPerPage: number,
diff --git a/polygerrit-ui/app/api/styles.ts b/polygerrit-ui/app/api/styles.ts
index e393667..55ac2cc 100644
--- a/polygerrit-ui/app/api/styles.ts
+++ b/polygerrit-ui/app/api/styles.ts
@@ -29,13 +29,15 @@
  */
 
 /** Lit plugins can cast Style to CSSResult. */
-export interface Style {
+export declare interface Style {
   toString(): string;
 }
 
-export interface Styles {
+export declare interface Styles {
+  font: Style;
   form: Style;
   menuPage: Style;
+  spinner: Style;
   subPage: Style;
   table: Style;
 }
diff --git a/polygerrit-ui/app/api/tsconfig.json b/polygerrit-ui/app/api/tsconfig.json
new file mode 100644
index 0000000..4d8ecac
--- /dev/null
+++ b/polygerrit-ui/app/api/tsconfig.json
@@ -0,0 +1,6 @@
+{
+  "extends": "../../../plugins/tsconfig-plugins-base.json",
+  "include": [
+    "**/*",
+  ],
+}
diff --git a/polygerrit-ui/app/compile_generated_templates.sh b/polygerrit-ui/app/compile_generated_templates.sh
new file mode 100755
index 0000000..68bf485
--- /dev/null
+++ b/polygerrit-ui/app/compile_generated_templates.sh
@@ -0,0 +1 @@
+$1 --project $2 --baseUrl ./external/ui_npm/node_modules/ --rootDir null
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 84f1c15..645e770 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -22,7 +22,6 @@
 import {DiffPreferencesInfo} from '../types/diff';
 import {EditPreferencesInfo, PreferencesInfo} from '../types/common';
 import {
-  AccountTag,
   AuthType,
   ChangeStatus,
   ConfigParameterInfoType,
@@ -42,7 +41,6 @@
 } from '../api/rest-api';
 
 export {
-  AccountTag,
   AuthType,
   ChangeStatus,
   ConfigParameterInfoType,
@@ -61,6 +59,10 @@
   SubmitType,
 };
 
+export enum AccountTag {
+  SERVICE_USER = 'SERVICE_USER',
+}
+
 export enum PrimaryTab {
   FILES = 'files',
   /**
@@ -302,3 +304,7 @@
     theme: 'DEFAULT',
   };
 }
+
+export const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
+
+export const SHOWN_ITEMS_COUNT = 25;
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index a09c1c3..a10bdda 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -23,6 +23,7 @@
   VISIBILILITY_VISIBLE = 'Visibility changed to visible',
   EXTENSION_DETECTED = 'Extension detected',
   PLUGINS_INSTALLED = 'Plugins installed',
+  PLUGINS_FAILED = 'Some plugins failed to load',
   USER_REFERRED_FROM = 'User referred from',
 }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index c41fe57..2328a05 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import '@polymer/iron-input/iron-input';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-button/gr-button';
@@ -114,7 +115,7 @@
 
   _updateSection(section: PermissionAccessSection) {
     this._permissions = toSortedPermissionsArray(section.value.permissions);
-    this._originalId = section.id as GitRef;
+    this._originalId = section.id;
   }
 
   _handleAccessSaved() {
@@ -169,7 +170,9 @@
   _computePermissions(
     name: string,
     capabilities?: CapabilityInfoMap,
-    labels?: LabelNameToLabelTypeInfoMap
+    labels?: LabelNameToLabelTypeInfoMap,
+    // This is just for triggering re-computation. We don't use the value.
+    _?: unknown
   ) {
     let allPermissions;
     const section = this.section;
@@ -226,10 +229,10 @@
   _computePermissionName(
     name: string,
     permission: PermissionArrayItem<EditablePermissionInfo>,
-    capabilities: CapabilityInfoMap
-  ) {
+    capabilities?: CapabilityInfoMap
+  ): string | undefined {
     if (name === GLOBAL_NAME) {
-      return capabilities[permission.id].name;
+      return capabilities?.[permission.id].name;
     } else if (AccessPermissions[permission.id]) {
       return AccessPermissions[permission.id].name;
     } else if (permission.value.label) {
@@ -312,7 +315,7 @@
     if (
       editing &&
       this.section &&
-      this._isEditEnabled(canUpload, ownerOf, this.section.id as GitRef)
+      this._isEditEnabled(canUpload, ownerOf, this.section.id)
     ) {
       classList.push('editing');
     }
@@ -330,7 +333,7 @@
   }
 
   _handleAddPermission() {
-    const value = this.$.permissionSelect.value;
+    const value = this.$.permissionSelect.value as GitRef;
     const permission: PermissionArrayItem<EditablePermissionInfo> = {
       id: value,
       value: {rules: {}, added: true},
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
index 46968eb..1438825 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       display: block;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index 3e52165..b438420 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -23,7 +23,6 @@
 import '../gr-create-group-dialog/gr-create-group-dialog';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-admin-group-list_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {customElement, property, observe, computed} from '@polymer/decorators';
 import {AppElementAdminParams} from '../../gr-app-types';
@@ -32,6 +31,7 @@
 import {GrCreateGroupDialog} from '../gr-create-group-dialog/gr-create-group-dialog';
 import {fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -47,7 +47,7 @@
 }
 
 @customElement('gr-admin-group-list')
-export class GrAdminGroupList extends ListViewMixin(PolymerElement) {
+export class GrAdminGroupList extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -79,7 +79,7 @@
    * */
   @computed('_groups')
   get _shownGroups() {
-    return this.computeShownItems(this._groups);
+    return this._groups.slice(0, SHOWN_ITEMS_COUNT);
   }
 
   @property({type: Number})
@@ -103,8 +103,8 @@
   @observe('params')
   _paramsChanged(params: AppElementAdminParams) {
     this._loading = true;
-    this._filter = this.getFilterValue(params);
-    this._offset = this.getOffsetValue(params);
+    this._filter = params?.filter ?? '';
+    this._offset = Number(params?.offset ?? 0);
 
     return this._getGroups(this._filter, this._groupsPerPage, this._offset);
   }
@@ -181,4 +181,8 @@
   _visibleToAll(item: GroupInfo) {
     return item.options?.visible_to_all === true ? 'Y' : 'N';
   }
+
+  computeLoadingClass(loading: boolean) {
+    return loading ? 'loading' : '';
+  }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
index a2d62341..04d3198 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.ts
@@ -16,8 +16,8 @@
  */
 import '../../shared/gr-dialog/gr-dialog';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -26,7 +26,7 @@
 }
 
 @customElement('gr-confirm-delete-item-dialog')
-export class GrConfirmDeleteItemDialog extends GrLitElement {
+export class GrConfirmDeleteItemDialog extends LitElement {
   /**
    * Fired when the confirm button is pressed.
    *
@@ -45,7 +45,7 @@
   @property({type: String})
   itemTypeName?: string;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -57,7 +57,7 @@
     ];
   }
 
-  render() {
+  override render() {
     const item = this.item ?? 'UNKNOWN ITEM';
     const itemTypeName = this.itemTypeName ?? 'UNKNOWN ITEM TYPE';
     return html` <gr-dialog
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 63b10ec..15f6f4b 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -32,7 +32,7 @@
   InheritedBooleanInfo,
 } from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
 import {appContext} from '../../../services/app-context';
 import {Subject} from 'rxjs';
@@ -41,6 +41,7 @@
   serverConfig$,
 } from '../../../services/config/config-model';
 import {takeUntil} from 'rxjs/operators';
+import {IronInputElement} from '@polymer/iron-input/iron-input';
 
 const SUGGESTIONS_LIMIT = 15;
 const REF_PREFIX = 'refs/heads/';
@@ -48,8 +49,8 @@
 export interface GrCreateChangeDialog {
   $: {
     privateChangeCheckBox: HTMLInputElement;
-    branchInput: GrAutocomplete;
-    tagNameInput: HTMLInputElement;
+    branchInput: GrTypedAutocomplete<BranchName>;
+    tagNameInput: IronInputElement;
     messageInput: IronAutogrowTextareaElement;
   };
 }
@@ -63,19 +64,19 @@
   repoName?: RepoName;
 
   @property({type: String})
-  branch?: BranchName;
+  branch = '' as BranchName;
 
   @property({type: Object})
   _repoConfig?: ConfigInfo;
 
   @property({type: String})
-  subject?: string;
+  subject = '';
 
   @property({type: String})
   topic?: string;
 
   @property({type: Object})
-  _query?: (input: string) => Promise<{name: string}[]>;
+  _query?: (input: string) => Promise<{name: BranchName}[]>;
 
   @property({type: String})
   baseChange?: ChangeId;
@@ -90,7 +91,7 @@
   canCreate = false;
 
   @property({type: Boolean})
-  _privateChangesEnabled?: boolean;
+  _privateChangesEnabled = false;
 
   restApiService = appContext.restApiService;
 
@@ -120,7 +121,7 @@
     super.disconnectedCallback();
   }
 
-  _computeBranchClass(baseChange: boolean) {
+  _computeBranchClass(baseChange?: ChangeId) {
     return baseChange ? 'hide' : '';
   }
 
@@ -165,19 +166,19 @@
       .getRepoBranches(input, this.repoName, SUGGESTIONS_LIMIT)
       .then(response => {
         if (!response) return [];
-        const branches = [];
+        const branches: Array<{name: BranchName}> = [];
         for (const branchInfo of response) {
           let name: string = branchInfo.ref;
           if (name.startsWith('refs/heads/')) {
             name = name.substring('refs/heads/'.length);
           }
-          branches.push({name});
+          branches.push({name: name as BranchName});
         }
         return branches;
       });
   }
 
-  _formatBooleanString(config: InheritedBooleanInfo) {
+  _formatBooleanString(config?: InheritedBooleanInfo) {
     if (
       config &&
       config.configured_value === InheritedBooleanInfoConfiguredValue.TRUE
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index a9de24a..9ed5d81 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -20,7 +20,11 @@
 import {GrCreateChangeDialog} from './gr-create-change-dialog';
 import {BranchName, GitRef, RepoName} from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {createChange, createConfig} from '../../../test/test-data-generators';
+import {
+  createChange,
+  createConfig,
+  TEST_CHANGE_ID,
+} from '../../../test/test-data-generators';
 import {stubRestApi} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-create-change-dialog');
@@ -130,8 +134,8 @@
   });
 
   test('_computeBranchClass', () => {
-    assert.equal(element._computeBranchClass(true), 'hide');
-    assert.equal(element._computeBranchClass(false), '');
+    assert.equal(element._computeBranchClass(TEST_CHANGE_ID), 'hide');
+    assert.equal(element._computeBranchClass(undefined), '');
   });
 
   test('_computePrivateSectionClass', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
index af33691..321f069 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
@@ -31,41 +31,33 @@
     element = basicFixture.instantiate();
   });
 
-  test('name is updated correctly', done => {
+  test('name is updated correctly', async () => {
     assert.isFalse(element.hasNewGroupName);
 
     const inputEl = element.root.querySelector('iron-input');
     inputEl.bindValue = GROUP_NAME;
 
-    setTimeout(() => {
-      assert.isTrue(element.hasNewGroupName);
-      assert.deepEqual(element._name, GROUP_NAME);
-      done();
-    });
+    await new Promise(resolve => setTimeout(resolve));
+    assert.isTrue(element.hasNewGroupName);
+    assert.deepEqual(element._name, GROUP_NAME);
   });
 
-  test('test for redirecting to group on successful creation', done => {
+  test('test for redirecting to group on successful creation', async () => {
     stubRestApi('createGroup').returns(Promise.resolve({status: 201}));
     stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
 
     const showStub = sinon.stub(page, 'show');
-    element.handleCreateGroup()
-        .then(() => {
-          assert.isTrue(showStub.calledWith('/admin/groups/551'));
-          done();
-        });
+    await element.handleCreateGroup();
+    assert.isTrue(showStub.calledWith('/admin/groups/551'));
   });
 
-  test('test for unsuccessful group creation', done => {
+  test('test for unsuccessful group creation', async () => {
     stubRestApi('createGroup').returns(Promise.resolve({status: 409}));
     stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
 
     const showStub = sinon.stub(page, 'show');
-    element.handleCreateGroup()
-        .then(() => {
-          assert.isFalse(showStub.called);
-          done();
-        });
+    await element.handleCreateGroup();
+    assert.isFalse(showStub.called);
   });
 });
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
index b6a08b87..ea0919c 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
@@ -35,7 +35,7 @@
     element = basicFixture.instantiate();
   });
 
-  test('branch created', done => {
+  test('branch created', async () => {
     stubRestApi('createRepoBranch').returns(Promise.resolve(new Response()));
 
     assert.isFalse(element.hasNewItemName);
@@ -46,15 +46,14 @@
     ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
     ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
-    setTimeout(() => {
-      assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-branch2' as BranchName);
-      assert.equal(element._itemRevision, 'HEAD');
-      done();
-    });
+    await flush();
+
+    assert.isTrue(element.hasNewItemName);
+    assert.equal(element._itemName, 'test-branch2' as BranchName);
+    assert.equal(element._itemRevision, 'HEAD');
   });
 
-  test('tag created', done => {
+  test('tag created', async () => {
     stubRestApi('createRepoTag').returns(Promise.resolve(new Response()));
 
     assert.isFalse(element.hasNewItemName);
@@ -65,15 +64,13 @@
     ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
     ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
-    setTimeout(() => {
-      assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-tag2' as BranchName);
-      assert.equal(element._itemRevision, 'HEAD');
-      done();
-    });
+    await flush();
+    assert.isTrue(element.hasNewItemName);
+    assert.equal(element._itemName, 'test-tag2' as BranchName);
+    assert.equal(element._itemRevision, 'HEAD');
   });
 
-  test('tag created with annotations', done => {
+  test('tag created with annotations', async () => {
     stubRestApi('createRepoTag').returns(Promise.resolve(new Response()));
 
     assert.isFalse(element.hasNewItemName);
@@ -86,13 +83,11 @@
     ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
     ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
-    setTimeout(() => {
-      assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-tag2' as BranchName);
-      assert.equal(element._itemAnnotation, 'test-message2');
-      assert.equal(element._itemRevision, 'HEAD');
-      done();
-    });
+    await flush();
+    assert.isTrue(element.hasNewItemName);
+    assert.equal(element._itemName, 'test-tag2' as BranchName);
+    assert.equal(element._itemAnnotation, 'test-message2');
+    assert.equal(element._itemRevision, 'HEAD');
   });
 
   test('_computeHideItemClass returns hideItem if type is branches', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
index cdc2fb3..6605350 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
@@ -17,11 +17,9 @@
 
 import '../../../styles/gr-table-styles';
 import '../../../styles/shared-styles';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-account-link/gr-account-link';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-group-audit-log_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {customElement, property} from '@polymer/decorators';
 import {
@@ -37,7 +35,7 @@
 import {ErrorCallback} from '../../../api/rest';
 
 @customElement('gr-group-audit-log')
-export class GrGroupAuditLog extends ListViewMixin(PolymerElement) {
+export class GrGroupAuditLog extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -127,6 +125,10 @@
 
     return '';
   }
+
+  computeLoadingClass(loading: boolean) {
+    return loading ? 'loading' : '';
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
index 40c2f30..828aa55 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
@@ -43,7 +43,7 @@
       <template is="dom-repeat" items="[[_auditLog]]">
         <tr class="table">
           <td class="date">
-            <gr-date-formatter has-tooltip="" date-str="[[item.date]]">
+            <gr-date-formatter withTooltip date-str="[[item.date]]">
             </gr-date-formatter>
           </td>
           <td class="type">[[itemType(item.type)]]</td>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
index 70242d8..c38f8be 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/gr-subpage-styles';
 import '../../../styles/gr-table-styles';
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts
index 0c856bc..518abac 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="gr-form-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
index 54c5099..b91b04b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
@@ -18,7 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-group-members.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {addListenerForTest, stubBaseUrl, stubRestApi} from '../../../test/test-utils.js';
+import {addListenerForTest, mockPromise, stubBaseUrl, stubRestApi} from '../../../test/test-utils.js';
 import {ItemType} from './gr-group-members.js';
 
 const basicFixture = fixtureFromElement('gr-group-members');
@@ -140,7 +140,7 @@
     'https://test/site/group/url');
   });
 
-  test('save members correctly', () => {
+  test('save members correctly', async () => {
     element._groupOwner = true;
 
     const memberName = 'test-admin';
@@ -155,6 +155,7 @@
     element.$.groupMemberSearchInput.text = memberName;
     element.$.groupMemberSearchInput.value = 1234;
 
+    await flush();
     assert.isFalse(button.hasAttribute('disabled'));
 
     return element._handleSavingGroupMember().then(() => {
@@ -165,7 +166,7 @@
     });
   });
 
-  test('save included groups correctly', () => {
+  test('save included groups correctly', async () => {
     element._groupOwner = true;
 
     const includedGroupName = 'testName';
@@ -179,7 +180,7 @@
 
     element.$.includedGroupSearchInput.text = includedGroupName;
     element.$.includedGroupSearchInput.value = 'testId';
-
+    await flush();
     assert.isFalse(button.hasAttribute('disabled'));
 
     return element._handleSavingIncludedGroups().then(() => {
@@ -235,42 +236,32 @@
     assert.isTrue(exceptionThrown);
   });
 
-  test('_getAccountSuggestions empty', done => {
-    element
-        ._getAccountSuggestions('nonexistent').then(accounts => {
-          assert.equal(accounts.length, 0);
-          done();
-        });
+  test('_getAccountSuggestions empty', async () => {
+    const accounts = await element._getAccountSuggestions('nonexistent');
+    assert.equal(accounts.length, 0);
   });
 
-  test('_getAccountSuggestions non-empty', done => {
-    element
-        ._getAccountSuggestions('test-').then(accounts => {
-          assert.equal(accounts.length, 3);
-          assert.equal(accounts[0].name,
-              'test-account <test.account@example.com>');
-          assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
-          assert.equal(accounts[2].name, 'test-git');
-          done();
-        });
+  test('_getAccountSuggestions non-empty', async () => {
+    const accounts = await element._getAccountSuggestions('test-');
+    assert.equal(accounts.length, 3);
+    assert.equal(accounts[0].name,
+        'test-account <test.account@example.com>');
+    assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
+    assert.equal(accounts[2].name, 'test-git');
   });
 
-  test('_getGroupSuggestions empty', done => {
-    element
-        ._getGroupSuggestions('nonexistent').then(groups => {
-          assert.equal(groups.length, 0);
-          done();
-        });
+  test('_getGroupSuggestions empty', async () => {
+    const groups = await element._getGroupSuggestions('nonexistent');
+
+    assert.equal(groups.length, 0);
   });
 
-  test('_getGroupSuggestions non-empty', done => {
-    element
-        ._getGroupSuggestions('test').then(groups => {
-          assert.equal(groups.length, 2);
-          assert.equal(groups[0].name, 'test-admin');
-          assert.equal(groups[1].name, 'test/Administrator (admin)');
-          done();
-        });
+  test('_getGroupSuggestions non-empty', async () => {
+    const groups = await element._getGroupSuggestions('test');
+
+    assert.equal(groups.length, 2);
+    assert.equal(groups[0].name, 'test-admin');
+    assert.equal(groups[1].name, 'test/Administrator (admin)');
   });
 
   test('_computeHideItemClass returns string for admin', () => {
@@ -343,22 +334,24 @@
     assert.equal(element._computeGroupUrl(url), url);
   });
 
-  test('fires page-error', done => {
+  test('fires page-error', async () => {
     groupStub.restore();
 
     element.groupId = 1;
 
     const response = {status: 404};
-    stubRestApi('getGroupConfig')
-        .callsFake((group, errFn) => {
-          errFn(response);
-        });
+    stubRestApi('getGroupConfig').callsFake((group, errFn) => {
+      errFn(response);
+      return Promise.resolve();
+    });
+    const promise = mockPromise();
     addListenerForTest(document, 'page-error', e => {
       assert.deepEqual(e.detail.response, response);
-      done();
+      promise.resolve();
     });
 
     element._loadGroupDetails();
+    await promise;
   });
 
   test('_computeItemName', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 18862b5..596fe5b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -16,6 +16,7 @@
  */
 
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/gr-subpage-styles';
 import '../../../styles/shared-styles';
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
index 98d21f9..6bc5d2a 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
index 0668330..e390ac5 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
@@ -17,7 +17,11 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-group.js';
-import {stubRestApi, addListenerForTest} from '../../../test/test-utils.js';
+import {
+  addListenerForTest,
+  mockPromise,
+  stubRestApi,
+} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-group');
 
@@ -49,17 +53,15 @@
         .display === 'none');
   });
 
-  test('default values are populated with internal group', done => {
+  test('default values are populated with internal group', async () => {
     stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     element.groupId = 1;
-    element._loadGroup().then(() => {
-      assert.isTrue(element._groupIsInternal);
-      assert.isFalse(element.$.visibleToAll.bindValue);
-      done();
-    });
+    await element._loadGroup();
+    assert.isTrue(element._groupIsInternal);
+    assert.isFalse(element.$.visibleToAll.bindValue);
   });
 
-  test('default values with external group', done => {
+  test('default values with external group', async () => {
     const groupExternal = {...group};
     groupExternal.id = 'external-group-id';
     groupStub.restore();
@@ -67,14 +69,12 @@
         Promise.resolve(groupExternal));
     stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     element.groupId = 1;
-    element._loadGroup().then(() => {
-      assert.isFalse(element._groupIsInternal);
-      assert.isFalse(element.$.visibleToAll.bindValue);
-      done();
-    });
+    await element._loadGroup();
+    assert.isFalse(element._groupIsInternal);
+    assert.isFalse(element.$.visibleToAll.bindValue);
   });
 
-  test('rename group', done => {
+  test('rename group', async () => {
     const groupName = 'test-group';
     const groupName2 = 'test-group2';
     element.groupId = 1;
@@ -88,25 +88,23 @@
 
     const button = element.$.inputUpdateNameBtn;
 
-    element._loadGroup().then(() => {
-      assert.isTrue(button.hasAttribute('disabled'));
-      assert.isFalse(element.$.Title.classList.contains('edited'));
+    await element._loadGroup();
+    assert.isTrue(button.hasAttribute('disabled'));
+    assert.isFalse(element.$.Title.classList.contains('edited'));
 
-      element.$.groupNameInput.text = groupName2;
+    element.$.groupNameInput.text = groupName2;
 
-      assert.isFalse(button.hasAttribute('disabled'));
-      assert.isTrue(element.$.groupName.classList.contains('edited'));
+    await flush();
+    assert.isFalse(button.hasAttribute('disabled'));
+    assert.isTrue(element.$.groupName.classList.contains('edited'));
 
-      element._handleSaveName().then(() => {
-        assert.isTrue(button.hasAttribute('disabled'));
-        assert.isFalse(element.$.Title.classList.contains('edited'));
-        assert.equal(element._groupName, groupName2);
-        done();
-      });
-    });
+    await element._handleSaveName();
+    assert.isTrue(button.hasAttribute('disabled'));
+    assert.isFalse(element.$.Title.classList.contains('edited'));
+    assert.equal(element._groupName, groupName2);
   });
 
-  test('rename group owner', done => {
+  test('rename group owner', async () => {
     const groupName = 'test-group';
     element.groupId = 1;
     element._groupConfig = {
@@ -119,24 +117,22 @@
 
     const button = element.$.inputUpdateOwnerBtn;
 
-    element._loadGroup().then(() => {
-      assert.isTrue(button.hasAttribute('disabled'));
-      assert.isFalse(element.$.Title.classList.contains('edited'));
+    await element._loadGroup();
+    assert.isTrue(button.hasAttribute('disabled'));
+    assert.isFalse(element.$.Title.classList.contains('edited'));
 
-      element.$.groupOwnerInput.text = 'testId2';
+    element.$.groupOwnerInput.text = 'testId2';
 
-      assert.isFalse(button.hasAttribute('disabled'));
-      assert.isTrue(element.$.groupOwner.classList.contains('edited'));
+    await flush();
+    assert.isFalse(button.hasAttribute('disabled'));
+    assert.isTrue(element.$.groupOwner.classList.contains('edited'));
 
-      element._handleSaveOwner().then(() => {
-        assert.isTrue(button.hasAttribute('disabled'));
-        assert.isFalse(element.$.Title.classList.contains('edited'));
-        done();
-      });
-    });
+    await element._handleSaveOwner();
+    assert.isTrue(button.hasAttribute('disabled'));
+    assert.isFalse(element.$.Title.classList.contains('edited'));
   });
 
-  test('test for undefined group name', done => {
+  test('test for undefined group name', async () => {
     groupStub.restore();
 
     stubRestApi('getGroupConfig').returns(Promise.resolve({}));
@@ -149,16 +145,13 @@
 
     // Test that loading shows instead of filling
     // in group details
-    element._loadGroup().then(() => {
-      assert.isTrue(element.$.loading.classList.contains('loading'));
+    await element._loadGroup();
+    assert.isTrue(element.$.loading.classList.contains('loading'));
 
-      assert.isTrue(element._loading);
-
-      done();
-    });
+    assert.isTrue(element._loading);
   });
 
-  test('test fire event', done => {
+  test('test fire event', async () => {
     element._groupConfig = {
       name: 'test-group',
     };
@@ -166,11 +159,8 @@
     stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
 
     const showStub = sinon.stub(element, 'dispatchEvent');
-    element._handleSaveName()
-        .then(() => {
-          assert.isTrue(showStub.called);
-          done();
-        });
+    await element._handleSaveName();
+    assert.isTrue(showStub.called);
   });
 
   test('_computeGroupDisabled', () => {
@@ -206,7 +196,7 @@
     assert.equal(element._computeLoadingClass(false), '');
   });
 
-  test('fires page-error', done => {
+  test('fires page-error', async () => {
     groupStub.restore();
 
     element.groupId = 1;
@@ -214,14 +204,17 @@
     const response = {status: 404};
     stubRestApi('getGroupConfig').callsFake((group, errFn) => {
       errFn(response);
+      return Promise.resolve(undefined);
     });
 
+    const promise = mockPromise();
     addListenerForTest(document, 'page-error', e => {
       assert.deepEqual(e.detail.response, response);
-      done();
+      promise.resolve();
     });
 
     element._loadGroup();
+    await promise;
   });
 
   test('uuid', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index f275dfd..9f51688 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -19,21 +19,21 @@
 import '../../shared/gr-list-view/gr-list-view';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-plugin-list_html';
-import {
-  ListViewMixin,
-  ListViewParams,
-} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {customElement, property} from '@polymer/decorators';
 import {PluginInfo} from '../../../types/common';
 import {firePageError, fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {ListViewParams} from '../../gr-app-types';
 
 interface PluginInfoWithName extends PluginInfo {
   name: string;
 }
+
 @customElement('gr-plugin-list')
-export class GrPluginList extends ListViewMixin(PolymerElement) {
+export class GrPluginList extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -81,8 +81,8 @@
 
   _paramsChanged(params: ListViewParams) {
     this._loading = true;
-    this._filter = this.getFilterValue(params);
-    this._offset = this.getOffsetValue(params);
+    this._filter = params?.filter ?? '';
+    this._offset = Number(params?.offset ?? 0);
 
     return this._getPlugins(this._filter, this._pluginsPerPage, this._offset);
   }
@@ -110,7 +110,15 @@
   }
 
   _computePluginUrl(id: string) {
-    return this.getUrl('/', id);
+    return getBaseUrl() + '/' + encodeURL(id, true);
+  }
+
+  computeLoadingClass(loading: boolean) {
+    return loading ? 'loading' : '';
+  }
+
+  computeShownItems(plugins: PluginInfoWithName[]) {
+    return plugins.slice(0, SHOWN_ITEMS_COUNT);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
index a9281e4..62c89b2 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
@@ -18,7 +18,10 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-plugin-list.js';
 import 'lodash/lodash.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
+import {
+  addListenerForTest,
+  mockPromise,
+  stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-plugin-list');
 
@@ -52,52 +55,45 @@
     counter = 0;
   });
 
-  suite('list with plugins', () => {
-    setup(done => {
+  suite('list with plugins', async () => {
+    setup(async () => {
       plugins = _.times(26, pluginGenerator);
       stubRestApi('getPlugins').returns(Promise.resolve(plugins));
-      element._paramsChanged(value).then(() => { flush(done); });
+      await element._paramsChanged(value);
+      await flush();
     });
 
-    test('plugin in the list is formatted correctly', done => {
-      flush(() => {
-        assert.equal(element._plugins[4].id, 'test5');
-        assert.equal(element._plugins[4].index_url, 'plugins/test5/');
-        assert.equal(element._plugins[4].version, 'version-5');
-        assert.equal(element._plugins[4].api_version, 'api-version-5');
-        assert.equal(element._plugins[4].disabled, false);
-        done();
-      });
+    test('plugin in the list is formatted correctly', async () => {
+      await flush();
+      assert.equal(element._plugins[4].id, 'test5');
+      assert.equal(element._plugins[4].index_url, 'plugins/test5/');
+      assert.equal(element._plugins[4].version, 'version-5');
+      assert.equal(element._plugins[4].api_version, 'api-version-5');
+      assert.equal(element._plugins[4].disabled, false);
     });
 
-    test('with and without urls', done => {
-      flush(() => {
-        const names = element.root.querySelectorAll('.name');
-        assert.isOk(names[1].querySelector('a'));
-        assert.equal(names[1].querySelector('a').innerText, 'test1');
-        assert.isNotOk(names[2].querySelector('a'));
-        assert.equal(names[2].innerText, 'test2');
-        done();
-      });
+    test('with and without urls', async () => {
+      await flush();
+      const names = element.root.querySelectorAll('.name');
+      assert.isOk(names[1].querySelector('a'));
+      assert.equal(names[1].querySelector('a').innerText, 'test1');
+      assert.isNotOk(names[2].querySelector('a'));
+      assert.equal(names[2].innerText, 'test2');
     });
 
-    test('versions', done => {
-      flush(() => {
-        const versions = element.root.querySelectorAll('.version');
-        assert.equal(versions[2].innerText, 'version-2');
-        assert.equal(versions[3].innerText, '--');
-        done();
-      });
+    test('versions', async () => {
+      await flush();
+      const versions = element.root.querySelectorAll('.version');
+      assert.equal(versions[2].innerText, 'version-2');
+      assert.equal(versions[3].innerText, '--');
     });
 
-    test('api versions', done => {
-      flush(() => {
-        const apiVersions = element.root.querySelectorAll(
-            '.apiVersion');
-        assert.equal(apiVersions[3].innerText, 'api-version-3');
-        assert.equal(apiVersions[4].innerText, '--');
-        done();
-      });
+    test('api versions', async () => {
+      await flush();
+      const apiVersions = element.root.querySelectorAll(
+          '.apiVersion');
+      assert.equal(apiVersions[3].innerText, 'api-version-3');
+      assert.equal(apiVersions[4].innerText, '--');
     });
 
     test('_shownPlugins', () => {
@@ -106,10 +102,11 @@
   });
 
   suite('list with less then 26 plugins', () => {
-    setup(done => {
+    setup(async () => {
       plugins = _.times(25, pluginGenerator);
       stubRestApi('getPlugins').returns(Promise.resolve(plugins));
-      element._paramsChanged(value).then(() => { flush(done); });
+      await element._paramsChanged(value);
+      await flush();
     });
 
     test('_shownPlugins', () => {
@@ -133,7 +130,7 @@
   });
 
   suite('loading', () => {
-    test('correct contents are displayed', () => {
+    test('correct contents are displayed', async () => {
       assert.isTrue(element._loading);
       assert.equal(element.computeLoadingClass(element._loading), 'loading');
       assert.equal(getComputedStyle(element.$.loading).display, 'block');
@@ -141,30 +138,33 @@
       element._loading = false;
       element._plugins = _.times(25, pluginGenerator);
 
-      flush();
+      await flush();
       assert.equal(element.computeLoadingClass(element._loading), '');
       assert.equal(getComputedStyle(element.$.loading).display, 'none');
     });
   });
 
   suite('404', () => {
-    test('fires page-error', done => {
+    test('fires page-error', async () => {
       const response = {status: 404};
       stubRestApi('getPlugins').callsFake(
           (filter, pluginsPerPage, opt_offset, errFn) => {
             errFn(response);
+            return Promise.resolve(undefined);
           });
 
+      const promise = mockPromise();
       addListenerForTest(document, 'page-error', e => {
         assert.deepEqual(e.detail.response, response);
-        done();
+        promise.resolve();
       });
 
       const value = {
         filter: 'test',
         offset: 25,
       };
-      element._paramsChanged(value);
+      await element._paramsChanged(value);
+      await promise;
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
index 08493c5..6136f1e1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access-interfaces.ts
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 /**
- * @fileOverview This file contains interfaces shared between gr-repo-access
+ * @fileoverview This file contains interfaces shared between gr-repo-access
  * and nested elements (gr-access-section, gr-permission)
  */
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 9c98545..56e981a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-menu-page-styles';
 import '../../../styles/gr-subpage-styles';
 import '../../../styles/shared-styles';
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
index f5ef86b..65f0564 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
index 2b7fdf5..1ccfd5e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
@@ -20,7 +20,11 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {toSortedPermissionsArray} from '../../../utils/access-util.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
+import {
+  addListenerForTest,
+  mockPromise,
+  stubRestApi,
+} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-access');
 
@@ -238,19 +242,22 @@
     assert.equal(element._computeLoadingClass(false), '');
   });
 
-  test('fires page-error', done => {
+  test('fires page-error', async () => {
     const response = {status: 404};
 
     stubRestApi('getRepoAccessRights').callsFake((repoName, errFn) => {
       errFn(response);
+      return Promise.resolve(undefined);
     });
 
+    const promise = mockPromise();
     addListenerForTest(document, 'page-error', e => {
       assert.deepEqual(e.detail.response, response);
-      done();
+      promise.resolve();
     });
 
     element.repo = 'test';
+    await promise;
   });
 
   suite('with defined sections', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index 3cef13f..de43dc6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/gr-subpage-styles';
 import '../../../styles/shared-styles';
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
index f572ac3..9948f8f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
@@ -36,11 +39,11 @@
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
       <h2 id="options" class="heading-2">Command</h2>
       <div id="form">
-        <h3>Create change</h3>
+        <h3 class="heading-3">Create change</h3>
         <gr-button loading="[[_creatingChange]]" on-click="_createNewChange">
           Create change
         </gr-button>
-        <h3>Edit repo config</h3>
+        <h3 class="heading-3">Edit repo config</h3>
         <gr-button
           id="editRepoConfig"
           loading="[[_editingConfig]]"
@@ -48,7 +51,7 @@
         >
           Edit repo config
         </gr-button>
-        <h3 hidden="[[!_repoConfig.actions.gc.enabled]]">
+        <h3 class="heading-3" hidden="[[!_repoConfig.actions.gc.enabled]]">
           [[_repoConfig.actions.gc.label]]
         </h3>
         <gr-button
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
index 893efe55..ac48484 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
@@ -18,7 +18,11 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-repo-commands.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
+import {
+  addListenerForTest,
+  mockPromise,
+  stubRestApi,
+} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-commands');
 
@@ -112,7 +116,7 @@
   });
 
   suite('404', () => {
-    test('fires page-error', done => {
+    test('fires page-error', async () => {
       repoStub.restore();
 
       element.repo = 'test';
@@ -120,13 +124,18 @@
       const response = {status: 404};
       stubRestApi('getProjectConfig').callsFake((repo, errFn) => {
         errFn(response);
+        return Promise.resolve(undefined);
       });
+
+      await flush();
+      const promise = mockPromise();
       addListenerForTest(document, 'page-error', e => {
         assert.deepEqual(e.detail.response, response);
-        done();
+        promise.resolve();
       });
 
       element._loadRepo();
+      await promise;
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index 4cc0a80..7800653 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -22,8 +22,8 @@
 import {ErrorCallback} from '../../../api/rest';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, PropertyValues} from 'lit-element';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 interface DashboardRef {
   section: string;
@@ -31,7 +31,7 @@
 }
 
 @customElement('gr-repo-dashboards')
-export class GrRepoDashboards extends GrLitElement {
+export class GrRepoDashboards extends LitElement {
   @property({type: String})
   repo?: RepoName;
 
@@ -43,7 +43,7 @@
 
   private readonly restApiService = appContext.restApiService;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       tableStyles,
@@ -63,7 +63,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <table
       id="list"
       class="genericList ${this._computeLoadingClass(this._loading)}"
@@ -114,7 +114,7 @@
     </table>`;
   }
 
-  updated(changedProperties: PropertyValues) {
+  override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('repo')) {
       this.repoChanged();
     }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
index ede2bb9..a54eafb 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
@@ -21,6 +21,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {
   addListenerForTest,
+  mockPromise,
   queryAndAssert,
   stubRestApi,
 } from '../../../test/test-utils';
@@ -92,7 +93,7 @@
       );
     });
 
-    test('loading, sections, and ordering', done => {
+    test('loading, sections, and ordering', async () => {
       assert.isTrue(element._loading);
       assert.notEqual(
         getComputedStyle(queryAndAssert(element, '#loadingContainer')).display,
@@ -103,29 +104,25 @@
         'none'
       );
       element.repo = 'test' as RepoName;
-      flush(() => {
-        assert.equal(
-          getComputedStyle(queryAndAssert(element, '#loadingContainer'))
-            .display,
-          'none'
-        );
-        assert.notEqual(
-          getComputedStyle(queryAndAssert(element, '#dashboards')).display,
-          'none'
-        );
+      await flush();
+      assert.equal(
+        getComputedStyle(queryAndAssert(element, '#loadingContainer')).display,
+        'none'
+      );
+      assert.notEqual(
+        getComputedStyle(queryAndAssert(element, '#dashboards')).display,
+        'none'
+      );
 
-        const dashboard = element._dashboards!;
-        assert.equal(dashboard.length!, 2);
-        assert.equal(dashboard[0].section!, 'custom');
-        assert.equal(dashboard[1].section!, 'default');
+      const dashboard = element._dashboards!;
+      assert.equal(dashboard.length!, 2);
+      assert.equal(dashboard[0].section!, 'custom');
+      assert.equal(dashboard[1].section!, 'default');
 
-        const dashboards = dashboard[0].dashboards;
-        assert.equal(dashboards.length, 2);
-        assert.equal(dashboards[0].id, 'custom:custom1');
-        assert.equal(dashboards[1].id, 'custom:custom2');
-
-        done();
-      });
+      const dashboards = dashboard[0].dashboards;
+      assert.equal(dashboards.length, 2);
+      assert.equal(dashboards[0].id, 'custom:custom1');
+      assert.equal(dashboards[1].id, 'custom:custom2');
     });
   });
 
@@ -148,19 +145,21 @@
   });
 
   suite('404', () => {
-    test('fires page-error', done => {
+    test('fires page-error', async () => {
       const response = {status: 404} as Response;
       stubRestApi('getRepoDashboards').callsFake((_repo, errFn) => {
         errFn!(response);
         return Promise.resolve([]);
       });
 
+      const promise = mockPromise();
       addListenerForTest(document, 'page-error', e => {
         assert.deepEqual((e as PageErrorEvent).detail.response, response);
-        done();
+        promise.resolve();
       });
 
       element.repo = 'test' as RepoName;
+      await promise;
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
index e243974..00c5999 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
@@ -30,7 +30,6 @@
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-repo-detail-list_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {encodeURL} from '../../../utils/url-util';
 import {customElement, property} from '@polymer/decorators';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
@@ -49,6 +48,7 @@
 import {firePageError} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
 
 const PGP_START = '-----BEGIN PGP SIGNATURE-----';
 
@@ -59,8 +59,9 @@
     createNewModal: GrCreatePointerDialog;
   };
 }
+
 @customElement('gr-repo-detail-list')
-export class GrRepoDetailList extends ListViewMixin(PolymerElement) {
+export class GrRepoDetailList extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -151,8 +152,8 @@
 
     this.detailType = params.detail;
 
-    this._filter = this.getFilterValue(params);
-    this._offset = this.getOffsetValue(params);
+    this._filter = params?.filter ?? '';
+    this._offset = Number(params?.offset ?? 0);
     if (!this.detailType)
       return Promise.reject(new Error('undefined detailType'));
 
@@ -391,6 +392,14 @@
   _computeHideTagger(tagger?: GitPersonInfo) {
     return tagger ? '' : 'hide';
   }
+
+  computeLoadingClass(loading: boolean) {
+    return loading ? 'loading' : '';
+  }
+
+  computeShownItems(items: BranchInfo[] | TagInfo[]) {
+    return items.slice(0, SHOWN_ITEMS_COUNT);
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
index 4f66f0d..429a6d6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
@@ -145,10 +145,7 @@
             <td class$="tagger [[_hideIfBranch(detailType)]]">
               <div class$="tagger [[_computeHideTagger(item.tagger)]]">
                 <gr-account-link account="[[item.tagger]]"> </gr-account-link>
-                (<gr-date-formatter
-                  has-tooltip=""
-                  date-str="[[item.tagger.date]]"
-                >
+                (<gr-date-formatter withTooltip date-str="[[item.tagger.date]]">
                 </gr-date-formatter
                 >)
               </div>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
index 1af9c02..d5eb5d5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
@@ -20,7 +20,11 @@
 import 'lodash/lodash.js';
 import {page} from '../../../utils/page-wrapper-utils.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
+import {
+  addListenerForTest,
+  mockPromise,
+  stubRestApi,
+} from '../../../test/test-utils.js';
 import {RepoDetailView} from '../../core/gr-navigation/gr-navigation.js';
 
 const basicFixture = fixtureFromElement('gr-repo-detail-list');
@@ -71,7 +75,7 @@
     });
 
     suite('list of repo branches', () => {
-      setup(done => {
+      setup(async () => {
         branches = [{
           ref: 'HEAD',
           revision: 'master',
@@ -82,53 +86,43 @@
           repo: 'test',
           detail: 'branches',
         };
-        element._paramsChanged(params).then(() => { flush(done); });
+        await element._paramsChanged(params);
+        await flush();
       });
 
-      test('test for branch in the list', done => {
-        flush(() => {
-          assert.equal(element._items[2].ref, 'refs/heads/test2');
-          done();
-        });
+      test('test for branch in the list', () => {
+        assert.equal(element._items[2].ref, 'refs/heads/test2');
       });
 
-      test('test for web links in the branches list', done => {
-        flush(() => {
-          assert.equal(element._items[2].web_links[0].url,
-              'https://git.example.org/branch/test;refs/heads/test2');
-          done();
-        });
+      test('test for web links in the branches list', () => {
+        assert.equal(element._items[2].web_links[0].url,
+            'https://git.example.org/branch/test;refs/heads/test2');
       });
 
-      test('test for refs/heads/ being striped from ref', done => {
-        flush(() => {
-          assert.equal(element._stripRefs(element._items[2].ref,
-              element.detailType), 'test2');
-          done();
-        });
+      test('test for refs/heads/ being striped from ref', () => {
+        assert.equal(element._stripRefs(element._items[2].ref,
+            element.detailType), 'test2');
       });
 
       test('_shownItems', () => {
         assert.equal(element._shownItems.length, 25);
       });
 
-      test('Edit HEAD button not admin', done => {
+      test('Edit HEAD button not admin', async () => {
         sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
         stubRestApi('getRepoAccess').returns(
             Promise.resolve({
               test: {is_owner: false},
             }));
-        element._determineIfOwner('test').then(() => {
-          assert.equal(element._isOwner, false);
-          assert.equal(getComputedStyle(dom(element.root)
-              .querySelector('.revisionNoEditing')).display, 'inline');
-          assert.equal(getComputedStyle(dom(element.root)
-              .querySelector('.revisionEdit')).display, 'none');
-          done();
-        });
+        await element._determineIfOwner('test');
+        assert.equal(element._isOwner, false);
+        assert.equal(getComputedStyle(dom(element.root)
+            .querySelector('.revisionNoEditing')).display, 'inline');
+        assert.equal(getComputedStyle(dom(element.root)
+            .querySelector('.revisionEdit')).display, 'none');
       });
 
-      test('Edit HEAD button admin', done => {
+      test('Edit HEAD button admin', async () => {
         const saveBtn = element.root.querySelector('.saveBtn');
         const cancelBtn = element.root.querySelector('.cancelBtn');
         const editBtn = element.root.querySelector('.editBtn');
@@ -143,76 +137,74 @@
               test: {is_owner: true},
             }));
         sinon.stub(element, '_handleSaveRevision');
-        element._determineIfOwner('test').then(() => {
-          assert.equal(element._isOwner, true);
-          // The revision container for non-editing enabled row is not visible.
-          assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
+        await element._determineIfOwner('test');
+        assert.equal(element._isOwner, true);
+        // The revision container for non-editing enabled row is not visible.
+        assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
 
-          // The revision container for editing enabled row is visible.
-          assert.notEqual(getComputedStyle(dom(element.root)
-              .querySelector('.revisionEdit')).display, 'none');
+        // The revision container for editing enabled row is visible.
+        assert.notEqual(getComputedStyle(dom(element.root)
+            .querySelector('.revisionEdit')).display, 'none');
 
-          // The revision and edit button are visible.
-          assert.notEqual(getComputedStyle(revisionWithEditing).display,
-              'none');
-          assert.notEqual(getComputedStyle(editBtn).display, 'none');
+        // The revision and edit button are visible.
+        assert.notEqual(getComputedStyle(revisionWithEditing).display,
+            'none');
+        assert.notEqual(getComputedStyle(editBtn).display, 'none');
 
-          // The input, cancel, and save buttons are not visible.
-          const hiddenElements = dom(element.root)
-              .querySelectorAll('.canEdit .editItem');
+        // The input, cancel, and save buttons are not visible.
+        const hiddenElements = dom(element.root)
+            .querySelectorAll('.canEdit .editItem');
 
-          for (const item of hiddenElements) {
-            assert.equal(getComputedStyle(item).display, 'none');
-          }
+        for (const item of hiddenElements) {
+          assert.equal(getComputedStyle(item).display, 'none');
+        }
 
-          MockInteractions.tap(editBtn);
-          flush();
-          // The revision and edit button are not visible.
-          assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
-          assert.equal(getComputedStyle(editBtn).display, 'none');
+        MockInteractions.tap(editBtn);
+        await flush();
+        // The revision and edit button are not visible.
+        assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
+        assert.equal(getComputedStyle(editBtn).display, 'none');
 
-          // The input, cancel, and save buttons are not visible.
-          for (const item of hiddenElements) {
-            assert.notEqual(getComputedStyle(item).display, 'none');
-          }
+        // The input, cancel, and save buttons are not visible.
+        for (const item of hiddenElements) {
+          assert.notEqual(getComputedStyle(item).display, 'none');
+        }
 
-          // The revised ref was set correctly
-          assert.equal(element._revisedRef, 'master');
+        // The revised ref was set correctly
+        assert.equal(element._revisedRef, 'master');
 
-          assert.isFalse(saveBtn.disabled);
+        assert.isFalse(saveBtn.disabled);
 
-          // Delete the ref.
-          element._revisedRef = '';
-          assert.isTrue(saveBtn.disabled);
+        // Delete the ref.
+        element._revisedRef = '';
+        assert.isTrue(saveBtn.disabled);
 
-          // Change the ref to something else
-          element._revisedRef = 'newRef';
-          element._repo = 'test';
-          assert.isFalse(saveBtn.disabled);
+        // Change the ref to something else
+        element._revisedRef = 'newRef';
+        element._repo = 'test';
+        assert.isFalse(saveBtn.disabled);
 
-          // Save button calls handleSave. since this is stubbed, the edit
-          // section remains open.
-          MockInteractions.tap(saveBtn);
-          assert.isTrue(element._handleSaveRevision.called);
+        // Save button calls handleSave. since this is stubbed, the edit
+        // section remains open.
+        MockInteractions.tap(saveBtn);
+        assert.isTrue(element._handleSaveRevision.called);
 
-          // When cancel is tapped, the edit secion closes.
-          MockInteractions.tap(cancelBtn);
-          flush();
+        // When cancel is tapped, the edit secion closes.
+        MockInteractions.tap(cancelBtn);
+        await flush();
 
-          // The revision and edit button are visible.
-          assert.notEqual(getComputedStyle(revisionWithEditing).display,
-              'none');
-          assert.notEqual(getComputedStyle(editBtn).display, 'none');
+        // The revision and edit button are visible.
+        assert.notEqual(getComputedStyle(revisionWithEditing).display,
+            'none');
+        assert.notEqual(getComputedStyle(editBtn).display, 'none');
 
-          // The input, cancel, and save buttons are not visible.
-          for (const item of hiddenElements) {
-            assert.equal(getComputedStyle(item).display, 'none');
-          }
-          done();
-        });
+        // The input, cancel, and save buttons are not visible.
+        for (const item of hiddenElements) {
+          assert.equal(getComputedStyle(item).display, 'none');
+        }
       });
 
-      test('_handleSaveRevision with invalid rev', done => {
+      test('_handleSaveRevision with invalid rev', async () => {
         const event = {model: {set: sinon.stub()}};
         element._isEditing = true;
         stubRestApi('setRepoHead').returns(
@@ -221,14 +213,12 @@
             })
         );
 
-        element._setRepoHead('test', 'newRef', event).then(() => {
-          assert.isTrue(element._isEditing);
-          assert.isFalse(event.model.set.called);
-          done();
-        });
+        await element._setRepoHead('test', 'newRef', event);
+        assert.isTrue(element._isEditing);
+        assert.isFalse(event.model.set.called);
       });
 
-      test('_handleSaveRevision with valid rev', done => {
+      test('_handleSaveRevision with valid rev', async () => {
         const event = {model: {set: sinon.stub()}};
         element._isEditing = true;
         stubRestApi('setRepoHead').returns(
@@ -237,11 +227,9 @@
             })
         );
 
-        element._setRepoHead('test', 'newRef', event).then(() => {
-          assert.isFalse(element._isEditing);
-          assert.isTrue(event.model.set.called);
-          done();
-        });
+        await element._setRepoHead('test', 'newRef', event);
+        assert.isFalse(element._isEditing);
+        assert.isTrue(event.model.set.called);
       });
 
       test('test _computeItemName', () => {
@@ -251,7 +239,7 @@
     });
 
     suite('list with less then 25 branches', () => {
-      setup(done => {
+      setup(async () => {
         branches = _.times(25, branchGenerator);
         stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
 
@@ -260,7 +248,8 @@
           detail: 'branches',
         };
 
-        element._paramsChanged(params).then(() => { flush(done); });
+        await element._paramsChanged(params);
+        await flush();
       });
 
       test('_shownItems', () => {
@@ -287,16 +276,18 @@
     });
 
     suite('404', () => {
-      test('fires page-error', done => {
+      test('fires page-error', async () => {
         const response = {status: 404};
         stubRestApi('getRepoBranches').callsFake(
             (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
               errFn(response);
+              return Promise.resolve();
             });
 
+        const promise = mockPromise();
         addListenerForTest(document, 'page-error', e => {
           assert.deepEqual(e.detail.response, response);
-          done();
+          promise.resolve();
         });
 
         const params = {
@@ -306,6 +297,7 @@
           offset: 25,
         };
         element._paramsChanged(params);
+        await promise;
       });
     });
   });
@@ -341,7 +333,7 @@
     });
 
     suite('list of repo tags', () => {
-      setup(done => {
+      setup(async () => {
         tags = _.times(26, tagGenerator);
         stubRestApi('getRepoTags').returns(Promise.resolve(tags));
 
@@ -350,50 +342,37 @@
           detail: 'tags',
         };
 
-        element._paramsChanged(params).then(() => { flush(done); });
+        await element._paramsChanged(params);
+        await flush();
       });
 
-      test('test for tag in the list', done => {
-        flush(() => {
-          assert.equal(element._items[1].ref, 'refs/tags/test2');
-          done();
-        });
+      test('test for tag in the list', async () => {
+        assert.equal(element._items[1].ref, 'refs/tags/test2');
       });
 
-      test('test for tag message in the list', done => {
-        flush(() => {
-          assert.equal(element._items[1].message, 'Annotated tag');
-          done();
-        });
+      test('test for tag message in the list', async () => {
+        assert.equal(element._items[1].message, 'Annotated tag');
       });
 
-      test('test for tagger in the tag list', done => {
+      test('test for tagger in the tag list', async () => {
         const tagger = {
           name: 'Test User',
           email: 'test.user@gmail.com',
           date: '2017-09-19 14:54:00.000000000',
           tz: 540,
         };
-        flush(() => {
-          assert.deepEqual(element._items[1].tagger, tagger);
-          done();
-        });
+
+        assert.deepEqual(element._items[1].tagger, tagger);
       });
 
-      test('test for web links in the tags list', done => {
-        flush(() => {
-          assert.equal(element._items[1].web_links[0].url,
-              'https://git.example.org/tag/test;refs/tags/test2');
-          done();
-        });
+      test('test for web links in the tags list', async () => {
+        assert.equal(element._items[1].web_links[0].url,
+            'https://git.example.org/tag/test;refs/tags/test2');
       });
 
-      test('test for refs/tags/ being striped from ref', done => {
-        flush(() => {
-          assert.equal(element._stripRefs(element._items[1].ref,
-              element.detailType), 'test2');
-          done();
-        });
+      test('test for refs/tags/ being striped from ref', async () => {
+        assert.equal(element._stripRefs(element._items[1].ref,
+            element.detailType), 'test2');
       });
 
       test('_shownItems', () => {
@@ -411,7 +390,7 @@
     });
 
     suite('list with less then 25 tags', () => {
-      setup(done => {
+      setup(async () => {
         tags = _.times(25, tagGenerator);
         stubRestApi('getRepoTags').returns(Promise.resolve(tags));
 
@@ -420,7 +399,8 @@
           detail: 'tags',
         };
 
-        element._paramsChanged(params).then(() => { flush(done); });
+        await element._paramsChanged(params);
+        await flush();
       });
 
       test('_shownItems', () => {
@@ -482,16 +462,18 @@
     });
 
     suite('404', () => {
-      test('fires page-error', done => {
+      test('fires page-error', async () => {
         const response = {status: 404};
         stubRestApi('getRepoTags').callsFake(
             (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
               errFn(response);
+              return Promise.resolve();
             });
 
+        const promise = mockPromise();
         addListenerForTest(document, 'page-error', e => {
           assert.deepEqual(e.detail.response, response);
-          done();
+          promise.resolve();
         });
 
         const params = {
@@ -501,6 +483,7 @@
           offset: 25,
         };
         element._paramsChanged(params);
+        await promise;
       });
     });
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index 982fd8d..beef556 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -22,16 +22,16 @@
 import '../gr-create-repo-dialog/gr-create-repo-dialog';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-repo-list_html';
-import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {customElement, property, observe, computed} from '@polymer/decorators';
 import {AppElementAdminParams} from '../../gr-app-types';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {RepoName, ProjectInfoWithName} from '../../../types/common';
 import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {ProjectState} from '../../../constants/constants';
+import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
 import {fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -47,7 +47,7 @@
 }
 
 @customElement('gr-repo-list')
-export class GrRepoList extends ListViewMixin(PolymerElement) {
+export class GrRepoList extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -81,7 +81,7 @@
 
   @computed('_repos')
   get _shownRepos() {
-    return this.computeShownItems(this._repos);
+    return this._repos.slice(0, SHOWN_ITEMS_COUNT);
   }
 
   private readonly restApiService = appContext.restApiService;
@@ -96,8 +96,8 @@
   @observe('params')
   _paramsChanged(params: AppElementAdminParams) {
     this._loading = true;
-    this._filter = this.getFilterValue(params);
-    this._offset = this.getOffsetValue(params);
+    this._filter = params?.filter ?? '';
+    this._offset = Number(params?.offset ?? 0);
 
     return this._getRepos(this._filter, this._reposPerPage, this._offset);
   }
@@ -112,7 +112,7 @@
   }
 
   _computeRepoUrl(name: string) {
-    return this.getUrl(this._path + '/', name);
+    return getBaseUrl() + this._path + '/' + encodeURL(name, true);
   }
 
   _computeChangesLink(name: string) {
@@ -182,4 +182,8 @@
     const webLinks = repo.web_links;
     return webLinks.length ? webLinks : null;
   }
+
+  computeLoadingClass(loading: boolean) {
+    return loading ? 'loading' : '';
+  }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
index 4864af5..8fef4d0 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
@@ -53,17 +53,16 @@
   });
 
   suite('list with repos', () => {
-    setup(done => {
+    setup(async () => {
       repos = _.times(26, repoGenerator);
       stubRestApi('getRepos').returns(Promise.resolve(repos));
-      element._paramsChanged(value).then(() => { flush(done); });
+      await element._paramsChanged(value);
+      await flush();
     });
 
-    test('test for test repo in the list', done => {
-      flush(() => {
-        assert.equal(element._repos[1].id, 'test2');
-        done();
-      });
+    test('test for test repo in the list', async () => {
+      await flush();
+      assert.equal(element._repos[1].id, 'test2');
     });
 
     test('_shownRepos', () => {
@@ -84,10 +83,11 @@
   });
 
   suite('list with less then 25 repos', () => {
-    setup(done => {
+    setup(async () => {
       repos = _.times(25, repoGenerator);
       stubRestApi('getRepos').returns(Promise.resolve(repos));
-      element._paramsChanged(value).then(() => { flush(done); });
+      await element._paramsChanged(value);
+      await flush();
     });
 
     test('_shownRepos', () => {
@@ -113,17 +113,15 @@
       assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25));
     });
 
-    test('latest repos requested are always set', done => {
+    test('latest repos requested are always set', async () => {
       const repoStub = stubRestApi('getRepos');
       repoStub.withArgs('test').returns(Promise.resolve(repos));
       repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
       element._filter = 'test';
 
       // Repos are not set because the element._filter differs.
-      element._getRepos('filter', 25, 0).then(() => {
-        assert.deepEqual(element._repos, []);
-        done();
-      });
+      await element._getRepos('filter', 25, 0);
+      assert.deepEqual(element._repos, []);
     });
 
     test('filter is case insensitive', async () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config-types.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config-types.ts
index 6a96f55..d5515f9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config-types.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config-types.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 /**
- * @fileOverview This file contains interfaces shared between
+ * @fileoverview This file contains interfaces shared between
  * gr-repo-plugin-config.ts and nested editors
  * (e.g. gr-plugin-config-array-editor.ts)
  *
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 42060ee..6e68ae7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -23,8 +23,8 @@
 import '../../shared/gr-select/gr-select';
 import '../../shared/gr-tooltip-content/gr-tooltip-content';
 import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {ConfigParameterInfoType} from '../../../constants/constants';
 import {
   ConfigParameterInfo,
@@ -57,7 +57,7 @@
 }
 
 @customElement('gr-repo-plugin-config')
-export class GrRepoPluginConfig extends GrLitElement {
+export class GrRepoPluginConfig extends LitElement {
   /**
    * Fired when the plugin config changes.
    *
@@ -119,15 +119,7 @@
   private renderOption(option: PluginOption) {
     return html`
       <section class="section ${option.info.type}">
-        <span class="title">
-          <gr-tooltip-content
-            has-tooltip="${option.info.description}"
-            show-icon="${option.info.description}"
-            title="${option.info.description}"
-          >
-            <span>${option.info.display_name}</span>
-          </gr-tooltip-content>
-        </span>
+        <span class="title"> ${this.renderOptionTitle(option)} </span>
         <span class="value">
           ${this.renderOptionDetail(option)} ${this.renderInherited(option)}
         </span>
@@ -135,6 +127,18 @@
     `;
   }
 
+  private renderOptionTitle(option: PluginOption) {
+    const titleName = html`<span>${option.info.display_name}</span>`;
+    if (!option.info.description) return titleName;
+    return html` <gr-tooltip-content
+      has-tooltip
+      show-icon
+      title="${option.info.description}"
+    >
+      ${titleName}
+    </gr-tooltip-content>`;
+  }
+
   private renderOptionDetail(option: PluginOption) {
     if (option.info.type === ConfigParameterInfoType.ARRAY) {
       return html`
@@ -149,7 +153,7 @@
           ?checked=${this._computeChecked(option.info.value)}
           @change=${this._handleBooleanChange}
           data-option-key=${option._key}
-          ?disabled=${option.info.editable}
+          ?disabled=${!option.info.editable}
           @click=${this._onTapPluginBoolean}
         ></paper-toggle-button>
       `;
@@ -158,7 +162,7 @@
         <gr-select value=${option.info.value} @change=${this._handleListChange}>
           <select
             data-option-key=${option._key}
-            ?disabled=${option.info.editable}
+            ?disabled=${!option.info.editable}
           >
             ${(option.info.permitted_values || []).map(
               value => html`<option value="${value}">${value}</option>`
@@ -176,14 +180,14 @@
           value=${option.info.value}
           @input=${this._handleStringChange}
           data-option-key="${option._key}"
-          ?disabled=${option.info.editable}
+          ?disabled=${!option.info.editable}
         >
           <input
             is="iron-input"
-            value="${option.info.value}"
+            .value="${option.info.value}"
             @input=${this._handleStringChange}
             data-option-key="${option._key}"
-            ?disabled=${option.info.editable}
+            ?disabled=${!option.info.editable}
           />
         </iron-input>
       `;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
index 17e8256..8c2e6b3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
@@ -67,7 +67,7 @@
     test('ARRAY type option', async () => {
       element.pluginData = {
         name: 'testName',
-        config: {plugin: {value: 'test', type: 'ARRAY'}},
+        config: {plugin: {value: 'test', type: 'ARRAY', editable: true}},
       };
       await flush();
 
@@ -82,7 +82,7 @@
     test('BOOLEAN type option', async () => {
       element.pluginData = {
         name: 'testName',
-        config: {plugin: {value: 'true', type: 'BOOLEAN'}},
+        config: {plugin: {value: 'true', type: 'BOOLEAN', editable: true}},
       };
       await flush();
 
@@ -101,7 +101,7 @@
     test('INT/LONG/STRING type option', async () => {
       element.pluginData = {
         name: 'testName',
-        config: {plugin: {value: 'test', type: 'STRING'}},
+        config: {plugin: {value: 'test', type: 'STRING', editable: true}},
       };
       await flush();
 
@@ -122,7 +122,9 @@
       const permitted_values = ['test', 'newTest'];
       element.pluginData = {
         name: 'testName',
-        config: {plugin: {value: 'test', type: 'LIST', permitted_values}},
+        config: {plugin:
+          {value: 'test', type: 'LIST', editable: true, permitted_values},
+        },
       };
       await flush();
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index d132267..cd5b095 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -20,6 +20,7 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../shared/gr-download-commands/gr-download-commands';
 import '../../shared/gr-select/gr-select';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/gr-subpage-styles';
 import '../../../styles/shared-styles';
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
index ef0e6b4..71abec0 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
index 9c3646a..f3df132 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
@@ -180,7 +180,7 @@
   });
 
   suite('already existing generic rule', () => {
-    setup(done => {
+    setup(async () => {
       element.group = 'Group Name';
       element.permission = 'submit';
       element.rule = {
@@ -195,11 +195,8 @@
       // Typically called on ready since elements will have properties defined
       // by the parent element.
       element._setupValues(element.rule);
-      flush();
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -295,7 +292,7 @@
   });
 
   suite('new edit rule', () => {
-    setup(done => {
+    setup(async () => {
       element.group = 'Group Name';
       element.permission = 'editTopicName';
       element.rule = {
@@ -303,12 +300,10 @@
       };
       element.section = 'refs/*';
       element._setupValues(element.rule);
-      flush();
+      await flush();
       element.rule.value.added = true;
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -348,7 +343,7 @@
   });
 
   suite('already existing rule with labels', () => {
-    setup(done => {
+    setup(async () => {
       element.label = {values: [
         {value: -2, text: 'This shall not be merged'},
         {value: -1, text: 'I would prefer this is not merged as is'},
@@ -369,11 +364,8 @@
       };
       element.section = 'refs/*';
       element._setupValues(element.rule);
-      flush();
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -406,7 +398,7 @@
   });
 
   suite('new rule with labels', () => {
-    setup(done => {
+    setup(async () => {
       sinon.spy(element, '_setDefaultRuleValues');
       element.label = {values: [
         {value: -2, text: 'This shall not be merged'},
@@ -422,12 +414,10 @@
       };
       element.section = 'refs/*';
       element._setupValues(element.rule);
-      flush();
+      await flush();
       element.rule.value.added = true;
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -468,7 +458,7 @@
   });
 
   suite('already existing push rule', () => {
-    setup(done => {
+    setup(async () => {
       element.group = 'Group Name';
       element.permission = 'push';
       element.rule = {
@@ -480,11 +470,8 @@
       };
       element.section = 'refs/*';
       element._setupValues(element.rule);
-      flush();
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -513,7 +500,7 @@
   });
 
   suite('new push rule', () => {
-    setup(done => {
+    setup(async () => {
       element.group = 'Group Name';
       element.permission = 'push';
       element.rule = {
@@ -521,12 +508,10 @@
       };
       element.section = 'refs/*';
       element._setupValues(element.rule);
-      flush();
+      await flush();
       element.rule.value.added = true;
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -557,7 +542,7 @@
   });
 
   suite('already existing edit rule', () => {
-    setup(done => {
+    setup(async () => {
       element.group = 'Group Name';
       element.permission = 'editTopicName';
       element.rule = {
@@ -569,11 +554,8 @@
       };
       element.section = 'refs/*';
       element._setupValues(element.rule);
-      flush();
-      flush(() => {
-        element.connectedCallback();
-        done();
-      });
+      await flush();
+      element.connectedCallback();
     });
 
     test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -590,10 +572,10 @@
       assert.isNotOk(element.root.querySelector('#labelMax'));
     });
 
-    test('modify value', () => {
+    test('modify value', async () => {
       assert.isNotOk(element.rule.value.modified);
       element.$.action.bindValue = false;
-      flush();
+      await flush();
       assert.isTrue(element.rule.value.modified);
 
       // The original value should now differ from the rule values.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 50bbda9..c476d2d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -28,7 +28,6 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-list-item_html';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {getDisplayName} from '../../../utils/display-name-util';
 import {getPluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
@@ -77,7 +76,7 @@
 const PRIMARY_REVIEWERS_COUNT = 2;
 
 @customElement('gr-change-list-item')
-export class GrChangeListItem extends ChangeTableMixin(PolymerElement) {
+export class GrChangeListItem extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -285,7 +284,7 @@
    * @param truncate whether or not the project name should be
    * truncated. If this value is truthy, the name will be truncated.
    */
-  _computeRepoDisplay(change: ChangeInfo | undefined, truncate: boolean) {
+  _computeRepoDisplay(change?: ChangeInfo) {
     if (!change?.project) {
       return '';
     }
@@ -293,7 +292,19 @@
     if (change.internalHost) {
       str += change.internalHost + '/';
     }
-    str += truncate ? truncatePath(change.project, 2) : change.project;
+    str += change.project;
+    return str;
+  }
+
+  _computeTruncatedRepoDisplay(change?: ChangeInfo) {
+    if (!change?.project) {
+      return '';
+    }
+    let str = '';
+    if (change.internalHost) {
+      str += change.internalHost + '/';
+    }
+    str += truncatePath(change.project, 2);
     return str;
   }
 
@@ -351,10 +362,7 @@
     return this._computeAdditionalReviewers(change).length;
   }
 
-  _computeAdditionalReviewersTitle(
-    change: ChangeInfo | undefined,
-    config: ServerInfo
-  ) {
+  _computeAdditionalReviewersTitle(change?: ChangeInfo, config?: ServerInfo) {
     if (!change || !config) return '';
     return this._computeAdditionalReviewers(change)
       .map(user => getDisplayName(config, user, true))
@@ -390,13 +398,20 @@
   }
 
   _computeWaiting(
-    account?: AccountInfo,
-    change?: ChangeInfo
+    account?: AccountInfo | null,
+    change?: ChangeInfo | null
   ): Timestamp | undefined {
     if (!account?._account_id || !change?.attention_set) return undefined;
     return change?.attention_set[account._account_id]?.last_update;
   }
 
+  _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+    if (!columnsToDisplay || !columnToCheck) {
+      return false;
+    }
+    return !columnsToDisplay.includes(columnToCheck);
+  }
+
   toggleReviewed() {
     if (!this.change) return;
     const newVal = !this.change?.reviewed;
@@ -414,6 +429,11 @@
     );
   }
 
+  _formatDate(date: Timestamp | undefined): string | undefined {
+    if (!date) return undefined;
+    return date.toString();
+  }
+
   _handleChangeClick() {
     // Don't prevent the default and neither stop bubbling. We just want to
     // report the click, but then let the browser handle the click on the link.
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index 8ee6a3f..e1013e1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -127,7 +127,7 @@
   </td>
   <td
     class="cell subject"
-    hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Subject', visibleChangeTableColumns)]]"
   >
     <a
       title$="[[change.subject]]"
@@ -143,7 +143,7 @@
   </td>
   <td
     class="cell status"
-    hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Status', visibleChangeTableColumns)]]"
   >
     <template is="dom-repeat" items="[[statuses]]" as="status">
       <div class="comma">,</div>
@@ -155,7 +155,7 @@
   </td>
   <td
     class="cell owner"
-    hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Owner', visibleChangeTableColumns)]]"
   >
     <gr-account-link
       highlightAttention
@@ -165,7 +165,7 @@
   </td>
   <td
     class="cell assignee"
-    hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Assignee', visibleChangeTableColumns)]]"
   >
     <template is="dom-if" if="[[change.assignee]]">
       <gr-account-link
@@ -179,7 +179,7 @@
   </td>
   <td
     class="cell reviewers"
-    hidden$="[[isColumnHidden('Reviewers', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Reviewers', visibleChangeTableColumns)]]"
   >
     <div>
       <template
@@ -204,14 +204,14 @@
       </template>
       <template is="dom-if" if="[[_computeAdditionalReviewersCount(change)]]">
         <span title="[[_computeAdditionalReviewersTitle(change, config)]]">
-          +[[_computeAdditionalReviewersCount(change, config)]]
+          +[[_computeAdditionalReviewersCount(change)]]
         </span>
       </template>
     </div>
   </td>
   <td
     class="cell comments"
-    hidden$="[[isColumnHidden('Comments', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Comments', visibleChangeTableColumns)]]"
   >
     <iron-icon
       hidden$="[[!change.unresolved_comment_count]]"
@@ -221,22 +221,22 @@
   </td>
   <td
     class="cell repo"
-    hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Repo', visibleChangeTableColumns)]]"
   >
     <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
-      [[_computeRepoDisplay(change)]]
+      <div class="content">[[_computeRepoDisplay(change)]]</div>
     </a>
     <a
       class="truncatedRepo"
       href$="[[_computeRepoUrl(change)]]"
       title$="[[_computeRepoDisplay(change)]]"
     >
-      [[_computeRepoDisplay(change, 'true')]]
+      [[_computeTruncatedRepoDisplay(change)]]
     </a>
   </td>
   <td
     class="cell branch"
-    hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Branch', visibleChangeTableColumns)]]"
   >
     <a href$="[[_computeRepoBranchURL(change)]]"> [[change.branch]] </a>
     <template is="dom-if" if="[[change.topic]]">
@@ -250,38 +250,38 @@
   </td>
   <td
     class="cell updated"
-    hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Updated', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
-      has-tooltip=""
-      date-str="[[change.updated]]"
+      withTooltip
+      date-str="[[_formatDate(change.updated)]]"
     ></gr-date-formatter>
   </td>
   <td
     class="cell submitted"
-    hidden$="[[isColumnHidden('Submitted', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Submitted', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
-      has-tooltip=""
-      date-str="[[change.submitted]]"
+      withTooltip
+      date-str="[[_formatDate(change.submitted)]]"
     ></gr-date-formatter>
   </td>
   <td
     class="cell waiting"
-    hidden$="[[isColumnHidden('Waiting', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Waiting', visibleChangeTableColumns)]]"
   >
     <gr-date-formatter
-      has-tooltip=""
-      force-relative=""
-      relative-option-no-ago=""
+      withTooltip
+      forceRelative
+      relativeOptionNoAge
       date-str="[[_computeWaiting(account, change)]]"
     ></gr-date-formatter>
   </td>
   <td
     class="cell size"
-    hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]"
+    hidden$="[[_computeIsColumnHidden('Size', visibleChangeTableColumns)]]"
   >
-    <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]">
+    <gr-tooltip-content has-tooltip title="[[_computeSizeTooltip(change)]]">
       <template is="dom-if" if="[[_changeSize]]">
         <span>[[_changeSize]]</span>
       </template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index ac0b929..34cb6eb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -29,6 +29,7 @@
   TopicName,
 } from '../../../types/common';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {columnNames} from '../gr-change-list/gr-change-list';
 import './gr-change-list-item';
 import {GrChangeListItem, LabelCategory} from './gr-change-list-item';
 
@@ -372,7 +373,7 @@
 
     await flush();
 
-    for (const column of element.columnNames) {
+    for (const column of columnNames) {
       const elementClass = '.' + column.toLowerCase();
       assert.isFalse(
         queryAndAssert(element, elementClass).hasAttribute('hidden')
@@ -395,7 +396,7 @@
 
     await flush();
 
-    for (const column of element.columnNames) {
+    for (const column of columnNames) {
       const elementClass = '.' + column.toLowerCase();
       if (column === 'Repo') {
         assert.isTrue(
@@ -565,13 +566,13 @@
   });
 
   test('_computeRepoDisplay', () => {
+    assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
     assert.equal(
-      element._computeRepoDisplay(change, false),
-      'host/a/test/repo'
+      element._computeTruncatedRepoDisplay(change),
+      'host/…/test/repo'
     );
-    assert.equal(element._computeRepoDisplay(change, true), 'host/…/test/repo');
     delete change.internalHost;
-    assert.equal(element._computeRepoDisplay(change, false), 'a/test/repo');
-    assert.equal(element._computeRepoDisplay(change, true), '…/test/repo');
+    assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
+    assert.equal(element._computeTruncatedRepoDisplay(change), '…/test/repo');
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 9970dd5..2360312 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -39,6 +39,7 @@
 import {fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {GerritView} from '../../../services/router/router-model';
+import {RELOAD_DASHBOARD_INTERVAL_MS} from '../../../constants/constants';
 
 const LOOKUP_QUERY_PATTERNS: RegExp[] = [
   /^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
@@ -112,10 +113,26 @@
 
   private reporting = appContext.reportingService;
 
+  private lastVisibleTimestampMs = 0;
+
   constructor() {
     super();
     this.addEventListener('next-page', () => this._handleNextPage());
     this.addEventListener('previous-page', () => this._handlePreviousPage());
+    this.addEventListener('reload', () => this.reload());
+    // We are not currently verifying if the view is actually visible. We rely
+    // on gr-app-element to restamp the component if view changes
+    document.addEventListener('visibilitychange', () => {
+      if (document.visibilityState === 'visible') {
+        if (
+          Date.now() - this.lastVisibleTimestampMs >
+          RELOAD_DASHBOARD_INTERVAL_MS
+        )
+          this.reload();
+      } else {
+        this.lastVisibleTimestampMs = Date.now();
+      }
+    });
   }
 
   override connectedCallback() {
@@ -123,6 +140,15 @@
     this._loadPreferences();
   }
 
+  reload() {
+    if (this._loading) return;
+    this._loading = true;
+    this._getChanges().then(changes => {
+      this._changes = changes || [];
+      this._loading = false;
+    });
+  }
+
   _paramsChanged(value: AppElementParams) {
     if (value.view !== GerritView.SEARCH) return;
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts
index 54d3911..355ef45 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts
@@ -66,7 +66,7 @@
     ></gr-repo-header>
     <gr-user-header
       user-id="[[_userId]]"
-      show-dashboard-link=""
+      showDashboardLink=""
       logged-in="[[_loggedIn]]"
       class$="[[_computeHeaderClass(_userId)]]"
     ></gr-user-header>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
index 445254a..86b2fd1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
@@ -20,7 +20,7 @@
 import {page} from '../../../utils/page-wrapper-utils.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import 'lodash/lodash.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-change-list-view');
 
@@ -38,10 +38,8 @@
     element = basicFixture.instantiate();
   });
 
-  teardown(done => {
-    flush(() => {
-      done();
-    });
+  teardown(async () => {
+    await flush();
   });
 
   test('_computePage', () => {
@@ -126,128 +124,123 @@
     assert.isTrue(showStub.called);
   });
 
-  test('_userId query', done => {
+  test('_userId query', async () => {
     assert.isNull(element._userId);
     element._query = 'owner: foo@bar';
     element._changes = [{owner: {email: 'foo@bar'}}];
-    flush(() => {
-      assert.equal(element._userId, 'foo@bar');
+    await flush();
+    assert.equal(element._userId, 'foo@bar');
 
-      element._query = 'foo bar baz';
-      element._changes = [{owner: {email: 'foo@bar'}}];
-      assert.isNull(element._userId);
-
-      done();
-    });
+    element._query = 'foo bar baz';
+    element._changes = [{owner: {email: 'foo@bar'}}];
+    assert.isNull(element._userId);
   });
 
-  test('_userId query without email', done => {
+  test('_userId query without email', async () => {
     assert.isNull(element._userId);
     element._query = 'owner: foo@bar';
     element._changes = [{owner: {}}];
-    flush(() => {
-      assert.isNull(element._userId);
-      done();
-    });
+    await flush();
+    assert.isNull(element._userId);
   });
 
-  test('_repo query', done => {
+  test('_repo query', async () => {
     assert.isNull(element._repo);
     element._query = 'project: test-repo';
     element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
-    flush(() => {
-      assert.equal(element._repo, 'test-repo');
-      element._query = 'foo bar baz';
-      element._changes = [{owner: {email: 'foo@bar'}}];
-      assert.isNull(element._repo);
-      done();
-    });
+    await flush();
+    assert.equal(element._repo, 'test-repo');
+    element._query = 'foo bar baz';
+    element._changes = [{owner: {email: 'foo@bar'}}];
+    assert.isNull(element._repo);
   });
 
-  test('_repo query with open status', done => {
+  test('_repo query with open status', async () => {
     assert.isNull(element._repo);
     element._query = 'project:test-repo status:open';
     element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
-    flush(() => {
-      assert.equal(element._repo, 'test-repo');
-      element._query = 'foo bar baz';
-      element._changes = [{owner: {email: 'foo@bar'}}];
-      assert.isNull(element._repo);
-      done();
-    });
+    await flush();
+    assert.equal(element._repo, 'test-repo');
+    element._query = 'foo bar baz';
+    element._changes = [{owner: {email: 'foo@bar'}}];
+    assert.isNull(element._repo);
   });
 
   suite('query based navigation', () => {
     setup(() => {
     });
 
-    teardown(done => {
-      flush(() => {
-        sinon.restore();
-        done();
-      });
+    teardown(async () => {
+      await flush();
+      sinon.restore();
     });
 
-    test('Searching for a change ID redirects to change', done => {
+    test('Searching for a change ID redirects to change', async () => {
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
+      const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake(
           (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
             assert.equal(url, change);
             assert.isTrue(opt_redirect);
-            done();
+            promise.resolve();
           });
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
+      await promise;
     });
 
-    test('Searching for a change num redirects to change', done => {
+    test('Searching for a change num redirects to change', async () => {
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
+      const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake(
           (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
             assert.equal(url, change);
             assert.isTrue(opt_redirect);
-            done();
+            promise.resolve();
           });
 
       element.params = {view: GerritNav.View.SEARCH, query: '1'};
+      await promise;
     });
 
-    test('Commit hash redirects to change', done => {
+    test('Commit hash redirects to change', async () => {
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
+      const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake(
           (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
             assert.equal(url, change);
             assert.isTrue(opt_redirect);
-            done();
+            promise.resolve();
           });
 
       element.params = {view: GerritNav.View.SEARCH, query: COMMIT_HASH};
+      await promise;
     });
 
-    test('Searching for an invalid change ID searches', () => {
+    test('Searching for an invalid change ID searches', async () => {
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([]));
       const stub = sinon.stub(GerritNav, 'navigateToChange');
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
-      flush();
+      await flush();
 
       assert.isFalse(stub.called);
     });
 
-    test('Change ID with multiple search results searches', () => {
+    test('Change ID with multiple search results searches', async () => {
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([{}, {}]));
       const stub = sinon.stub(GerritNav, 'navigateToChange');
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
-      flush();
+      await flush();
 
       assert.isFalse(stub.called);
     });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 5c0bab8..a2a46e0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -24,7 +24,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-list_html';
 import {appContext} from '../../../services/app-context';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {
   KeyboardShortcutMixin,
   Shortcut,
@@ -57,18 +56,34 @@
 const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
 const MAX_SHORTCUT_CHARS = 5;
 
+export const columnNames = [
+  'Subject',
+  'Status',
+  'Owner',
+  'Assignee',
+  'Reviewers',
+  'Comments',
+  'Repo',
+  'Branch',
+  'Updated',
+  'Size',
+];
+
 export interface ChangeListSection {
   name?: string;
   query?: string;
   results: ChangeInfo[];
 }
+
 export interface GrChangeList {
   $: {};
 }
+
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-change-list')
-export class GrChangeList extends ChangeTableMixin(
-  KeyboardShortcutMixin(PolymerElement)
-) {
+export class GrChangeList extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -216,32 +231,44 @@
       return;
     }
 
-    this.changeTableColumns = this.columnNames;
+    this.changeTableColumns = columnNames;
     this.showNumber = false;
-    this.visibleChangeTableColumns = this.getEnabledColumns(
-      this.columnNames,
-      config,
-      this.flagsService.enabledExperiments
+    this.visibleChangeTableColumns = this.changeTableColumns.filter(col =>
+      this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
     );
-
     if (account && preferences) {
       this.showNumber = !!(
         preferences && preferences.legacycid_in_change_table
       );
       if (preferences.change_table && preferences.change_table.length > 0) {
-        const prefColumns = this.renameProjectToRepoColumn(
-          preferences.change_table
+        const prefColumns = preferences.change_table.map(column =>
+          column === 'Project' ? 'Repo' : column
         );
-        this.visibleChangeTableColumns = this.getEnabledColumns(
-          prefColumns,
-          config,
-          this.flagsService.enabledExperiments
+        this.visibleChangeTableColumns = prefColumns.filter(col =>
+          this._isColumnEnabled(
+            col,
+            config,
+            this.flagsService.enabledExperiments
+          )
         );
       }
     }
   }
 
   /**
+   * Is the column disabled by a server config or experiment? For example the
+   * assignee feature might be disabled and thus the corresponding column is
+   * also disabled.
+   *
+   */
+  _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+    if (!config || !config.change) return true;
+    if (column === 'Assignee') return !!config.change.enable_assignee;
+    if (column === 'Comments') return experiments.includes('comments-column');
+    return true;
+  }
+
+  /**
    * This methods allows us to customize the columns per section.
    *
    * @param visibleColumns are the columns according to configs and user prefs
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
index 37d969e..c31da77 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
@@ -63,7 +63,7 @@
               class="cell"
               colspan$="[[_computeColspan(changeSection, visibleChangeTableColumns, labelNames)]]"
             >
-              <h2>
+              <h2 class="heading-3">
                 <a
                   href$="[[_sectionHref(changeSection.query)]]"
                   class="section-title"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 13490d7..7b226e7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -19,7 +19,7 @@
 import './gr-change-list.js';
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {mockPromise, TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
 import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {YOUR_TURN} from '../../core/gr-navigation/gr-navigation.js';
 
@@ -149,7 +149,7 @@
         {}, changeTableColumns, labelNames));
   });
 
-  test('keyboard shortcuts', done => {
+  test('keyboard shortcuts', async () => {
     sinon.stub(element, '_computeLabelNames');
     element.sections = [
       {results: new Array(1)},
@@ -161,7 +161,8 @@
       {_number: 1},
       {_number: 2},
     ];
-    flush();
+    await flush();
+    const promise = mockPromise();
     afterNextRender(element, () => {
       const elementItems = element.root.querySelectorAll(
           'gr-change-list-item');
@@ -192,8 +193,9 @@
       MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
       assert.equal(element.selectedIndex, 0);
 
-      done();
+      promise.resolve();
     });
+    await promise;
   });
 
   test('no changes', () => {
@@ -267,7 +269,7 @@
     });
 
     test('all columns visible', () => {
-      for (const column of element.columnNames) {
+      for (const column of element.changeTableColumns) {
         const elementClass = '.' + element._lowerCase(column);
         assert.isFalse(element.shadowRoot
             .querySelector(elementClass).hidden);
@@ -436,7 +438,7 @@
       element = basicFixture.instantiate();
     });
 
-    test('keyboard shortcuts', done => {
+    test('keyboard shortcuts', async () => {
       element.selectedIndex = 0;
       element.sections = [
         {
@@ -461,7 +463,8 @@
           ],
         },
       ];
-      flush();
+      await flush();
+      const promise = mockPromise();
       afterNextRender(element, () => {
         const elementItems = element.root.querySelectorAll(
             'gr-change-list-item');
@@ -499,8 +502,9 @@
         MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r'
         assert.equal(change.reviewed, false,
             'Should mark change as unreviewed');
-        done();
+        promise.resolve();
       });
+      await promise;
     });
 
     test('_computeItemHighlight gives false for null account', () => {
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 055996ee..fd2a5d7 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
@@ -19,8 +19,9 @@
 import '../../shared/gr-icons/gr-icons';
 import {fireEvent} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement} from 'lit/decorators';
+import {fontStyles} from '../../../styles/gr-font-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -29,10 +30,11 @@
 }
 
 @customElement('gr-create-change-help')
-export class GrCreateChangeHelp extends GrLitElement {
+export class GrCreateChangeHelp extends LitElement {
   static override get styles() {
     return [
       sharedStyles,
+      fontStyles,
       css`
         :host {
           display: block;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts
index 1a359e5..e170a74 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.ts
@@ -18,7 +18,7 @@
 import '../../../test/common-test-setup-karma';
 import './gr-create-change-help';
 import {GrCreateChangeHelp} from './gr-create-change-help';
-import {queryAndAssert} from '../../../test/test-utils';
+import {mockPromise, queryAndAssert} from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 
@@ -32,8 +32,10 @@
     await flush();
   });
 
-  test('Create change tap', done => {
-    element.addEventListener('create-tap', () => done());
+  test('Create change tap', async () => {
+    const promise = mockPromise();
+    element.addEventListener('create-tap', () => promise.resolve());
     MockInteractions.tap(queryAndAssert<GrButton>(element, 'gr-button'));
+    await promise;
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
index 171c1c3..6c9fa68 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.ts
@@ -20,8 +20,8 @@
 import '../../shared/gr-shell-command/gr-shell-command';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, query} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, query} from 'lit/decorators';
 
 enum Commands {
   CREATE = 'git commit',
@@ -36,7 +36,7 @@
 }
 
 @customElement('gr-create-commands-dialog')
-export class GrCreateCommandsDialog extends GrLitElement {
+export class GrCreateCommandsDialog extends LitElement {
   @query('#commandsOverlay')
   commandsOverlay?: GrOverlay;
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index c341dea..9eca3bc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../gr-change-list/gr-change-list';
 import '../../shared/gr-button/gr-button';
@@ -54,9 +55,9 @@
 import {DashboardViewState} from '../../../types/types';
 import {firePageError, fireTitleChange} from '../../../utils/event-util';
 import {GerritView} from '../../../services/router/router-model';
+import {RELOAD_DASHBOARD_INTERVAL_MS} from '../../../constants/constants';
 
 const PROJECT_PLACEHOLDER_PATTERN = /\${project}/g;
-const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
 
 export interface GrDashboardView {
   $: {
@@ -122,12 +123,9 @@
 
   constructor() {
     super();
-  }
-
-  override connectedCallback() {
-    super.connectedCallback();
-    this._loadPreferences();
     this.addEventListener('reload', () => this._reload(this.params));
+    // We are not currently verifying if the view is actually visible. We rely
+    // on gr-app-element to restamp the component if view changes
     document.addEventListener('visibilitychange', () => {
       if (document.visibilityState === 'visible') {
         if (
@@ -141,6 +139,11 @@
     });
   }
 
+  override connectedCallback() {
+    super.connectedCallback();
+    this._loadPreferences();
+  }
+
   _loadPreferences() {
     return this.restApiService.getLoggedIn().then(loggedIn => {
       if (loggedIn) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index 484a952..4778890 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       display: block;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index 473d2b9..96a19e0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -22,7 +22,7 @@
 import {changeIsOpen} from '../../../utils/change-util.js';
 import {ChangeStatus} from '../../../constants/constants.js';
 import {createAccountWithId} from '../../../test/test-data-generators.js';
-import {addListenerForTest, stubRestApi, isHidden} from '../../../test/test-utils.js';
+import {addListenerForTest, stubRestApi, isHidden, mockPromise} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-dashboard-view');
 
@@ -395,21 +395,23 @@
         'hide');
   });
 
-  test('404 page', done => {
+  test('404 page', async () => {
     const response = {status: 404};
     stubRestApi('getDashboard').callsFake(
         async (project, dashboard, errFn) => {
           errFn(response);
         });
+    const promise = mockPromise();
     addListenerForTest(document, 'page-error', e => {
       assert.strictEqual(e.detail.response, response);
-      paramsChangedPromise.then(done);
+      promise.resolve();
     });
     element.params = {
       view: GerritNav.View.DASHBOARD,
       project: 'project',
       dashboard: 'dashboard',
     };
+    await Promise.all([paramsChangedPromise, promise]);
   });
 
   test('params change triggers dashboardDisplayed()', async () => {
@@ -427,7 +429,7 @@
     assert.isTrue(element.reporting.dashboardDisplayed.calledOnce);
   });
 
-  test('selectedChangeIndex is derived from the params', () => {
+  test('selectedChangeIndex is derived from the params', async () => {
     stubRestApi('getDashboard').returns(Promise.resolve({
       title: 'title',
       sections: [],
@@ -443,9 +445,8 @@
     };
     flush();
     sinon.stub(element.reporting, 'dashboardDisplayed');
-    paramsChangedPromise.then(() => {
-      assert.equal(element._selectedChangeIndex, 23);
-    });
+    await paramsChangedPromise;
+    assert.equal(element._selectedChangeIndex, 23);
   });
 });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
index 7b60715..9242a58 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {RepoName} from '../../../types/common';
 import {WebLinkInfo} from '../../../types/diff';
 import {appContext} from '../../../services/app-context';
 import {sharedStyles} from '../../../styles/shared-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
 import {dashboardHeaderStyles} from '../../../styles/dashboard-header-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, PropertyValues} from 'lit-element';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 @customElement('gr-repo-header')
-export class GrRepoHeader extends GrLitElement {
+export class GrRepoHeader extends LitElement {
   @property({type: String})
   repo?: RepoName;
 
@@ -42,6 +42,7 @@
     return [
       sharedStyles,
       dashboardHeaderStyles,
+      fontStyles,
       css`
         .browse {
           display: inline-block;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index 4c42005..e87b4f9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -25,11 +25,12 @@
 import {appContext} from '../../../services/app-context';
 import {dashboardHeaderStyles} from '../../../styles/dashboard-header-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, PropertyValues} from 'lit-element';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 @customElement('gr-user-header')
-export class GrUserHeader extends GrLitElement {
+export class GrUserHeader extends LitElement {
   @property({type: String})
   userId?: AccountId;
 
@@ -47,10 +48,11 @@
 
   private readonly restApiService = appContext.restApiService;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       dashboardHeaderStyles,
+      fontStyles,
       css`
         .status.hide,
         .name.hide,
@@ -61,7 +63,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <gr-avatar
         .account="${this._accountDetails}"
         .imageSize=${100}
@@ -114,7 +116,7 @@
       </div>`;
   }
 
-  updated(changedProperties: PropertyValues) {
+  override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('userId')) {
       this._accountChanged(this.userId);
     }
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 081df40..891482d 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
@@ -1190,6 +1190,13 @@
     });
   }
 
+  showSubmitDialog() {
+    if (!this._canSubmitChange()) {
+      return;
+    }
+    this._showActionDialog(this.$.confirmSubmitDialog);
+  }
+
   _handleActionTap(e: MouseEvent) {
     e.preventDefault();
     let el = (dom(e) as EventApi).localTarget as Element;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
index 614339a..b5f55ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
@@ -84,24 +84,27 @@
       hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"
     >
       <template is="dom-repeat" items="[[_topLevelPrimaryActions]]" as="action">
-        <gr-button
-          link=""
+        <gr-tooltip-content
           title$="[[action.title]]"
           has-tooltip="[[_computeHasTooltip(action.title)]]"
           position-below="true"
-          data-action-key$="[[action.__key]]"
-          class$="[[action.__key]]"
-          data-action-type$="[[action.__type]]"
-          data-label$="[[action.label]]"
-          disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-          on-click="_handleActionTap"
         >
-          <iron-icon
-            class$="[[_computeHasIcon(action)]]"
-            icon$="gr-icons:[[action.icon]]"
-          ></iron-icon>
-          [[action.label]]
-        </gr-button>
+          <gr-button
+            link=""
+            data-action-key$="[[action.__key]]"
+            class$="[[action.__key]]"
+            data-action-type$="[[action.__type]]"
+            data-label$="[[action.label]]"
+            disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
+            on-click="_handleActionTap"
+          >
+            <iron-icon
+              class$="[[_computeHasIcon(action)]]"
+              icon$="gr-icons:[[action.icon]]"
+            ></iron-icon>
+            [[action.label]]
+          </gr-button>
+        </gr-tooltip-content>
       </template>
     </section>
     <section
@@ -113,24 +116,27 @@
         items="[[_topLevelSecondaryActions]]"
         as="action"
       >
-        <gr-button
-          link=""
+        <gr-tooltip-content
           title$="[[action.title]]"
           has-tooltip="[[_computeHasTooltip(action.title)]]"
           position-below="true"
-          data-action-key$="[[action.__key]]"
-          class$="[[action.__key]]"
-          data-action-type$="[[action.__type]]"
-          data-label$="[[action.label]]"
-          disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
-          on-click="_handleActionTap"
         >
-          <iron-icon
-            class$="[[_computeHasIcon(action)]]"
-            icon$="gr-icons:[[action.icon]]"
-          ></iron-icon>
-          [[action.label]]
-        </gr-button>
+          <gr-button
+            link=""
+            data-action-key$="[[action.__key]]"
+            class$="[[action.__key]]"
+            data-action-type$="[[action.__type]]"
+            data-label$="[[action.label]]"
+            disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]"
+            on-click="_handleActionTap"
+          >
+            <iron-icon
+              class$="[[_computeHasIcon(action)]]"
+              icon$="gr-icons:[[action.icon]]"
+            ></iron-icon>
+            [[action.label]]
+          </gr-button>
+        </gr-tooltip-content>
       </template>
     </section>
     <gr-button hidden$="[[!_loading]]" disabled=""
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 906300f..26e2fb4 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -54,7 +54,7 @@
 } from '../../../types/common';
 import {ActionType} from '../../../api/change-actions';
 import {tap} from '@polymer/iron-test-helpers/mock-interactions';
-import {SinonFakeTimers} from 'sinon/pkg/sinon-esm';
+import {SinonFakeTimers} from 'sinon';
 import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
@@ -1094,7 +1094,7 @@
         );
         tap(abandonButton);
 
-        assert.isUndefined(element.$.confirmAbandonDialog.message);
+        assert.equal(element.$.confirmAbandonDialog.message, '');
       });
 
       test('works', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index e06f664..08ad2bd 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -17,7 +17,6 @@
 import '../../../styles/shared-styles';
 import '../../../styles/gr-change-metadata-shared-styles';
 import '../../../styles/gr-change-view-integration-shared-styles';
-import '../../../styles/gr-voting-styles';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../plugins/gr-external-style/gr-external-style';
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index c080345..a37daaa 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -134,9 +134,9 @@
         <span class="title">Submitted</span>
         <span class="value">
           <gr-date-formatter
-            has-tooltip=""
+            withTooltip
             date-str="[[change.submitted]]"
-            show-yesterday=""
+            showYesterday=""
           ></gr-date-formatter>
         </span>
       </section>
@@ -154,9 +154,9 @@
       </span>
       <span class="value">
         <gr-date-formatter
-          has-tooltip=""
+          withTooltip
           date-str="[[change.updated]]"
-          show-yesterday=""
+          showYesterday
         ></gr-date-formatter>
       </span>
     </section>
@@ -354,8 +354,8 @@
               ></gr-commit-info>
               <gr-tooltip-content
                 id="parentNotCurrentMessage"
-                has-tooltip=""
-                show-icon=""
+                has-tooltip
+                show-icon
                 title$="[[_notCurrentMessage]]"
               ></gr-tooltip-content>
             </li>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index cb4c9a4..422c91b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -57,7 +57,7 @@
   LabelValueToDescriptionMap,
   Hashtag,
 } from '../../../types/common';
-import {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {SinonStubbedMember} from 'sinon';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {tap} from '@polymer/iron-test-helpers/mock-interactions';
 import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label';
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
index 6a31c0a..414784c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
@@ -17,7 +17,6 @@
 import '../../../styles/shared-styles';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icons/gr-icons';
-import '../../shared/gr-label/gr-label';
 import '../../shared/gr-label-info/gr-label-info';
 import '../../shared/gr-limited-text/gr-limited-text';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 97d8a37..831a309 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -14,9 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {css, customElement, property} from 'lit-element';
-import {GrLitElement} from '../../lit/gr-lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {subscribe} from '../../lit/subscription-controller';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {appContext} from '../../../services/app-context';
 import {
@@ -24,11 +24,13 @@
   aPluginHasRegistered$,
   CheckResult,
   CheckRun,
-  errorMessageLatest$,
+  ErrorMessages,
+  errorMessagesLatest$,
   loginCallbackLatest$,
-  someProvidersAreLoadingLatest$,
+  someProvidersAreLoadingFirstTime$,
+  topLevelActionsLatest$,
 } from '../../../services/checks/checks-model';
-import {Category, RunStatus} from '../../../api/checks';
+import {Action, Category, Link, RunStatus} from '../../../api/checks';
 import {fireShowPrimaryTab} from '../../../utils/event-util';
 import '../../shared/gr-avatar/gr-avatar';
 import {
@@ -60,6 +62,8 @@
 import {ChecksTabState, CommentTabState} from '../../../types/events';
 import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {modifierPressed} from '../../../utils/dom-util';
+import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
+import {fontStyles} from '../../../styles/gr-font-styles';
 
 export enum SummaryChipStyles {
   INFO = 'info',
@@ -78,7 +82,7 @@
 }
 
 @customElement('gr-summary-chip')
-export class GrSummaryChip extends GrLitElement {
+export class GrSummaryChip extends LitElement {
   @property()
   icon = '';
 
@@ -93,6 +97,7 @@
   static override get styles() {
     return [
       sharedStyles,
+      fontStyles,
       css`
         .summaryChip {
           color: var(--chip-color);
@@ -104,6 +109,10 @@
           border-radius: 12px;
           border: 1px solid gray;
           vertical-align: top;
+          /* centered position of 20px chips in 24px line-height inline flow */
+          vertical-align: top;
+          position: relative;
+          top: 2px;
         }
         iron-icon {
           width: var(--line-height-small);
@@ -164,19 +173,25 @@
 }
 
 @customElement('gr-checks-chip')
-export class GrChecksChip extends GrLitElement {
+export class GrChecksChip extends LitElement {
   @property()
   statusOrCategory?: Category | RunStatus;
 
   @property()
   text = '';
 
+  @property()
+  links: Link[] = [];
+
   static override get styles() {
     return [
+      fontStyles,
       sharedStyles,
       css`
         :host {
           display: inline-block;
+          position: relative;
+          white-space: nowrap;
         }
         .checksChip {
           color: var(--chip-color);
@@ -187,7 +202,21 @@
             var(--spacing-s);
           border-radius: 12px;
           border: 1px solid gray;
+          /* centered position of 20px chips in 24px line-height inline flow */
           vertical-align: top;
+          position: relative;
+          top: 2px;
+        }
+        .checksChip.hoverFullLength {
+          position: absolute;
+          z-index: 1;
+          display: none;
+        }
+        .checksChip.hoverFullLength .text {
+          max-width: 400px;
+        }
+        :host(:hover) .checksChip.hoverFullLength {
+          display: inline-block;
         }
         .checksChip .text {
           display: inline-block;
@@ -292,20 +321,51 @@
       ariaLabel = `${this.text} ${label} ${type}${plural}`;
     }
     const chipClass = `checksChip font-small ${icon}`;
+    const chipClassFullLength = `${chipClass} hoverFullLength`;
     const grIcon = `gr-icons:${icon}`;
+    // 15 is roughly the number of chars for the chip exceeding its 120px width.
     return html`
-      <div
-        class="${chipClass}"
-        role="link"
-        tabindex="0"
-        aria-label="${ariaLabel}"
-      >
-        <iron-icon icon="${grIcon}"></iron-icon>
+      ${this.text.length > 15
+        ? html` ${this.renderChip(chipClassFullLength, ariaLabel, grIcon)}`
+        : ''}
+      ${this.renderChip(chipClass, ariaLabel, grIcon)}
+    `;
+  }
+
+  private renderChip(clazz: string, ariaLabel: string, icon: string) {
+    return html`
+      <div class="${clazz}" role="link" tabindex="0" aria-label="${ariaLabel}">
+        <iron-icon icon="${icon}"></iron-icon>
         <div class="text">${this.text}</div>
-        <slot></slot>
+        ${this.renderLinks()}
       </div>
     `;
   }
+
+  private renderLinks() {
+    return this.links.map(
+      link => html`
+        <a
+          href="${link.url}"
+          target="_blank"
+          @click="${this.onLinkClick}"
+          @keydown="${this.onLinkKeyDown}"
+          aria-label="Link to check details"
+          ><iron-icon class="launch" icon="gr-icons:launch"></iron-icon
+        ></a>
+      `
+    );
+  }
+
+  private onLinkKeyDown(e: KeyboardEvent) {
+    // Prevents onChipKeyDown() from reacting to <a> link keyboard events.
+    e.stopPropagation();
+  }
+
+  private onLinkClick(e: MouseEvent) {
+    // Prevents onChipClick() from reacting to <a> link clicks.
+    e.stopPropagation();
+  }
 }
 
 /** What is the maximum number of detailed checks chips? */
@@ -315,7 +375,7 @@
 DETAILS_QUOTA.set(RunStatus.RUNNING, 2);
 
 @customElement('gr-change-summary')
-export class GrChangeSummary extends GrLitElement {
+export class GrChangeSummary extends LitElement {
   @property({type: Object})
   changeComments?: ChangeComments;
 
@@ -335,20 +395,30 @@
   someProvidersAreLoading = false;
 
   @property()
-  errorMessage?: string;
+  errorMessages: ErrorMessages = {};
 
   @property()
   loginCallback?: () => void;
 
+  @property()
+  actions: Action[] = [];
+
   private showAllChips = new Map<RunStatus | Category, boolean>();
 
+  private checksService = appContext.checksService;
+
   constructor() {
     super();
-    this.subscribe('runs', allRunsLatestPatchsetLatestAttempt$);
-    this.subscribe('showChecksSummary', aPluginHasRegistered$);
-    this.subscribe('someProvidersAreLoading', someProvidersAreLoadingLatest$);
-    this.subscribe('errorMessage', errorMessageLatest$);
-    this.subscribe('loginCallback', loginCallbackLatest$);
+    subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
+    subscribe(this, aPluginHasRegistered$, x => (this.showChecksSummary = x));
+    subscribe(
+      this,
+      someProvidersAreLoadingFirstTime$,
+      x => (this.someProvidersAreLoading = x)
+    );
+    subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x));
+    subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x));
+    subscribe(this, topLevelActionsLatest$, x => (this.actions = x));
   }
 
   static override get styles() {
@@ -372,7 +442,8 @@
         .login {
           display: flex;
           color: var(--primary-text-color);
-          padding: var(--spacing-s);
+          padding: 0 var(--spacing-s);
+          margin: var(--spacing-xs) 0;
           width: 490px;
         }
         div.error {
@@ -383,9 +454,17 @@
           width: 16px;
           height: 16px;
           position: relative;
-          top: 2px;
+          top: 4px;
           margin-right: var(--spacing-s);
         }
+        div.error .right {
+          overflow: hidden;
+        }
+        div.error .right .message {
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
         .login {
           justify-content: space-between;
           background: var(--info-background);
@@ -398,8 +477,8 @@
         }
         td.key {
           padding-right: var(--spacing-l);
-          padding-bottom: var(--spacing-m);
-          vertical-align: top;
+          padding-bottom: var(--spacing-s);
+          line-height: calc(var(--line-height-normal) + var(--spacing-s));
         }
         td.value {
           padding-right: var(--spacing-l);
@@ -428,27 +507,90 @@
           /* Making up for the 2px reduced height above. */
           top: 1px;
         }
+        .actions {
+          margin-left: calc(0px - var(--spacing-m));
+          line-height: var(--line-height-normal);
+        }
+        .actions gr-checks-action,
+        .actions gr-dropdown {
+          vertical-align: top;
+          --padding: 0 var(--spacing-m);
+        }
+        .actions #moreMessage {
+          display: none;
+        }
       `,
     ];
   }
 
-  renderChecksError() {
-    if (!this.errorMessage) return;
+  private renderActions() {
+    const actions = this.actions ?? [];
+    const summaryActions = actions.filter(a => a.summary).slice(0, 2);
+    if (summaryActions.length === 0) return;
+    const topActions = summaryActions.slice(0, 2);
+    const overflowActions = summaryActions.slice(2).map(action => {
+      return {...action, id: action.name};
+    });
+    const disabledActionIds = overflowActions
+      .filter(action => action.disabled)
+      .map(action => action.id);
+
     return html`
-      <div class="error zeroState">
-        <div class="left">
-          <iron-icon icon="gr-icons:error"></iron-icon>
-        </div>
-        <div class="right">
-          <div>Error while fetching check results</div>
-          <div>${this.errorMessage}</div>
-        </div>
+      <div class="actions">
+        ${topActions.map(this.renderAction)}
+        ${this.renderOverflow(overflowActions, disabledActionIds)}
       </div>
     `;
   }
 
+  private renderAction(action?: Action) {
+    if (!action) return;
+    return html`<gr-checks-action .action="${action}"></gr-checks-action>`;
+  }
+
+  private handleAction(e: CustomEvent<Action>) {
+    this.checksService.triggerAction(e.detail);
+  }
+
+  private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
+    if (items.length === 0) return;
+    return html`
+      <gr-dropdown
+        id="moreActions"
+        link=""
+        vertical-offset="32"
+        horizontal-align="right"
+        @tap-item="${this.handleAction}"
+        .items="${items}"
+        .disabledIds="${disabledIds}"
+      >
+        <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
+        </iron-icon>
+        <span id="moreMessage">More</span>
+      </gr-dropdown>
+    `;
+  }
+
+  renderErrorMessages() {
+    return Object.entries(this.errorMessages).map(
+      ([plugin, message]) =>
+        html`
+          <div class="error zeroState">
+            <div class="left">
+              <iron-icon icon="gr-icons:error"></iron-icon>
+            </div>
+            <div class="right">
+              <div class="message" title="${message}">
+                Error while fetching results for ${plugin}: ${message}
+              </div>
+            </div>
+          </div>
+        `
+    );
+  }
+
   renderChecksLogin() {
-    if (this.errorMessage || !this.loginCallback) return;
+    if (!this.loginCallback) return;
     return html`
       <div class="login">
         <div class="left">
@@ -466,14 +608,14 @@
   }
 
   renderChecksZeroState() {
-    if (this.errorMessage || this.loginCallback) return;
+    if (Object.keys(this.errorMessages).length > 0) return;
+    if (this.loginCallback) return;
     if (this.runs.some(isRunningOrHasCompleted)) return;
     const msg = this.someProvidersAreLoading ? 'Loading results' : 'No results';
     return html`<span role="status" class="loading zeroState">${msg}</span>`;
   }
 
   renderChecksChipForCategory(category: Category) {
-    if (this.errorMessage || this.loginCallback) return;
     const runs = this.runs.filter(run => {
       if (hasResultsOf(run, category)) return true;
       return category === Category.SUCCESS && hasCompletedWithoutResults(run);
@@ -486,7 +628,6 @@
   }
 
   renderChecksChipRunning() {
-    if (this.errorMessage || this.loginCallback) return;
     const runs = this.runs.filter(isRunning);
     return this.renderChecksChipsExpanded(runs, RunStatus.RUNNING, () => []);
   }
@@ -563,21 +704,10 @@
     return html`<gr-checks-chip
       .statusOrCategory="${statusOrCategory}"
       .text="${text}"
+      .links="${links}"
       @click="${handler}"
       @keydown="${(e: KeyboardEvent) => handleSpaceOrEnter(e, handler)}"
-      >${links.map(
-        link => html`
-          <a
-            href="${link.url}"
-            target="_blank"
-            @click="${this.onLinkClick}"
-            @keydown="${this.onLinkKeyDown}"
-            aria-label="Link to check details"
-            ><iron-icon class="launch" icon="gr-icons:launch"></iron-icon
-          ></a>
-        `
-      )}
-    </gr-checks-chip>`;
+    ></gr-checks-chip>`;
   }
 
   private onChipClick(state: ChecksTabState) {
@@ -586,16 +716,6 @@
     });
   }
 
-  private onLinkKeyDown(e: KeyboardEvent) {
-    // Prevents onConChipKeyDown() from reacting to <a> link keyboard events.
-    e.stopPropagation();
-  }
-
-  private onLinkClick(e: MouseEvent) {
-    // Prevents onChipClick() from reacting to <a> link clicks.
-    e.stopPropagation();
-  }
-
   override render() {
     const commentThreads =
       this.commentThreads?.filter(t => !isRobotThread(t) || hasHumanReply(t)) ??
@@ -616,7 +736,6 @@
             <td class="key">Checks</td>
             <td class="value">
               <div class="checksSummary">
-                ${this.renderChecksError()}${this.renderChecksLogin()}
                 ${this.renderChecksZeroState()}${this.renderChecksChipForCategory(
                   Category.ERROR
                 )}${this.renderChecksChipForCategory(
@@ -632,6 +751,7 @@
                   class="loadingSpin"
                   ?hidden="${!this.someProvidersAreLoading}"
                 ></span>
+                ${this.renderErrorMessages()}${this.renderChecksLogin()}${this.renderActions()}
               </div>
             </td>
           </tr>
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 5ec35c4..3396e13 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,6 +15,7 @@
  * limitations under the License.
  */
 import '@polymer/paper-tabs/paper-tabs';
+import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../../diff/gr-comment-api/gr-comment-api';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
@@ -23,7 +24,6 @@
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-change-star/gr-change-star';
 import '../../shared/gr-change-status/gr-change-status';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-editable-content/gr-editable-content';
 import '../../shared/gr-linked-text/gr-linked-text';
 import '../../shared/gr-overlay/gr-overlay';
@@ -51,7 +51,7 @@
 } from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {GrEditConstants} from '../../edit/gr-edit-constants';
 import {pluralize} from '../../../utils/string-util';
-import {windowLocationReload} from '../../../utils/dom-util';
+import {windowLocationReload, querySelectorAll} from '../../../utils/dom-util';
 import {
   GeneratedWebLink,
   GerritNav,
@@ -84,6 +84,7 @@
   isCc,
   isOwner,
   isReviewer,
+  isInvolved,
 } from '../../../utils/change-util';
 import {EventType as PluginEventType} from '../../../api/plugin';
 import {customElement, observe, property} from '@polymer/decorators';
@@ -188,6 +189,11 @@
   changeComments$,
   drafts$,
 } from '../../../services/comments/comments-model';
+import {
+  hasAttention,
+  getAddedByReason,
+  getRemovedByReason,
+} from '../../../utils/attention-set-util';
 
 const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
 
@@ -200,7 +206,7 @@
 
 const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm;
 
-const MSG_PREFIX = '#message-';
+const PREFIX = '#message-';
 
 const ReloadToastMessage = {
   NEWER_REVISION: 'A newer patch set has been uploaded',
@@ -239,8 +245,11 @@
 
 export type ChangeViewPatchRange = Partial<PatchRange>;
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-change-view')
-export class GrChangeView extends KeyboardShortcutMixin(PolymerElement) {
+export class GrChangeView extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -575,6 +584,8 @@
       [Shortcut.DIFF_BASE_AGAINST_LEFT]: '_handleDiffBaseAgainstLeft',
       [Shortcut.DIFF_RIGHT_AGAINST_LATEST]: '_handleDiffRightAgainstLatest',
       [Shortcut.DIFF_BASE_AGAINST_LATEST]: '_handleDiffBaseAgainstLatest',
+      [Shortcut.OPEN_SUBMIT_DIALOG]: '_handleOpenSubmitDialog',
+      [Shortcut.TOGGLE_ATTENTION_SET]: '_handleToggleAttentionSet',
     };
   }
 
@@ -1168,6 +1179,9 @@
   _paramsChanged(value: AppElementChangeViewParams) {
     if (value.view !== GerritView.CHANGE) {
       this._initialLoadComplete = false;
+      querySelectorAll(this, 'gr-overlay').forEach(overlay =>
+        (overlay as GrOverlay).close()
+      );
       return;
     }
 
@@ -1182,10 +1196,12 @@
       this.restApiService.setInProjectLookup(value.changeNum, value.project);
     }
 
+    if (value.basePatchNum === undefined)
+      value.basePatchNum = ParentPatchSetNum;
+
     const patchChanged =
       this._patchRange &&
       value.patchNum !== undefined &&
-      value.basePatchNum !== undefined &&
       (this._patchRange.patchNum !== value.patchNum ||
         this._patchRange.basePatchNum !== value.basePatchNum);
 
@@ -1196,7 +1212,7 @@
 
     const patchRange: ChangeViewPatchRange = {
       patchNum: value.patchNum,
-      basePatchNum: value.basePatchNum || ParentPatchSetNum,
+      basePatchNum: value.basePatchNum,
     };
 
     this.$.fileList.collapseAllDiffs();
@@ -1311,7 +1327,7 @@
     assertIsDefined(this._change, '_change');
     if (!this._patchRange)
       throw new Error('missing required _patchRange property');
-    const hash = MSG_PREFIX + e.detail.id;
+    const hash = PREFIX + e.detail.id;
     const url = GerritNav.getUrlForChange(
       this._change,
       this._patchRange.patchNum,
@@ -1323,8 +1339,8 @@
   }
 
   _maybeScrollToMessage(hash: string) {
-    if (hash.startsWith(MSG_PREFIX) && this.messagesList) {
-      this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
+    if (hash.startsWith(PREFIX) && this.messagesList) {
+      this.messagesList.scrollToMessage(hash.substr(PREFIX.length));
     }
   }
 
@@ -1508,6 +1524,57 @@
     this.$.metadata.editTopic();
   }
 
+  _handleOpenSubmitDialog(e: CustomKeyboardEvent) {
+    if (this.shouldSuppressKeyboardShortcut(e) || !this._submitEnabled) {
+      return;
+    }
+
+    e.preventDefault();
+    this.$.actions.showSubmitDialog();
+  }
+
+  _handleToggleAttentionSet(e: CustomKeyboardEvent) {
+    if (this.shouldSuppressKeyboardShortcut(e)) {
+      return;
+    }
+    if (!this._change || !this._account?._account_id) return;
+    if (!this._loggedIn || !isInvolved(this._change, this._account)) return;
+    if (!this._change.attention_set) this._change.attention_set = {};
+    if (hasAttention(this._account, this._change)) {
+      const reason = getRemovedByReason(this._account, this._serverConfig);
+      if (this._change.attention_set)
+        delete this._change.attention_set[this._account._account_id];
+      fireAlert(this, 'Removing you from the attention set ...');
+      this.restApiService
+        .removeFromAttentionSet(
+          this._change._number,
+          this._account._account_id,
+          reason
+        )
+        .then(() => {
+          fireEvent(this, 'hide-alert');
+        });
+    } else {
+      const reason = getAddedByReason(this._account, this._serverConfig);
+      fireAlert(this, 'Adding you to the attention set ...');
+      this._change.attention_set[this._account._account_id!] = {
+        account: this._account,
+        reason,
+        reason_account: this._account,
+      };
+      this.restApiService
+        .addToAttentionSet(
+          this._change._number,
+          this._account._account_id,
+          reason
+        )
+        .then(() => {
+          fireEvent(this, 'hide-alert');
+        });
+    }
+    this._change = {...this._change};
+  }
+
   _handleDiffAgainstBase(e: CustomKeyboardEvent) {
     if (this.shouldSuppressKeyboardShortcut(e)) {
       return;
@@ -1811,8 +1878,9 @@
         this.restApiService.getChange(changeId)
       )
     ).then(changes => {
+      // if a change is deleted then getChanges returns null for that changeId
       changes = changes.filter(
-        change => change?.status !== ChangeStatus.ABANDONED
+        change => change && change.status !== ChangeStatus.ABANDONED
       );
       if (!changes.length) return;
       const submittedRevert = changes.find(
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 94d3117..d57aca8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     .container:not(.loading) {
       background-color: var(--background-color-tertiary);
@@ -299,7 +302,7 @@
     .show-robot-comments {
       margin: var(--spacing-m);
     }
-    .patchInfo gr-thread-list {
+    .patchInfo gr-thread-list::part(threads) {
       padding: var(--spacing-l);
     }
   </style>
@@ -476,7 +479,7 @@
         class="commentThreads"
       >
         <gr-tooltip-content
-          has-tooltip=""
+          has-tooltip
           title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
         >
           <span>Comments</span></gr-tooltip-content
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 5508ee0..6668e15 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
@@ -36,6 +36,7 @@
 
 import 'lodash/lodash';
 import {
+  mockPromise,
   stubRestApi,
   TestKeyboardShortcutBinder,
 } from '../../../test/test-utils';
@@ -58,6 +59,7 @@
   createAccountWithIdNameAndEmail,
   createChangeViewChange,
   createRelatedChangeAndCommitInfo,
+  createAccountDetailWithId,
 } from '../../../test/test-data-generators';
 import {ChangeViewPatchRange, GrChangeView} from './gr-change-view';
 import {
@@ -89,7 +91,7 @@
 } from '@polymer/iron-test-helpers/mock-interactions';
 import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
 import {AppElementChangeViewParams} from '../../gr-app-types';
-import {SinonFakeTimers, SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 import {CustomKeyboardEvent} from '../../../types/events';
 import {CommentThread, UIRobot} from '../../../utils/comment-util';
@@ -365,7 +367,9 @@
         },
       })
     );
-    stubRestApi('getAccount').returns(Promise.resolve(undefined));
+    stubRestApi('getAccount').returns(
+      Promise.resolve(createAccountDetailWithId(5))
+    );
     stubRestApi('getDiffComments').returns(Promise.resolve({}));
     stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
     stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -389,10 +393,8 @@
     );
   });
 
-  teardown(done => {
-    flush(() => {
-      done();
-    });
+  teardown(async () => {
+    await flush();
   });
 
   test('_handleMessageAnchorTap', () => {
@@ -446,7 +448,7 @@
     const args = navigateToChangeStub.getCall(0).args;
     assert.equal(args[0], element._change);
     assert.equal(args[1], 10 as PatchSetNum);
-    assert.equal(args[2], 1 as PatchSetNum);
+    assert.equal(args[2], 1 as BasePatchSetNum);
   });
 
   test('_handleDiffBaseAgainstLeft', () => {
@@ -484,7 +486,7 @@
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
     assert.equal(args[1], 10 as PatchSetNum);
-    assert.equal(args[2], 3 as PatchSetNum);
+    assert.equal(args[2], 3 as BasePatchSetNum);
   });
 
   test('_handleDiffBaseAgainstLatest', () => {
@@ -506,12 +508,45 @@
     assert.isNotOk(args[2]);
   });
 
+  test('toggle attention set status', async () => {
+    element._change = {
+      ...createChangeViewChange(),
+      revisions: createRevisions(10),
+    };
+    const addToAttentionSetStub = stubRestApi('addToAttentionSet').returns(
+      Promise.resolve(new Response())
+    );
+
+    const removeFromAttentionSetStub = stubRestApi(
+      'removeFromAttentionSet'
+    ).returns(Promise.resolve(new Response()));
+    element._patchRange = {
+      basePatchNum: 1 as BasePatchSetNum,
+      patchNum: 3 as RevisionPatchSetNum,
+    };
+    sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+
+    assert.isNotOk(element._change.attention_set);
+    await element._getLoggedIn();
+    await element.restApiService.getAccount();
+    element._handleToggleAttentionSet(
+      new CustomEvent('') as CustomKeyboardEvent
+    );
+    assert.isTrue(addToAttentionSetStub.called);
+    assert.isFalse(removeFromAttentionSetStub.called);
+
+    element._handleToggleAttentionSet(
+      new CustomEvent('') as CustomKeyboardEvent
+    );
+    assert.isTrue(removeFromAttentionSetStub.called);
+  });
+
   suite('plugins adding to file tab', () => {
-    setup(done => {
+    setup(async () => {
       element._changeNum = TEST_NUMERIC_CHANGE_ID;
       // Resolving it here instead of during setup() as other tests depend
       // on flush() not being called during setup.
-      flush(() => done());
+      await flush();
     });
 
     test('plugin added tab shows up as a dynamic endpoint', () => {
@@ -527,19 +562,17 @@
       assert.equal(paperTabs[2].dataset.name, 'change-view-tab-header-url');
     });
 
-    test('_setActivePrimaryTab switched tab correctly', done => {
+    test('_setActivePrimaryTab switched tab correctly', async () => {
       element._setActivePrimaryTab(
         new CustomEvent('', {
           detail: {tab: 'change-view-tab-header-url'},
         })
       );
-      flush(() => {
-        assert.equal(element._activeTabs[0], 'change-view-tab-header-url');
-        done();
-      });
+      await flush();
+      assert.equal(element._activeTabs[0], 'change-view-tab-header-url');
     });
 
-    test('show-primary-tab switched primary tab correctly', done => {
+    test('show-primary-tab switched primary tab correctly', async () => {
       element.dispatchEvent(
         new CustomEvent('show-primary-tab', {
           composed: true,
@@ -549,13 +582,11 @@
           },
         })
       );
-      flush(() => {
-        assert.equal(element._activeTabs[0], 'change-view-tab-header-url');
-        done();
-      });
+      await flush();
+      assert.equal(element._activeTabs[0], 'change-view-tab-header-url');
     });
 
-    test('param change should switch primary tab correctly', done => {
+    test('param change should switch primary tab correctly', async () => {
       assert.equal(element._activeTabs[0], PrimaryTab.FILES);
       const queryMap = new Map<string, string>();
       queryMap.set('tab', PrimaryTab.FINDINGS);
@@ -565,13 +596,11 @@
         ...element.params,
         queryMap,
       };
-      flush(() => {
-        assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
-        done();
-      });
+      await flush();
+      assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
     });
 
-    test('invalid param change should not switch primary tab', done => {
+    test('invalid param change should not switch primary tab', async () => {
       assert.equal(element._activeTabs[0], PrimaryTab.FILES);
       const queryMap = new Map<string, string>();
       queryMap.set('tab', 'random');
@@ -581,22 +610,18 @@
         ...element.params,
         queryMap,
       };
-      flush(() => {
-        assert.equal(element._activeTabs[0], PrimaryTab.FILES);
-        done();
-      });
+      await flush();
+      assert.equal(element._activeTabs[0], PrimaryTab.FILES);
     });
 
-    test('switching tab sets _selectedTabPluginEndpoint', done => {
+    test('switching tab sets _selectedTabPluginEndpoint', async () => {
       const paperTabs = element.shadowRoot!.querySelector('#primaryTabs')!;
       tap(paperTabs.querySelectorAll('paper-tab')[2]);
-      flush(() => {
-        assert.equal(
-          element._selectedTabPluginEndpoint,
-          'change-view-tab-content-url'
-        );
-        done();
-      });
+      await flush();
+      assert.equal(
+        element._selectedTabPluginEndpoint,
+        'change-view-tab-content-url'
+      );
     });
   });
 
@@ -653,28 +678,24 @@
       );
     });
 
-    test('A fires an error event when not logged in', done => {
+    test('A fires an error event when not logged in', async () => {
       sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       pressAndReleaseKeyOn(element, 65, null, 'a');
-      flush(() => {
-        assert.isFalse(element.$.replyOverlay.opened);
-        assert.isTrue(loggedInErrorSpy.called);
-        done();
-      });
+      await flush();
+      assert.isFalse(element.$.replyOverlay.opened);
+      assert.isTrue(loggedInErrorSpy.called);
     });
 
-    test('shift A does not open reply overlay', done => {
+    test('shift A does not open reply overlay', async () => {
       sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
       pressAndReleaseKeyOn(element, 65, 'shift', 'a');
-      flush(() => {
-        assert.isFalse(element.$.replyOverlay.opened);
-        done();
-      });
+      await flush();
+      assert.isFalse(element.$.replyOverlay.opened);
     });
 
-    test('A toggles overlay when logged in', done => {
+    test('A toggles overlay when logged in', async () => {
       sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
       element._change = {
         ...createChangeViewChange(),
@@ -695,19 +716,17 @@
       const openSpy = sinon.spy(element, '_openReplyDialog');
 
       pressAndReleaseKeyOn(element, 65, null, 'a');
-      flush(() => {
-        assert.isTrue(element.$.replyOverlay.opened);
-        element.$.replyOverlay.close();
-        assert.isFalse(element.$.replyOverlay.opened);
-        assert(
-          openSpy.lastCall.calledWithExactly(
-            element.$.replyDialog.FocusTarget.ANY
-          ),
-          '_openReplyDialog should have been passed ANY'
-        );
-        assert.equal(openSpy.callCount, 1);
-        done();
-      });
+      await flush();
+      assert.isTrue(element.$.replyOverlay.opened);
+      element.$.replyOverlay.close();
+      assert.isFalse(element.$.replyOverlay.opened);
+      assert(
+        openSpy.lastCall.calledWithExactly(
+          element.$.replyDialog.FocusTarget.ANY
+        ),
+        '_openReplyDialog should have been passed ANY'
+      );
+      assert.equal(openSpy.callCount, 1);
     });
 
     test('fullscreen-overlay-opened hides content', () => {
@@ -785,28 +804,24 @@
       assert.isTrue(handleCollapse.called);
     });
 
-    test('X should expand all messages', done => {
-      flush(() => {
-        const handleExpand = sinon.stub(
-          element.messagesList!,
-          'handleExpandCollapse'
-        );
-        pressAndReleaseKeyOn(element, 88, null, 'x');
-        assert(handleExpand.calledWith(true));
-        done();
-      });
+    test('X should expand all messages', async () => {
+      await flush();
+      const handleExpand = sinon.stub(
+        element.messagesList!,
+        'handleExpandCollapse'
+      );
+      pressAndReleaseKeyOn(element, 88, null, 'x');
+      assert(handleExpand.calledWith(true));
     });
 
-    test('Z should collapse all messages', done => {
-      flush(() => {
-        const handleExpand = sinon.stub(
-          element.messagesList!,
-          'handleExpandCollapse'
-        );
-        pressAndReleaseKeyOn(element, 90, null, 'z');
-        assert(handleExpand.calledWith(false));
-        done();
-      });
+    test('Z should collapse all messages', async () => {
+      await flush();
+      const handleExpand = sinon.stub(
+        element.messagesList!,
+        'handleExpandCollapse'
+      );
+      pressAndReleaseKeyOn(element, 90, null, 'z');
+      assert(handleExpand.calledWith(false));
     });
 
     test('d should open download overlay', () => {
@@ -946,12 +961,10 @@
       );
     });
 
-    test('changing patchsets resets robot comments', done => {
+    test('changing patchsets resets robot comments', async () => {
       element.set('_change.current_revision', 'rev3');
-      flush(() => {
-        assert.equal(element._robotCommentThreads!.length, 1);
-        done();
-      });
+      await flush();
+      assert.equal(element._robotCommentThreads!.length, 1);
     });
 
     test('Show more button is hidden', () => {
@@ -959,15 +972,13 @@
     });
 
     suite('robot comments show more button', () => {
-      setup(done => {
+      setup(async () => {
         const arr = [];
         for (let i = 0; i <= 30; i++) {
           arr.push(...THREADS);
         }
         element._commentThreads = arr;
-        flush(() => {
-          done();
-        });
+        await flush();
       });
 
       test('Show more button is rendered', () => {
@@ -978,12 +989,10 @@
         );
       });
 
-      test('Clicking show more button renders all comments', done => {
+      test('Clicking show more button renders all comments', async () => {
         tap(element.shadowRoot!.querySelector('.show-robot-comments')!);
-        flush(() => {
-          assert.equal(element._robotCommentThreads!.length, 62);
-          done();
-        });
+        await flush();
+        assert.equal(element._robotCommentThreads!.length, 62);
       });
     });
   });
@@ -1005,14 +1014,12 @@
     assert.isTrue(openDialogStub.called);
   });
 
-  test('fetches the server config on attached', done => {
-    flush(() => {
-      assert.equal(
-        element._serverConfig!.user.anonymous_coward_name,
-        'test coward name'
-      );
-      done();
-    });
+  test('fetches the server config on attached', async () => {
+    await flush();
+    assert.equal(
+      element._serverConfig!.user.anonymous_coward_name,
+      'test coward name'
+    );
   });
 
   test('_changeStatuses', () => {
@@ -1047,7 +1054,7 @@
   });
 
   suite('ChangeStatus revert', () => {
-    test('do not show any chip if no revert created', done => {
+    test('do not show any chip if no revert created', async () => {
       const change = {
         ...createChange(),
         messages: createChangeMessages(2),
@@ -1066,20 +1073,18 @@
       element._change = change;
       element._mergeable = true;
       element._submitEnabled = true;
-      flush();
+      await flush();
       element.computeRevertSubmitted(element._change);
-      flush(() => {
-        assert.isFalse(
-          element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-        );
-        assert.isFalse(
-          element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-        );
-        done();
-      });
+      await flush();
+      assert.isFalse(
+        element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
+      );
+      assert.isFalse(
+        element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
+      );
     });
 
-    test('do not show any chip if all reverts are abandoned', done => {
+    test('do not show any chip if all reverts are abandoned', async () => {
       const change = {
         ...createChange(),
         messages: createChangeMessages(2),
@@ -1106,20 +1111,18 @@
       element._change = change;
       element._mergeable = true;
       element._submitEnabled = true;
-      flush();
+      await flush();
       element.computeRevertSubmitted(element._change);
-      flush(() => {
-        assert.isFalse(
-          element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-        );
-        assert.isFalse(
-          element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-        );
-        done();
-      });
+      await flush();
+      assert.isFalse(
+        element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
+      );
+      assert.isFalse(
+        element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
+      );
     });
 
-    test('show revert created if no revert is merged', done => {
+    test('show revert created if no revert is merged', async () => {
       const change = {
         ...createChange(),
         messages: createChangeMessages(2),
@@ -1144,20 +1147,18 @@
       element._change = change;
       element._mergeable = true;
       element._submitEnabled = true;
-      flush();
+      await flush();
       element.computeRevertSubmitted(element._change);
-      flush(() => {
-        assert.isFalse(
-          element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-        );
-        assert.isTrue(
-          element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-        );
-        done();
-      });
+      await flush();
+      assert.isFalse(
+        element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
+      );
+      assert.isTrue(
+        element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
+      );
     });
 
-    test('show revert submitted if revert is merged', done => {
+    test('show revert submitted if revert is merged', async () => {
       const change = {
         ...createChange(),
         messages: createChangeMessages(2),
@@ -1179,17 +1180,15 @@
       element._change = change;
       element._mergeable = true;
       element._submitEnabled = true;
-      flush();
+      await flush();
       element.computeRevertSubmitted(element._change);
-      flush(() => {
-        assert.isFalse(
-          element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
-        );
-        assert.isTrue(
-          element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
-        );
-        done();
-      });
+      await flush();
+      assert.isFalse(
+        element._changeStatuses?.includes(ChangeStates.REVERT_CREATED)
+      );
+      assert.isTrue(
+        element._changeStatuses?.includes(ChangeStates.REVERT_SUBMITTED)
+      );
     });
   });
 
@@ -1343,17 +1342,15 @@
     assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
   });
 
-  test('diffMode defaults to side by side without preferences', done => {
+  test('diffMode defaults to side by side without preferences', async () => {
     stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
     // No user prefs or diff view mode set.
 
-    element._setDiffViewMode()!.then(() => {
-      assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
-      done();
-    });
+    await element._setDiffViewMode()!;
+    assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
   });
 
-  test('diffMode defaults to preference when not already set', done => {
+  test('diffMode defaults to preference when not already set', async () => {
     stubRestApi('getPreferences').returns(
       Promise.resolve({
         ...createPreferences(),
@@ -1361,13 +1358,11 @@
       })
     );
 
-    element._setDiffViewMode()!.then(() => {
-      assert.equal(element.viewState.diffMode, DiffViewMode.UNIFIED);
-      done();
-    });
+    await element._setDiffViewMode()!;
+    assert.equal(element.viewState.diffMode, DiffViewMode.UNIFIED);
   });
 
-  test('existing diffMode overrides preference', done => {
+  test('existing diffMode overrides preference', async () => {
     element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
     stubRestApi('getPreferences').returns(
       Promise.resolve({
@@ -1375,10 +1370,8 @@
         default_diff_view: DiffViewMode.UNIFIED,
       })
     );
-    element._setDiffViewMode()!.then(() => {
-      assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
-      done();
-    });
+    await element._setDiffViewMode()!;
+    assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
   });
 
   test('don’t reload entire page when patchRange changes', async () => {
@@ -1486,17 +1479,15 @@
     assert.isTrue(recreateSpy.calledOnce);
   });
 
-  test('related changes are not updated after other action', done => {
+  test('related changes are not updated after other action', async () => {
     sinon.stub(element, 'loadData').callsFake(() => Promise.resolve());
-    flush();
+    await flush();
     const relatedChanges = element.shadowRoot!.querySelector(
       '#relatedChanges'
     ) as GrRelatedChangesList;
     sinon.stub(relatedChanges, 'reload');
-    element.loadData(true).then(() => {
-      assert.isFalse(navigateToChangeStub.called);
-      done();
-    });
+    await element.loadData(true);
+    assert.isFalse(navigateToChangeStub.called);
   });
 
   test('_computeCopyTextForTitle', () => {
@@ -1578,7 +1569,7 @@
     assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
   });
 
-  test('topic is coalesced to null', done => {
+  test('topic is coalesced to null', async () => {
     sinon.stub(element, '_changeChanged');
     stubRestApi('getChangeDetail').returns(
       Promise.resolve({
@@ -1589,13 +1580,11 @@
       })
     );
 
-    element._getChangeDetail().then(() => {
-      assert.isNull(element._change!.topic);
-      done();
-    });
+    await element._getChangeDetail();
+    assert.isNull(element._change!.topic);
   });
 
-  test('commit sha is populated from getChangeDetail', done => {
+  test('commit sha is populated from getChangeDetail', async () => {
     sinon.stub(element, '_changeChanged');
     stubRestApi('getChangeDetail').callsFake(() =>
       Promise.resolve({
@@ -1606,10 +1595,8 @@
       })
     );
 
-    element._getChangeDetail().then(() => {
-      assert.equal('foo', element._commitInfo!.commit);
-      done();
-    });
+    await element._getChangeDetail();
+    assert.equal('foo', element._commitInfo!.commit);
   });
 
   test('edit is added to change', () => {
@@ -1693,43 +1680,39 @@
     assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT');
   });
 
-  test('_openReplyDialog called with `ANY` when coming from tap event', done => {
-    flush(() => {
-      const openStub = sinon.stub(element, '_openReplyDialog');
-      tap(element.$.replyBtn);
-      assert(
-        openStub.lastCall.calledWithExactly(
-          element.$.replyDialog.FocusTarget.ANY
-        ),
-        '_openReplyDialog should have been passed ANY'
-      );
-      assert.equal(openStub.callCount, 1);
-      done();
-    });
+  test('_openReplyDialog called with `ANY` when coming from tap event', async () => {
+    await flush();
+    const openStub = sinon.stub(element, '_openReplyDialog');
+    tap(element.$.replyBtn);
+    assert(
+      openStub.lastCall.calledWithExactly(
+        element.$.replyDialog.FocusTarget.ANY
+      ),
+      '_openReplyDialog should have been passed ANY'
+    );
+    assert.equal(openStub.callCount, 1);
   });
 
   test(
     '_openReplyDialog called with `BODY` when coming from message reply' +
       'event',
-    done => {
-      flush(() => {
-        const openStub = sinon.stub(element, '_openReplyDialog');
-        element.messagesList!.dispatchEvent(
-          new CustomEvent('reply', {
-            detail: {message: {message: 'text'}},
-            composed: true,
-            bubbles: true,
-          })
-        );
-        assert(
-          openStub.lastCall.calledWithExactly(
-            element.$.replyDialog.FocusTarget.BODY
-          ),
-          '_openReplyDialog should have been passed BODY'
-        );
-        assert.equal(openStub.callCount, 1);
-        done();
-      });
+    async () => {
+      await flush();
+      const openStub = sinon.stub(element, '_openReplyDialog');
+      element.messagesList!.dispatchEvent(
+        new CustomEvent('reply', {
+          detail: {message: {message: 'text'}},
+          composed: true,
+          bubbles: true,
+        })
+      );
+      assert(
+        openStub.lastCall.calledWithExactly(
+          element.$.replyDialog.FocusTarget.BODY
+        ),
+        '_openReplyDialog should have been passed BODY'
+      );
+      assert.equal(openStub.callCount, 1);
     }
   );
 
@@ -1771,7 +1754,7 @@
     assert.isNull(element._getUrlParameter('test'));
   });
 
-  test('revert dialog opened with revert param', done => {
+  test('revert dialog opened with revert param', async () => {
     const awaitPluginsLoadedStub = sinon
       .stub(getPluginLoader(), 'awaitPluginsLoaded')
       .callsFake(() => Promise.resolve());
@@ -1797,10 +1780,15 @@
       return param;
     });
 
-    sinon.stub(element.$.actions, 'showRevertDialog').callsFake(done);
+    const promise = mockPromise();
+
+    sinon
+      .stub(element.$.actions, 'showRevertDialog')
+      .callsFake(() => promise.resolve());
 
     element._maybeShowRevertDialog();
     assert.isTrue(awaitPluginsLoadedStub.called);
+    await promise;
   });
 
   suite('reply dialog tests', () => {
@@ -1823,7 +1811,7 @@
       );
     });
 
-    test('show reply dialog on open-reply-dialog event', done => {
+    test('show reply dialog on open-reply-dialog event', async () => {
       const openReplyDialogStub = sinon.stub(element, '_openReplyDialog');
       element.dispatchEvent(
         new CustomEvent('open-reply-dialog', {
@@ -1832,10 +1820,8 @@
           detail: {},
         })
       );
-      flush(() => {
-        assert.isTrue(openReplyDialogStub.calledOnce);
-        done();
-      });
+      await flush();
+      assert.isTrue(openReplyDialogStub.calledOnce);
     });
 
     test('reply from comment adds quote text', () => {
@@ -1887,13 +1873,11 @@
     });
   });
 
-  test('reply button is disabled until server config is loaded', done => {
+  test('reply button is disabled until server config is loaded', async () => {
     assert.isTrue(element._replyDisabled);
     // fetches the server config on attached
-    flush(() => {
-      assert.isFalse(element._replyDisabled);
-      done();
-    });
+    await flush();
+    assert.isFalse(element._replyDisabled);
   });
 
   test('header class computation', () => {
@@ -1901,19 +1885,17 @@
     assert.equal(element._computeHeaderClass(true), 'header editMode');
   });
 
-  test('_maybeScrollToMessage', done => {
-    flush(() => {
-      const scrollStub = sinon.stub(element.messagesList!, 'scrollToMessage');
+  test('_maybeScrollToMessage', async () => {
+    await flush();
+    const scrollStub = sinon.stub(element.messagesList!, 'scrollToMessage');
 
-      element._maybeScrollToMessage('');
-      assert.isFalse(scrollStub.called);
-      element._maybeScrollToMessage('message');
-      assert.isFalse(scrollStub.called);
-      element._maybeScrollToMessage('#message-TEST');
-      assert.isTrue(scrollStub.called);
-      assert.equal(scrollStub.lastCall.args[0], 'TEST');
-      done();
-    });
+    element._maybeScrollToMessage('');
+    assert.isFalse(scrollStub.called);
+    element._maybeScrollToMessage('message');
+    assert.isFalse(scrollStub.called);
+    element._maybeScrollToMessage('#message-TEST');
+    assert.isTrue(scrollStub.called);
+    assert.equal(scrollStub.lastCall.args[0], 'TEST');
   });
 
   test('topic update reloads related changes', () => {
@@ -2172,94 +2154,93 @@
       };
     });
 
-    test('edit exists in revisions', done => {
+    test('edit exists in revisions', async () => {
+      const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 2);
         assert.equal(args[1], EditPatchSetNum); // patchNum
-        done();
+        promise.resolve();
       });
 
       element.set('_change.revisions.rev2', {
         _number: EditPatchSetNum,
       });
-      flush();
+      await flush();
 
       fireEdit();
+      await promise;
     });
 
-    test('no edit exists in revisions, non-latest patchset', done => {
+    test('no edit exists in revisions, non-latest patchset', async () => {
+      const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 4);
         assert.equal(args[1], 1 as PatchSetNum); // patchNum
         assert.equal(args[3], true); // opt_isEdit
-        done();
+        promise.resolve();
       });
 
       element.set('_change.revisions.rev2', {_number: 2});
       element._patchRange = {patchNum: 1 as RevisionPatchSetNum};
-      flush();
+      await flush();
 
       fireEdit();
+      await promise;
     });
 
-    test('no edit exists in revisions, latest patchset', done => {
+    test('no edit exists in revisions, latest patchset', async () => {
+      const promise = mockPromise();
       sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 4);
         // No patch should be specified when patchNum == latest.
         assert.isNotOk(args[1]); // patchNum
         assert.equal(args[3], true); // opt_isEdit
-        done();
+        promise.resolve();
       });
 
       element.set('_change.revisions.rev2', {_number: 2});
       element._patchRange = {patchNum: 2 as RevisionPatchSetNum};
-      flush();
+      await flush();
 
       fireEdit();
+      await promise;
     });
   });
 
-  test('_handleStopEditTap', done => {
+  test('_handleStopEditTap', async () => {
     element._change = {
       ...createChangeViewChange(),
     };
     sinon.stub(element.$.metadata, '_computeLabelNames');
     navigateToChangeStub.restore();
+    const promise = mockPromise();
     sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
       assert.equal(args.length, 2);
       assert.equal(args[1], 1 as PatchSetNum); // patchNum
-      done();
+      promise.resolve();
     });
 
     element._patchRange = {patchNum: 1 as RevisionPatchSetNum};
     element.$.actions.dispatchEvent(
       new CustomEvent('stop-edit-tap', {bubbles: false})
     );
+    await promise;
   });
 
   suite('plugin endpoints', () => {
-    test('endpoint params', done => {
+    test('endpoint params', async () => {
       element._change = {...createChangeViewChange(), labels: {}};
       element._selectedRevision = createRevision();
-      let hookEl: HTMLElement;
-      let plugin: PluginApi;
-      pluginApi.install(
-        p => {
-          plugin = p;
-          plugin
-            .hook('change-view-integration')
-            .getLastAttached()
-            .then(el => (hookEl = el));
-        },
-        '0.1',
-        'http://some/plugins/url.js'
-      );
-      flush(() => {
-        assert.strictEqual((hookEl as any).plugin, plugin);
-        assert.strictEqual((hookEl as any).change, element._change);
-        assert.strictEqual((hookEl as any).revision, element._selectedRevision);
-        done();
-      });
+      const promise = mockPromise();
+      pluginApi.install(promise.resolve, '0.1', 'http://some/plugins/url.js');
+      await flush();
+      const plugin: PluginApi = (await promise) as PluginApi;
+      const hookEl = await plugin
+        .hook('change-view-integration')
+        .getLastAttached();
+      assert.strictEqual((hookEl as any).plugin, plugin);
+      assert.strictEqual((hookEl as any).change, element._change);
+      assert.strictEqual((hookEl as any).revision, element._selectedRevision);
     });
   });
 
@@ -2328,7 +2309,7 @@
         .returns(Promise.resolve([undefined, undefined, undefined]));
     });
 
-    test("don't report changeDisplayed on reply", done => {
+    test("don't report changeDisplayed on reply", async () => {
       const changeDisplayStub = sinon.stub(
         appContext.reportingService,
         'changeDisplayed'
@@ -2338,11 +2319,9 @@
         'changeFullyLoaded'
       );
       element._handleReplySent();
-      flush(() => {
-        assert.isFalse(changeDisplayStub.called);
-        assert.isFalse(changeFullyLoadedStub.called);
-        done();
-      });
+      await flush();
+      assert.isFalse(changeDisplayStub.called);
+      assert.isFalse(changeFullyLoadedStub.called);
     });
 
     test('report changeDisplayed on _paramsChanged', async () => {
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index 796b35d..4326939 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -19,8 +19,8 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {ChangeInfo, CommitInfo, ServerInfo} from '../../../types/common';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -29,7 +29,7 @@
 }
 
 @customElement('gr-commit-info')
-export class GrCommitInfo extends GrLitElement {
+export class GrCommitInfo extends LitElement {
   // TODO(TS): can not use `?` here as @computed require dependencies as
   // not optional
   @property({type: Object})
@@ -43,7 +43,7 @@
   @property({type: Object})
   serverConfig: ServerInfo | undefined;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -55,7 +55,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <div class="container">
       <a
         target="_blank"
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
index 0954b7f..07da53f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
@@ -35,10 +35,11 @@
   }
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-confirm-abandon-dialog')
-export class GrConfirmAbandonDialog extends KeyboardShortcutMixin(
-  PolymerElement
-) {
+export class GrConfirmAbandonDialog extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -56,7 +57,7 @@
    */
 
   @property({type: String})
-  message?: string;
+  message = '';
 
   get keyBindings() {
     return {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js
deleted file mode 100644
index 14d16f5..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-confirm-abandon-dialog.js';
-
-const basicFixture = fixtureFromElement('gr-confirm-abandon-dialog');
-
-suite('gr-confirm-abandon-dialog tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('_handleConfirmTap', () => {
-    const confirmHandler = sinon.stub();
-    element.addEventListener('confirm', confirmHandler);
-    sinon.spy(element, '_handleConfirmTap');
-    sinon.spy(element, '_confirm');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('confirm', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(confirmHandler.called);
-    assert.isTrue(confirmHandler.calledOnce);
-    assert.isTrue(element._handleConfirmTap.called);
-    assert.isTrue(element._confirm.called);
-    assert.isTrue(element._confirm.calledOnce);
-  });
-
-  test('_handleCancelTap', () => {
-    const cancelHandler = sinon.stub();
-    element.addEventListener('cancel', cancelHandler);
-    sinon.spy(element, '_handleCancelTap');
-    element.shadowRoot
-        .querySelector('gr-dialog').dispatchEvent(
-            new CustomEvent('cancel', {
-              composed: true, bubbles: true,
-            }));
-    assert.isTrue(cancelHandler.called);
-    assert.isTrue(cancelHandler.calledOnce);
-    assert.isTrue(element._handleCancelTap.called);
-    assert.isTrue(element._handleCancelTap.calledOnce);
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
new file mode 100644
index 0000000..08dda14
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.ts
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-confirm-abandon-dialog';
+import {GrConfirmAbandonDialog} from './gr-confirm-abandon-dialog';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+
+const basicFixture = fixtureFromElement('gr-confirm-abandon-dialog');
+
+suite('gr-confirm-abandon-dialog tests', () => {
+  let element: GrConfirmAbandonDialog;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('_handleConfirmTap', () => {
+    const confirmHandler = sinon.stub();
+    element.addEventListener('confirm', confirmHandler);
+    const confirmTapSpy = sinon.spy(element, '_handleConfirmTap');
+    const confirmSpy = sinon.spy(element, '_confirm');
+    queryAndAssert<GrDialog>(element, 'gr-dialog').dispatchEvent(
+      new CustomEvent('confirm', {
+        composed: true,
+        bubbles: true,
+      })
+    );
+    assert.isTrue(confirmHandler.called);
+    assert.isTrue(confirmHandler.calledOnce);
+    assert.isTrue(confirmTapSpy.called);
+    assert.isTrue(confirmSpy.called);
+    assert.isTrue(confirmSpy.calledOnce);
+  });
+
+  test('_handleCancelTap', () => {
+    const cancelHandler = sinon.stub();
+    element.addEventListener('cancel', cancelHandler);
+    const cancelTapSpy = sinon.spy(element, '_handleCancelTap');
+    queryAndAssert<GrDialog>(element, 'gr-dialog').dispatchEvent(
+      new CustomEvent('cancel', {
+        composed: true,
+        bubbles: true,
+      })
+    );
+    assert.isTrue(cancelHandler.called);
+    assert.isTrue(cancelHandler.calledOnce);
+    assert.isTrue(cancelTapSpy.called);
+    assert.isTrue(cancelTapSpy.calledOnce);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index a090aee..b061043 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -25,15 +25,14 @@
 import {appContext} from '../../../services/app-context';
 import {
   ChangeInfo,
-  BranchInfo,
-  RepoName,
   BranchName,
+  RepoName,
   CommitId,
   ChangeInfoId,
 } from '../../../types/common';
 import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
 import {customElement, property, observe} from '@polymer/decorators';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 import {HttpMethod, ChangeStatus} from '../../../constants/constants';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {fireEvent} from '../../../utils/event-util';
@@ -68,7 +67,7 @@
 
 export interface GrConfirmCherrypickDialog {
   $: {
-    branchInput: HTMLElement;
+    branchInput: GrTypedAutocomplete<BranchName>;
   };
 }
 
@@ -91,7 +90,7 @@
    */
 
   @property({type: String})
-  branch?: BranchName;
+  branch = '' as BranchName;
 
   @property({type: String})
   baseCommit?: string;
@@ -106,7 +105,7 @@
   commitNum?: CommitId;
 
   @property({type: String})
-  message?: string;
+  message = '';
 
   @property({type: String})
   project?: RepoName;
@@ -115,7 +114,7 @@
   changes: ChangeInfo[] = [];
 
   @property({type: Object})
-  _query: (input: string) => Promise<AutocompleteSuggestion[]>;
+  _query?: (input: string) => Promise<{name: BranchName}[]>;
 
   @property({type: Boolean})
   _showCherryPickTopic = false;
@@ -249,7 +248,7 @@
     cherryPickType: CherryPickType,
     duplicateProjectChanges: boolean,
     statuses: Statuses,
-    branch?: BranchName
+    branch: BranchName
   ) {
     if (!branch) return true;
     const duplicateProject =
@@ -298,6 +297,9 @@
     let newMessage = commitMessage;
 
     if (changeStatus === 'MERGED') {
+      if (!newMessage.endsWith('\n')) {
+        newMessage += '\n';
+      }
       newMessage += '(cherry picked from commit ' + commitNum.toString() + ')';
     }
     this.message = newMessage;
@@ -395,29 +397,22 @@
     this.$.branchInput.focus();
   }
 
-  _getProjectBranchesSuggestions(
-    input: string
-  ): Promise<AutocompleteSuggestion[]> {
-    if (!this.project) {
-      this.reporting.error(new Error('no project specified'));
-      return Promise.resolve([]);
-    }
+  _getProjectBranchesSuggestions(input: string) {
+    if (!this.project) return Promise.reject(new Error('Missing project'));
     if (input.startsWith('refs/heads/')) {
       input = input.substring('refs/heads/'.length);
     }
     return this.restApiService
       .getRepoBranches(input, this.project, SUGGESTIONS_LIMIT)
-      .then((response: BranchInfo[] | undefined) => {
+      .then(response => {
         if (!response) return [];
-        const branches = [];
+        const branches: Array<{name: BranchName}> = [];
         for (const branchInfo of response) {
-          let branch;
-          if (branchInfo.ref.startsWith('refs/heads/')) {
-            branch = branchInfo.ref.substring('refs/heads/'.length);
-          } else {
-            branch = branchInfo.ref;
+          let name: string = branchInfo.ref;
+          if (name.startsWith('refs/heads/')) {
+            name = name.substring('refs/heads/'.length);
           }
-          branches.push({name: branch});
+          branches.push({name: name as BranchName});
         }
         return branches;
       });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index 1034674..1256cc1 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -46,6 +46,16 @@
     element.project = 'test-project';
   });
 
+  test('with message missing newline', () => {
+    element.changeStatus = 'MERGED';
+    element.commitMessage = 'message';
+    element.commitNum = '123';
+    element.branch = 'master';
+    flush();
+    const expectedMessage = 'message\n(cherry picked from commit 123)';
+    assert.equal(element.message, expectedMessage);
+  });
+
   test('with merged change', () => {
     element.changeStatus = 'MERGED';
     element.commitMessage = 'message\n';
@@ -105,28 +115,26 @@
         current_revision: 'a',
       },
     ];
-    setup(() => {
+    setup(async () => {
       element.updateChanges(changes);
       element._cherryPickType = CHERRY_PICK_TYPES.TOPIC;
-      flush();
+      await flush();
     });
 
-    test('cherry pick topic submit', done => {
+    test('cherry pick topic submit', async () => {
       element.branch = 'master';
       const executeChangeActionStub = stubRestApi(
           'executeChangeAction').returns(Promise.resolve([]));
       MockInteractions.tap(element.shadowRoot.
           querySelector('gr-dialog').$.confirm);
-      flush(() => {
-        const args = executeChangeActionStub.args[0];
-        assert.equal(args[0], 1);
-        assert.equal(args[1], 'POST');
-        assert.equal(args[2], '/cherrypick');
-        assert.equal(args[4].destination, 'master');
-        assert.isTrue(args[4].allow_conflicts);
-        assert.isTrue(args[4].allow_empty);
-        done();
-      });
+      await flush();
+      const args = executeChangeActionStub.args[0];
+      assert.equal(args[0], 1);
+      assert.equal(args[1], 'POST');
+      assert.equal(args[2], '/cherrypick');
+      assert.equal(args[4].destination, 'master');
+      assert.isTrue(args[4].allow_conflicts);
+      assert.isTrue(args[4].allow_empty);
     });
 
     test('deselecting a change removes it from being cherry picked', () => {
@@ -171,18 +179,16 @@
       ), 'error');
     });
 
-    test('submit button is blocked while cherry picks is running', done => {
+    test('submit button is blocked while cherry picks is running', async () => {
       const confirmButton = element.shadowRoot.querySelector('gr-dialog').$
           .confirm;
       assert.isTrue(confirmButton.hasAttribute('disabled'));
       element.branch = 'b';
-      flush();
+      await flush();
       assert.isFalse(confirmButton.hasAttribute('disabled'));
       element.updateStatus(changes[0], {status: 'RUNNING'});
-      flush(() => {
-        assert.isTrue(confirmButton.hasAttribute('disabled'));
-        done();
-      });
+      await flush();
+      assert.isTrue(confirmButton.hasAttribute('disabled'));
     });
   });
 
@@ -192,12 +198,11 @@
     assert.isTrue(focusStub.called);
   });
 
-  test('_getProjectBranchesSuggestions non-empty', done => {
-    element._getProjectBranchesSuggestions('test-branch').then(branches => {
-      assert.equal(branches.length, 1);
-      assert.equal(branches[0].name, 'test-branch');
-      done();
-    });
+  test('_getProjectBranchesSuggestions non-empty', async () => {
+    const branches = await element._getProjectBranchesSuggestions(
+        'test-branch');
+    assert.equal(branches.length, 1);
+    assert.equal(branches[0].name, 'test-branch');
   });
 });
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index eb4053a..8e6521d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -21,14 +21,25 @@
 import {htmlTemplate} from './gr-confirm-move-dialog_html';
 import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {customElement, property} from '@polymer/decorators';
-import {RepoName, BranchName} from '../../../types/common';
-import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {BranchName, RepoName} from '../../../types/common';
 import {appContext} from '../../../services/app-context';
+import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
 
 const SUGGESTIONS_LIMIT = 15;
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
+// This is used to make sure 'branch'
+// can be typed as BranchName.
+export interface GrConfirmMoveDialog {
+  $: {
+    branchInput: GrTypedAutocomplete<BranchName>;
+  };
+}
+
 @customElement('gr-confirm-move-dialog')
-export class GrConfirmMoveDialog extends KeyboardShortcutMixin(PolymerElement) {
+export class GrConfirmMoveDialog extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -46,16 +57,16 @@
    */
 
   @property({type: String})
-  branch?: BranchName;
+  branch = '' as BranchName;
 
   @property({type: String})
-  message?: string;
+  message = '';
 
   @property({type: String})
   project?: RepoName;
 
   @property({type: Object})
-  _query: (input: string) => Promise<AutocompleteSuggestion[]>;
+  _query?: (input: string) => Promise<{name: BranchName}[]>;
 
   get keyBindings() {
     return {
@@ -92,9 +103,7 @@
     );
   }
 
-  _getProjectBranchesSuggestions(
-    input: string
-  ): Promise<AutocompleteSuggestion[]> {
+  _getProjectBranchesSuggestions(input: string) {
     if (!this.project) return Promise.reject(new Error('Missing project'));
     if (input.startsWith('refs/heads/')) {
       input = input.substring('refs/heads/'.length);
@@ -102,21 +111,15 @@
     return this.restApiService
       .getRepoBranches(input, this.project, SUGGESTIONS_LIMIT)
       .then(response => {
-        const branches: AutocompleteSuggestion[] = [];
-        let branch;
-        if (response) {
-          response.forEach(value => {
-            if (value.ref.startsWith('refs/heads/')) {
-              branch = value.ref.substring('refs/heads/'.length);
-            } else {
-              branch = value.ref;
-            }
-            branches.push({
-              name: branch,
-            });
-          });
+        if (!response) return [];
+        const branches: Array<{name: BranchName}> = [];
+        for (const branchInfo of response) {
+          let name: string = branchInfo.ref;
+          if (name.startsWith('refs/heads/')) {
+            name = name.substring('refs/heads/'.length);
+          }
+          branches.push({name: name as BranchName});
         }
-
         return branches;
       });
   }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
deleted file mode 100644
index 36a2ad3..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-confirm-move-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-confirm-move-dialog');
-
-suite('gr-confirm-move-dialog tests', () => {
-  let element;
-
-  setup(() => {
-    stubRestApi('getRepoBranches').callsFake(input => {
-      if (input.startsWith('test')) {
-        return Promise.resolve([
-          {
-            ref: 'refs/heads/test-branch',
-            revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
-            can_delete: true,
-          },
-        ]);
-      } else {
-        return Promise.resolve(undefined);
-      }
-    });
-    element = basicFixture.instantiate();
-    element.project = 'test-project';
-  });
-
-  test('with updated commit message', () => {
-    element.branch = 'master';
-    const myNewMessage = 'updated commit message';
-    element.message = myNewMessage;
-    flush();
-    assert.equal(element.message, myNewMessage);
-  });
-
-  test('_getProjectBranchesSuggestions empty', done => {
-    element._getProjectBranchesSuggestions('nonexistent').then(branches => {
-      assert.equal(branches.length, 0);
-      done();
-    });
-  });
-
-  test('_getProjectBranchesSuggestions non-empty', done => {
-    element._getProjectBranchesSuggestions('test-branch').then(branches => {
-      assert.equal(branches.length, 1);
-      assert.equal(branches[0].name, 'test-branch');
-      done();
-    });
-  });
-
-  test('_getProjectBranchesSuggestions input empty string', done => {
-    element._getProjectBranchesSuggestions('').then(branches => {
-      assert.equal(branches.length, 0);
-      done();
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
new file mode 100644
index 0000000..ea5d320
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.ts
@@ -0,0 +1,74 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-confirm-move-dialog';
+import {GrConfirmMoveDialog} from './gr-confirm-move-dialog';
+import {stubRestApi} from '../../../test/test-utils';
+import {BranchName, GitRef, RepoName} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-confirm-move-dialog');
+
+suite('gr-confirm-move-dialog tests', () => {
+  let element: GrConfirmMoveDialog;
+
+  setup(() => {
+    stubRestApi('getRepoBranches').callsFake((input: string) => {
+      if (input.startsWith('test')) {
+        return Promise.resolve([
+          {
+            ref: 'refs/heads/test-branch' as GitRef,
+            revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+            can_delete: true,
+          },
+        ]);
+      } else {
+        return Promise.resolve([]);
+      }
+    });
+    element = basicFixture.instantiate();
+    element.project = 'test-repo' as RepoName;
+  });
+
+  test('with updated commit message', () => {
+    element.branch = 'master' as BranchName;
+    const myNewMessage = 'updated commit message';
+    element.message = myNewMessage;
+    flush();
+    assert.equal(element.message, myNewMessage);
+  });
+
+  test('_getProjectBranchesSuggestions empty', async () => {
+    const branches = await element._getProjectBranchesSuggestions(
+      'nonexistent'
+    );
+    assert.equal(branches.length, 0);
+  });
+
+  test('_getProjectBranchesSuggestions non-empty', async () => {
+    const branches = await element._getProjectBranchesSuggestions(
+      'test-branch'
+    );
+    assert.equal(branches.length, 1);
+    assert.equal(branches[0].name, 'test-branch');
+  });
+
+  test('_getProjectBranchesSuggestions input empty string', async () => {
+    const branches = await element._getProjectBranchesSuggestions('');
+    assert.equal(branches.length, 0);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 95169ca..3ab9c82 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -25,11 +25,12 @@
 import {pluralize} from '../../../utils/string-util';
 import {CommentThread, isUnresolved} from '../../../utils/comment-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, query} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, query} from 'lit/decorators';
+import {fontStyles} from '../../../styles/gr-font-styles';
 
 @customElement('gr-confirm-submit-dialog')
-export class GrConfirmSubmitDialog extends GrLitElement {
+export class GrConfirmSubmitDialog extends LitElement {
   @query('#dialog')
   dialog?: GrDialog;
 
@@ -57,9 +58,10 @@
   @property({type: Boolean})
   _initialised = false;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
+      fontStyles,
       css`
         #dialog {
           min-width: 40em;
@@ -152,7 +154,7 @@
     `;
   }
 
-  render() {
+  override render() {
     return html` <gr-dialog
       id="dialog"
       confirm-label="Continue"
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
index 968800d..d0865c9 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-font-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-download-commands/gr-download-commands';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -41,8 +42,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-download-dialog')
-export class GrDownloadDialog extends KeyboardShortcutMixin(PolymerElement) {
+export class GrDownloadDialog extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.ts
index 6e40f84..097fb0e 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       display: block;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index ff303e3..bdc6a43 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -60,8 +60,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-file-list-header')
-export class GrFileListHeader extends KeyboardShortcutMixin(PolymerElement) {
+export class GrFileListHeader extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index bceee27..85f0330 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -180,50 +180,61 @@
           hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]"
           hidden=""
         >
-          <gr-button
-            link=""
-            has-tooltip=""
+          <gr-tooltip-content
+            has-tooltip
             title="Diff preferences"
-            class="prefsButton desktop"
-            on-click="_handlePrefsTap"
-            ><iron-icon icon="gr-icons:settings"></iron-icon
-          ></gr-button>
+          >
+            <gr-button
+              link=""
+
+              class="prefsButton desktop"
+              on-click="_handlePrefsTap"
+              ><iron-icon icon="gr-icons:settings"></iron-icon
+            ></gr-button>
+          </gr-tooltip-content>
         </span>
         <span class="separator"></span>
       </div>
       <span class="downloadContainer desktop">
-        <gr-button
-          link=""
-          class="download"
+        <gr-tooltip-content
+          has-tooltip
           title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
-                ShortcutSection.ACTIONS)]]"
-          has-tooltip=""
-          on-click="_handleDownloadTap"
-          >Download</gr-button
+                   ShortcutSection.ACTIONS)]]"
         >
+          <gr-button
+            link=""
+            class="download"
+            on-click="_handleDownloadTap"
+            >Download</gr-button
+          >
+        </gr-tooltip-content>
       </span>
       <template
         is="dom-if"
         if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"
       >
-        <gr-button
-          id="expandBtn"
-          link=""
-          title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
-                ShortcutSection.FILE_LIST)]]"
-          has-tooltip=""
-          on-click="_expandAllDiffs"
-          >Expand All</gr-button
-        >
-        <gr-button
-          id="collapseBtn"
-          link=""
-          on-click="_collapseAllDiffs"
-          title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
-          ShortcutSection.FILE_LIST)]]"
-          has-tooltip=""
-          >Collapse All</gr-button
-        >
+        <gr-tooltip-content
+            has-tooltip
+            title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+                  ShortcutSection.FILE_LIST)]]">
+          <gr-button
+            id="expandBtn"
+            link=""
+
+            on-click="_expandAllDiffs"
+            >Expand All</gr-button
+          >
+        <gr-tooltip-content
+            has-tooltip
+            title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
+                  ShortcutSection.FILE_LIST)]]">
+          <gr-button
+            id="collapseBtn"
+            link=""
+            on-click="_collapseAllDiffs"
+            >Collapse All</gr-button
+          >
+        </gr-tooltip-content>
       </template>
       <template
         is="dom-if"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index dd70678..c90cfcc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -34,10 +34,8 @@
     element = basicFixture.instantiate();
   });
 
-  teardown(done => {
-    flush(() => {
-      done();
-    });
+  teardown(async () => {
+    await flush();
   });
 
   test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => {
@@ -77,43 +75,41 @@
     assert.isTrue(element._collapseAllDiffs.called);
   });
 
-  test('show/hide diffs disabled for large amounts of files', done => {
+  test('show/hide diffs disabled for large amounts of files', async () => {
     const computeSpy = sinon.spy(element, '_fileListActionsVisible');
     element._files = [];
     element.changeNum = '42';
     element.basePatchNum = 'PARENT';
     element.patchNum = '2';
     element.shownFileCount = 1;
-    flush(() => {
-      assert.isTrue(computeSpy.lastCall.returnValue);
-      _.times(element._maxFilesForBulkActions + 1, () => {
-        element.shownFileCount = element.shownFileCount + 1;
-      });
-      assert.isFalse(computeSpy.lastCall.returnValue);
-      done();
+    await flush();
+    assert.isTrue(computeSpy.lastCall.returnValue);
+    _.times(element._maxFilesForBulkActions + 1, () => {
+      element.shownFileCount = element.shownFileCount + 1;
     });
+    assert.isFalse(computeSpy.lastCall.returnValue);
   });
 
-  test('fileViewActions are properly hidden', () => {
+  test('fileViewActions are properly hidden', async () => {
     const actions = element.shadowRoot
         .querySelector('.fileViewActions');
     assert.equal(getComputedStyle(actions).display, 'none');
     element.filesExpanded = FilesExpandedState.SOME;
-    flush();
+    await flush();
     assert.notEqual(getComputedStyle(actions).display, 'none');
     element.filesExpanded = FilesExpandedState.ALL;
-    flush();
+    await flush();
     assert.notEqual(getComputedStyle(actions).display, 'none');
     element.filesExpanded = FilesExpandedState.NONE;
-    flush();
+    await flush();
     assert.equal(getComputedStyle(actions).display, 'none');
   });
 
-  test('expand/collapse buttons are toggled correctly', () => {
+  test('expand/collapse buttons are toggled correctly', async () => {
     // Only the expand button should be visible in the initial state when
     // NO files are expanded.
     element.shownFileCount = 10;
-    flush();
+    await flush();
     const expandBtn = element.shadowRoot.querySelector('#expandBtn');
     const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
     assert.notEqual(getComputedStyle(expandBtn).display, 'none');
@@ -122,19 +118,19 @@
     // Both expand and collapse buttons should be visible when SOME files are
     // expanded.
     element.filesExpanded = FilesExpandedState.SOME;
-    flush();
+    await flush();
     assert.notEqual(getComputedStyle(expandBtn).display, 'none');
     assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
 
     // Only the collapse button should be visible when ALL files are expanded.
     element.filesExpanded = FilesExpandedState.ALL;
-    flush();
+    await flush();
     assert.equal(getComputedStyle(expandBtn).display, 'none');
     assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
 
     // Only the expand button should be visible when NO files are expanded.
     element.filesExpanded = FilesExpandedState.NONE;
-    flush();
+    await flush();
     assert.notEqual(getComputedStyle(expandBtn).display, 'none');
     assert.equal(getComputedStyle(collapseBtn).display, 'none');
   });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index b49fceb..40a2e1f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../../diff/gr-diff-cursor/gr-diff-cursor';
 import '../../diff/gr-diff-host/gr-diff-host';
@@ -172,8 +173,11 @@
  * @property {number} lines_inserted - fallback to 0 if not present in api
  */
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-file-list')
-export class GrFileList extends KeyboardShortcutMixin(PolymerElement) {
+export class GrFileList extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -1134,7 +1138,7 @@
     // Polymer 2: check for undefined
     if (
       change === undefined ||
-      patchRange === undefined ||
+      !patchRange?.patchNum ||
       path === undefined ||
       editMode === undefined
     ) {
@@ -1428,11 +1432,9 @@
     );
 
     // Find the paths introduced by the new index splices:
-    const newFiles = record.indexSplices
-      .map(splice =>
-        splice.object.slice(splice.index, splice.index + splice.addedCount)
-      )
-      .reduce((acc, paths) => acc.concat(paths), []);
+    const newFiles = record.indexSplices.flatMap(splice =>
+      splice.object.slice(splice.index, splice.index + splice.addedCount)
+    );
 
     // Required so that the newly created diff view is included in this.diffs.
     flush();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index 4d04744..35f01d8 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       display: block;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 225130f..b8c6cde 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
+import '../../shared/gr-date-formatter/gr-date-formatter.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-file-list.js';
 import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api.js';
@@ -43,7 +44,6 @@
   createParsedChange,
   createRevision,
 } from '../../../test/test-data-generators.js';
-import sinon from 'sinon/pkg/sinon-esm';
 import {createDefaultDiffPrefs} from '../../../constants/constants.js';
 import {queryAndAssert} from '../../../utils/common-util.js';
 
@@ -93,7 +93,7 @@
   });
 
   suite('basic tests', () => {
-    setup(done => {
+    setup(async () => {
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
       stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
@@ -118,7 +118,6 @@
       };
       saveStub = sinon.stub(element, '_saveReviewedState').callsFake(
           () => Promise.resolve());
-      done();
     });
 
     test('correct number of files are shown', () => {
@@ -943,14 +942,15 @@
       assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
 
-    test('_expandedFilesChanged', done => {
+    test('_expandedFilesChanged', async () => {
       sinon.stub(element, '_reviewFile');
       const path = 'path/to/my/file.txt';
+      const promise = mockPromise();
       const diffs = [{
         path,
         style: {},
         reload() {
-          done();
+          promise.resolve();
         },
         prefetchDiff() {},
         cancel() {},
@@ -964,6 +964,7 @@
       }];
       sinon.stub(element, 'diffs').get(() => diffs);
       element.push('_expandedFiles', {path});
+      await promise;
     });
 
     test('_clearCollapsedDiffs', () => {
@@ -1524,7 +1525,7 @@
       return diffs;
     }
 
-    setup(done => {
+    setup(async () => {
       stubRestApi('getPreferences').returns(Promise.resolve({}));
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
       stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
@@ -1569,13 +1570,12 @@
         patchNum: 2,
       };
       sinon.stub(window, 'fetch').callsFake(() => Promise.resolve());
-      flush();
-      done();
+      await flush();
     });
 
     test('cursor with individually opened files', async () => {
       MockInteractions.keyUpOn(element, 73, null, 'i');
-      flush();
+      await flush();
       let diffs = await renderAndGetNewDiffs(0);
       const diffStops = diffs[0].getCursorStops();
 
@@ -1588,13 +1588,13 @@
       // Tapping content on a line selects the line number.
       MockInteractions.tap(dom(
           diffStops[10]).querySelectorAll('.contentText')[0]);
-      flush();
+      await flush();
       assert.isTrue(diffStops[10].classList.contains('target-row'));
 
       // Keyboard shortcuts are still moving the file cursor, not the diff
       // cursor.
       MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
-      flush();
+      await flush();
       assert.isTrue(diffStops[10].classList.contains('target-row'));
       assert.isFalse(diffStops[11].classList.contains('target-row'));
 
@@ -1602,7 +1602,7 @@
       assert.equal(element.fileCursor.index, 1);
 
       MockInteractions.keyUpOn(element, 73, null, 'i');
-      flush();
+      await flush();
       diffs = await renderAndGetNewDiffs(1);
 
       // Two diffs should be rendered.
@@ -1617,7 +1617,7 @@
 
     test('cursor with toggle all files', async () => {
       MockInteractions.keyUpOn(element, 73, 'shift', 'i');
-      flush();
+      await flush();
 
       const diffs = await renderAndGetNewDiffs(0);
       const diffStops = diffs[0].getCursorStops();
@@ -1631,13 +1631,13 @@
       // Tapping content on a line selects the line number.
       MockInteractions.tap(dom(
           diffStops[10]).querySelectorAll('.contentText')[0]);
-      flush();
+      await flush();
       assert.isTrue(diffStops[10].classList.contains('target-row'));
 
       // Keyboard shortcuts are still moving the file cursor, not the diff
       // cursor.
       MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
-      flush();
+      await flush();
       assert.isFalse(diffStops[10].classList.contains('target-row'));
       assert.isTrue(diffStops[11].classList.contains('target-row'));
 
@@ -1662,9 +1662,9 @@
             element.root.querySelectorAll('.row:not(.header-row)');
       });
 
-      test('n key with some files expanded and no shift key', () => {
+      test('n key with some files expanded and no shift key', async () => {
         MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
-        flush();
+        await flush();
 
         // Handle N key should return before calling diff cursor functions.
         MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
@@ -1676,9 +1676,9 @@
         assert.equal(element.filesExpanded, 'some');
       });
 
-      test('n key with some files expanded and shift key', () => {
+      test('n key with some files expanded and shift key', async () => {
         MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
-        flush();
+        await flush();
         assert.equal(nextChunkStub.callCount, 0);
 
         MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
@@ -1690,9 +1690,9 @@
         assert.equal(element.filesExpanded, 'some');
       });
 
-      test('n key without all files expanded and shift key', () => {
+      test('n key without all files expanded and shift key', async () => {
         MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
-        flush();
+        await flush();
 
         MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
         assert.isTrue(nKeySpy.called);
@@ -1703,9 +1703,9 @@
         assert.equal(element.filesExpanded, FilesExpandedState.ALL);
       });
 
-      test('n key without all files expanded and no shift key', () => {
+      test('n key without all files expanded and no shift key', async () => {
         MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i');
-        flush();
+        await flush();
 
         MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
         assert.isTrue(nKeySpy.called);
@@ -1717,7 +1717,7 @@
       });
     });
 
-    test('_openSelectedFile behavior', () => {
+    test('_openSelectedFile behavior', async () => {
       const _filesByPath = element._filesByPath;
       element.set('_filesByPath', {});
       const navStub = sinon.stub(GerritNav, 'navigateToDiff');
@@ -1726,7 +1726,7 @@
       assert.isFalse(navStub.called);
 
       element.set('_filesByPath', _filesByPath);
-      flush();
+      await flush();
       // Navigates when a file is selected.
       element._openSelectedFile();
       assert.isTrue(navStub.called);
@@ -1754,7 +1754,7 @@
     });
 
     suite('editMode behavior', () => {
-      test('reviewed checkbox', () => {
+      test('reviewed checkbox', async () => {
         element._reviewFile.restore();
         const saveReviewStub = sinon.stub(element, '_saveReviewedState');
 
@@ -1763,7 +1763,7 @@
         assert.isTrue(saveReviewStub.calledOnce);
 
         element.editMode = true;
-        flush();
+        await flush();
 
         MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
         assert.isTrue(saveReviewStub.calledOnce);
@@ -1779,13 +1779,13 @@
       });
     });
 
-    test('editing actions', () => {
+    test('editing actions', async () => {
       // Edit controls are guarded behind a dom-if initially and not rendered.
       assert.isNotOk(dom(element.root)
           .querySelector('gr-edit-file-controls'));
 
       element.editMode = true;
-      flush();
+      await flush();
 
       // Commit message should not have edit controls.
       const editControls =
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
index c11e484..d50e00f 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
@@ -16,6 +16,7 @@
  */
 import '@polymer/iron-input/iron-input';
 import '../../../styles/shared-styles';
+import '../../../styles/gr-font-styles';
 import '../../shared/gr-button/gr-button';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-included-in-dialog_html';
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.ts
index 2029209..674b7e7 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       background-color: var(--dialog-background-color);
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
index 283a133..4e02155 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
@@ -76,7 +76,7 @@
     ]);
   });
 
-  test('_computeGroups with .bindValue', done => {
+  test('_computeGroups with .bindValue', async () => {
     queryAndAssert<IronInputElement>(element, '#filterInput')!.bindValue =
       'stable-3.2';
     const includedIn = {branches: [], tags: []} as IncludedInInfo;
@@ -84,14 +84,10 @@
       'master' as BranchName,
       'stable-3.2' as BranchName
     );
-
-    setTimeout(() => {
-      const filterText = element._filterText;
-      assert.deepEqual(element._computeGroups(includedIn, filterText), [
-        {title: 'Branches', items: ['stable-3.2']},
-      ]);
-
-      done();
-    });
+    await flush();
+    const filterText = element._filterText;
+    assert.deepEqual(element._computeGroups(includedIn, filterText), [
+      {title: 'Branches', items: ['stable-3.2']},
+    ]);
   });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 02416ff..9a38095 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -16,7 +16,6 @@
  */
 import '@polymer/iron-selector/iron-selector';
 import '../../shared/gr-button/gr-button';
-import '../../../styles/gr-voting-styles';
 import '../../../styles/shared-styles';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-label-score-row_html';
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
index c53f386..0af2f36 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
@@ -17,9 +17,6 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
-  <style include="gr-voting-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
   <style include="shared-styles">
     .labelNameCell,
     .buttonsCell,
@@ -48,58 +45,37 @@
     gr-button {
       min-width: 42px;
       box-sizing: border-box;
-      --gr-button: {
-        background-color: var(
-          --button-background-color,
-          var(--table-header-background-color)
-        );
-        padding: 0 var(--spacing-m);
-        @apply --vote-chip-styles;
-      }
     }
-    gr-button.iron-selected[vote='max'] {
+    gr-button::part(paper-button) {
+      background-color: var(
+        --button-background-color,
+        var(--table-header-background-color)
+      );
+      padding: 0 var(--spacing-m);
+    }
+    gr-tooltip-content.iron-selected > gr-button[vote='max'] {
       --button-background-color: var(--vote-color-approved);
     }
-    gr-button.iron-selected[vote='positive'] {
+    gr-tooltip-content.iron-selected > gr-buttonvote='positive'] {
       --button-background-color: var(--vote-color-recommended);
-      --gr-button: {
-        padding: 0 var(--spacing-m);
-        border-style: solid;
-        border-color: var(--vote-outline-recommended);
-        border-top-left-radius: 1em;
-        border-top-right-radius: 1em;
-        border-bottom-right-radius: 1em;
-        border-bottom-left-radius: 1em;
-        border-top-width: 1px;
-        border-right-width: 1px;
-        border-bottom-width: 1px;
-        border-left-width: 1px;
-        color: var(--chip-color);
-      }
     }
-    gr-button.iron-selected[vote='min'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='min'] {
       --button-background-color: var(--vote-color-rejected);
     }
-    gr-button.iron-selected[vote='negative'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='negative'] {
       --button-background-color: var(--vote-color-disliked);
-      --gr-button: {
-        padding: 0 var(--spacing-m);
-        border-style: solid;
-        border-color: var(--vote-outline-disliked);
-        border-top-left-radius: 1em;
-        border-top-right-radius: 1em;
-        border-bottom-right-radius: 1em;
-        border-bottom-left-radius: 1em;
-        border-top-width: 1px;
-        border-right-width: 1px;
-        border-bottom-width: 1px;
-        border-left-width: 1px;
-        color: var(--chip-color);
-      }
     }
-    gr-button.iron-selected[vote='neutral'] {
+    gr-tooltip-content.iron-selected > gr-button[vote='neutral'] {
       --button-background-color: var(--vote-color-neutral);
     }
+    gr-tooltip-content.iron-selected
+      > gr-button[vote='positive']::part(paper-button) {
+      border-color: var(--vote-outline-recommended);
+    }
+    gr-tooltip-content.iron-selected
+      > gr-button[vote='negative']::part(paper-button) {
+      border-color: var(--vote-outline-disliked);
+    }
     .placeholder {
       display: inline-block;
       width: 42px;
@@ -142,16 +118,20 @@
       aria-labelledby="labelName"
     >
       <template is="dom-repeat" items="[[_items]]" as="value">
-        <gr-button
-          role="radio"
-          vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
-          has-tooltip=""
+        <gr-tooltip-content
+          has-tooltip
+          title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
           data-name$="[[label.name]]"
           data-value$="[[value]]"
-          title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
         >
-          [[value]]</gr-button
-        >
+          <gr-button
+            role="radio"
+            vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
+            voteChip
+          >
+            [[value]]
+          </gr-button>
+        </gr-tooltip-content>
       </template>
     </iron-selector>
     <template
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
index 3c9b7c7..34e959b 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
@@ -23,7 +23,7 @@
 suite('gr-label-row-score tests', () => {
   let element;
 
-  setup(done => {
+  setup(async () => {
     element = basicFixture.instantiate();
     element.labels = {
       'Code-Review': {
@@ -80,7 +80,7 @@
       value: '+1',
     };
 
-    flush(done);
+    await flush();
   });
 
   function checkAriaCheckedValid() {
@@ -97,14 +97,14 @@
     }
   }
 
-  test('label picker', () => {
+  test('label picker', async () => {
     const labelsChangedHandler = sinon.stub();
     element.addEventListener('labels-changed', labelsChangedHandler);
     assert.ok(element.$.labelSelector);
     MockInteractions.tap(element.shadowRoot
         .querySelector(
-            'gr-button[data-value="-1"]'));
-    flush();
+            'gr-tooltip-content[data-value="-1"] > gr-button'));
+    await flush();
     assert.strictEqual(element.selectedValue, '-1');
     assert.strictEqual(element.selectedItem
         .textContent.trim(), '-1');
@@ -160,26 +160,6 @@
     checkAriaCheckedValid();
   });
 
-  test('do not display tooltips on touch devices', () => {
-    const verifiedBtn = element.shadowRoot
-        .querySelector(
-            'iron-selector > gr-button[data-value="-1"]');
-
-    // On touch devices, tooltips should not be shown.
-    verifiedBtn._isTouchDevice = true;
-    verifiedBtn._handleShowTooltip();
-    assert.isNotOk(verifiedBtn._tooltip);
-    verifiedBtn._handleHideTooltip();
-    assert.isNotOk(verifiedBtn._tooltip);
-
-    // On other devices, tooltips should be shown.
-    verifiedBtn._isTouchDevice = false;
-    verifiedBtn._handleShowTooltip();
-    assert.isOk(verifiedBtn._tooltip);
-    verifiedBtn._handleHideTooltip();
-    assert.isNotOk(verifiedBtn._tooltip);
-  });
-
   test('_computeLabelValue', () => {
     assert.strictEqual(element._computeLabelValue(element.labels,
         element.permittedLabels,
@@ -209,7 +189,7 @@
         'Code-Review'), []);
   });
 
-  test('changes in label score are reflected in the DOM', () => {
+  test('changes in label score are reflected in the DOM', async () => {
     element.labels = {
       'Code-Review': {
         values: {
@@ -232,16 +212,17 @@
         default_value: 0,
       },
     };
+    await flush();
     const selector = element.$.labelSelector;
     element.set('label', {name: 'Verified', value: ' 0'});
-    flush();
+    await flush();
     assert.strictEqual(selector.selected, ' 0');
     assert.strictEqual(
         element.$.selectedValueLabel.textContent.trim(), 'No score');
     checkAriaCheckedValid();
   });
 
-  test('without permitted labels', () => {
+  test('without permitted labels', async () => {
     element.permittedLabels = {
       Verified: [
         '-1',
@@ -249,22 +230,22 @@
         '+1',
       ],
     };
-    flush();
+    await flush();
     assert.isOk(element.$.labelSelector);
     assert.isFalse(element.$.labelSelector.hidden);
 
     element.permittedLabels = {};
-    flush();
+    await flush();
     assert.isOk(element.$.labelSelector);
     assert.isTrue(element.$.labelSelector.hidden);
 
     element.permittedLabels = {Verified: []};
-    flush();
+    await flush();
     assert.isOk(element.$.labelSelector);
     assert.isTrue(element.$.labelSelector.hidden);
   });
 
-  test('asymmetrical labels', done => {
+  test('asymmetrical labels', async () => {
     element.permittedLabels = {
       'Code-Review': [
         '-2',
@@ -278,35 +259,32 @@
         '+1',
       ],
     };
-    flush(() => {
-      assert.strictEqual(element.$.labelSelector
-          .items.length, 2);
-      assert.strictEqual(
-          element.root.querySelectorAll('.placeholder').length,
-          3);
+    await flush();
+    assert.strictEqual(element.$.labelSelector
+        .items.length, 2);
+    assert.strictEqual(
+        element.root.querySelectorAll('.placeholder').length,
+        3);
 
-      element.permittedLabels = {
-        'Code-Review': [
-          ' 0',
-          '+1',
-        ],
-        'Verified': [
-          '-2',
-          '-1',
-          ' 0',
-          '+1',
-          '+2',
-        ],
-      };
-      flush(() => {
-        assert.strictEqual(element.$.labelSelector
-            .items.length, 5);
-        assert.strictEqual(
-            element.root.querySelectorAll('.placeholder').length,
-            0);
-        done();
-      });
-    });
+    element.permittedLabels = {
+      'Code-Review': [
+        ' 0',
+        '+1',
+      ],
+      'Verified': [
+        '-2',
+        '-1',
+        ' 0',
+        '+1',
+        '+2',
+      ],
+    };
+    await flush();
+    assert.strictEqual(element.$.labelSelector
+        .items.length, 5);
+    assert.strictEqual(
+        element.root.querySelectorAll('.placeholder').length,
+        0);
   });
 
   test('default_value', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index 4147cc0..a496be5 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -16,9 +16,8 @@
  */
 import '../gr-label-score-row/gr-label-score-row';
 import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-label-scores_html';
-import {customElement, property} from '@polymer/decorators';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {hasOwnProperty} from '../../../utils/common-util';
 import {
   LabelNameToValueMap,
@@ -33,21 +32,14 @@
   Label,
   LabelValuesMap,
 } from '../gr-label-score-row/gr-label-score-row';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {appContext} from '../../../services/app-context';
 import {labelCompare} from '../../../utils/label-util';
 import {Execution} from '../../../constants/reporting';
+import {ChangeStatus} from '../../../constants/constants';
 
 @customElement('gr-label-scores')
-export class GrLabelScores extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  @property({type: Array, computed: '_computeLabels(change.labels.*, account)'})
-  _labels: Label[] = [];
-
-  @property({type: Object, observer: '_computeColumns'})
+export class GrLabelScores extends LitElement {
+  @property({type: Object})
   permittedLabels?: LabelNameToValueMap;
 
   @property({type: Object})
@@ -56,11 +48,63 @@
   @property({type: Object})
   account?: AccountInfo;
 
-  @property({type: Object})
-  _labelValues?: LabelValuesMap;
-
   private readonly reporting = appContext.reportingService;
 
+  static override get styles() {
+    return [
+      css`
+        .scoresTable {
+          display: table;
+          width: 100%;
+        }
+        .mergedMessage,
+        .abandonedMessage {
+          font-style: italic;
+          text-align: center;
+          width: 100%;
+        }
+        gr-label-score-row:hover {
+          background-color: var(--hover-background-color);
+        }
+        gr-label-score-row {
+          display: table-row;
+        }
+        gr-label-score-row.no-access {
+          display: none;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    const labels = this._computeLabels();
+    const labelValues = this._computeColumns();
+    return html`<div class="scoresTable">
+        ${labels.map(
+          label => html`<gr-label-score-row
+            class="${this.computeLabelAccessClass(label.name)}"
+            .label="${label}"
+            .name="${label.name}"
+            .labels="${this.change?.labels}"
+            .permittedLabels="${this.permittedLabels}"
+            .labelValues="${labelValues}"
+          ></gr-label-score-row>`
+        )}
+      </div>
+      <div
+        class="mergedMessage"
+        ?hidden=${this.change?.status !== ChangeStatus.MERGED}
+      >
+        Because this change has been merged, votes may not be decreased.
+      </div>
+      <div
+        class="abandonedMessage"
+        ?hidden=${this.change?.status !== ChangeStatus.ABANDONED}
+      >
+        Because this change has been abandoned, you cannot vote.
+      </div>`;
+  }
+
   getLabelValues(includeDefaults = true): LabelNameToValuesMap {
     const labels: LabelNameToValuesMap = {};
     if (this.shadowRoot === null || !this.change) {
@@ -79,7 +123,7 @@
 
       if (selectedVal === undefined) continue;
 
-      const defValNum = this._getDefaultValue(this.change.labels, label);
+      const defValNum = this.getDefaultValue(label);
       if (includeDefaults || selectedVal !== defValNum) {
         labels[label] = selectedVal;
       }
@@ -87,7 +131,7 @@
     return labels;
   }
 
-  _getStringLabelValue(
+  private getStringLabelValue(
     labels: LabelNameToInfoMap,
     labelName: string,
     numberValue?: number
@@ -108,25 +152,26 @@
     return stringVal;
   }
 
-  _getDefaultValue(labels?: LabelNameToInfoMap, labelName?: string) {
+  private getDefaultValue(labelName?: string) {
+    const labels = this.change?.labels;
     if (!labelName || !labels?.[labelName]) return undefined;
     const labelInfo = labels[labelName] as DetailedLabelInfo;
     return labelInfo.default_value;
   }
 
-  _getVoteForAccount(
-    labels: LabelNameToInfoMap | undefined,
-    labelName: string,
-    account?: AccountInfo
-  ): string | null {
+  _getVoteForAccount(labelName: string): string | null {
+    const labels = this.change?.labels;
     if (!labels) return null;
     const votes = labels[labelName] as DetailedLabelInfo;
     if (votes.all && votes.all.length > 0) {
       for (let i = 0; i < votes.all.length; i++) {
-        // TODO(TS): Replace == with === and check code can assign string to _account_id instead of number
-        // eslint-disable-next-line eqeqeq
-        if (account && votes.all[i]._account_id == account._account_id) {
-          return this._getStringLabelValue(
+        if (
+          this.account &&
+          // TODO(TS): Replace == with === and check code can assign string to _account_id instead of number
+          // eslint-disable-next-line eqeqeq
+          votes.all[i]._account_id == this.account._account_id
+        ) {
+          return this.getStringLabelValue(
             labels,
             labelName,
             votes.all[i].value
@@ -137,32 +182,26 @@
     return null;
   }
 
-  _computeLabels(
-    labelRecord: PolymerDeepPropertyChange<
-      LabelNameToInfoMap,
-      LabelNameToInfoMap
-    >,
-    account?: AccountInfo
-  ): Label[] {
-    if (!account) return [];
-    if (!labelRecord?.base) return [];
-    const labelsObj = labelRecord.base;
+  _computeLabels(): Label[] {
+    if (!this.account) return [];
+    const labelsObj = this.change?.labels;
+    if (!labelsObj) return [];
     return Object.keys(labelsObj)
       .sort(labelCompare)
       .map(key => {
         return {
           name: key,
-          value: this._getVoteForAccount(labelsObj, key, this.account),
+          value: this._getVoteForAccount(key),
         };
       });
   }
 
-  _computeColumns(permittedLabels?: LabelNameToValueMap) {
-    if (!permittedLabels) return;
-    const labels = Object.keys(permittedLabels);
+  _computeColumns() {
+    if (!this.permittedLabels) return;
+    const labels = Object.keys(this.permittedLabels);
     const values: Set<number> = new Set();
     for (const label of labels) {
-      for (const value of permittedLabels[label]) {
+      for (const value of this.permittedLabels[label]) {
         values.add(Number(value));
       }
     }
@@ -173,23 +212,14 @@
     for (let i = 0; i < orderedValues.length; i++) {
       labelValues[orderedValues[i]] = i;
     }
-    this._labelValues = labelValues;
+    return labelValues;
   }
 
-  _changeIsMerged(changeStatus: string) {
-    return changeStatus === 'MERGED';
-  }
+  private computeLabelAccessClass(label?: string) {
+    if (!this.permittedLabels || !label) return '';
 
-  _computeLabelAccessClass(
-    label?: string,
-    permittedLabels?: LabelNameToValueMap
-  ) {
-    if (!permittedLabels || !label) {
-      return '';
-    }
-
-    return hasOwnProperty(permittedLabels, label) &&
-      permittedLabels[label].length
+    return hasOwnProperty(this.permittedLabels, label) &&
+      this.permittedLabels[label].length
       ? 'access'
       : 'no-access';
   }
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.ts
deleted file mode 100644
index 7b1fb7f..0000000
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    .scoresTable {
-      display: table;
-      width: 100%;
-    }
-    .mergedMessage {
-      font-style: italic;
-      text-align: center;
-      width: 100%;
-    }
-    gr-label-score-row:hover {
-      background-color: var(--hover-background-color);
-    }
-    gr-label-score-row {
-      display: table-row;
-    }
-    gr-label-score-row.no-access {
-      display: none;
-    }
-  </style>
-  <div class="scoresTable">
-    <template is="dom-repeat" items="[[_labels]]" as="label">
-      <gr-label-score-row
-        class$="[[_computeLabelAccessClass(label.name, permittedLabels)]]"
-        label="[[label]]"
-        name="[[label.name]]"
-        labels="[[change.labels]]"
-        permitted-labels="[[permittedLabels]]"
-        label-values="[[_labelValues]]"
-      ></gr-label-score-row>
-    </template>
-  </div>
-  <div class="mergedMessage" hidden$="[[!_changeIsMerged(change.status)]]">
-    Because this change has been merged, votes may not be decreased.
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
index 58fe189..f529464 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.ts
@@ -17,7 +17,7 @@
 
 import '../../../test/common-test-setup-karma';
 import './gr-label-scores';
-import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {isHidden, queryAndAssert, stubRestApi} from '../../../test/test-utils';
 import {GrLabelScores} from './gr-label-scores';
 import {AccountId} from '../../../types/common';
 import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
@@ -25,6 +25,7 @@
   createAccountWithId,
   createChange,
 } from '../../../test/test-data-generators';
+import {ChangeStatus} from '../../../constants/constants';
 
 const basicFixture = fixtureFromElement('gr-label-scores');
 
@@ -84,7 +85,7 @@
     await flush();
   });
 
-  test('get and set label scores', () => {
+  test('get and set label scores', async () => {
     for (const label of Object.keys(element.permittedLabels!)) {
       const row = queryAndAssert<GrLabelScoreRow>(
         element,
@@ -92,6 +93,7 @@
       );
       row.setSelectedValue('-1');
     }
+    await flush();
     assert.deepEqual(element.getLabelValues(), {
       'Code-Review': -1,
       Verified: -1,
@@ -116,19 +118,12 @@
 
   test('_getVoteForAccount', () => {
     const labelName = 'Code-Review';
-    assert.strictEqual(
-      element._getVoteForAccount(
-        element.change!.labels,
-        labelName,
-        element.account
-      ),
-      '+1'
-    );
+    assert.strictEqual(element._getVoteForAccount(labelName), '+1');
   });
 
   test('_computeColumns', () => {
-    element._computeColumns(element.permittedLabels);
-    assert.deepEqual(element._labelValues, {
+    const labelValues = element._computeColumns();
+    assert.deepEqual(labelValues, {
       '-2': 0,
       '-1': 1,
       '0': 2,
@@ -137,31 +132,8 @@
     });
   });
 
-  test('_computeLabelAccessClass undefined case', () => {
-    assert.strictEqual(
-      element._computeLabelAccessClass(undefined, undefined),
-      ''
-    );
-    assert.strictEqual(element._computeLabelAccessClass('', undefined), '');
-    assert.strictEqual(element._computeLabelAccessClass(undefined, {}), '');
-  });
-
-  test('_computeLabelAccessClass has access', () => {
-    assert.strictEqual(
-      element._computeLabelAccessClass('foo', {foo: ['']}),
-      'access'
-    );
-  });
-
-  test('_computeLabelAccessClass no access', () => {
-    assert.strictEqual(
-      element._computeLabelAccessClass('zap', {foo: ['']}),
-      'no-access'
-    );
-  });
-
-  test('changes in label score are reflected in _labels', () => {
-    element.change = {
+  test('changes in label score are reflected in _labels', async () => {
+    const change = {
       ...createChange(),
       labels: {
         'Code-Review': {
@@ -186,17 +158,62 @@
         },
       },
     };
-    assert.deepEqual(element._labels, [
+    element.change = change;
+    await flush();
+    let labels = element._computeLabels();
+    assert.deepEqual(labels, [
       {name: 'Code-Review', value: null},
       {name: 'Verified', value: null},
     ]);
-    element.set(
-      ['change', 'labels', 'Verified', 'all'],
-      [{_account_id: accountId, value: 1}]
-    );
-    assert.deepEqual(element._labels, [
+    element.change = {
+      ...change,
+      labels: {
+        ...change.labels,
+        Verified: {
+          ...change.labels.Verified,
+          all: [
+            {
+              _account_id: accountId,
+              value: 1,
+            },
+          ],
+        },
+      },
+    };
+    await flush();
+    labels = element._computeLabels();
+    assert.deepEqual(labels, [
       {name: 'Code-Review', value: null},
       {name: 'Verified', value: '+1'},
     ]);
   });
+  suite('message', () => {
+    test('shown when change is abandoned', async () => {
+      element.change = {
+        ...createChange(),
+        status: ChangeStatus.ABANDONED,
+      };
+      await flush();
+      assert.isFalse(isHidden(queryAndAssert(element, '.abandonedMessage')));
+      assert.isTrue(isHidden(queryAndAssert(element, '.mergedMessage')));
+    });
+    test('shown when change is merged', async () => {
+      element.change = {
+        ...createChange(),
+        status: ChangeStatus.MERGED,
+      };
+      await flush();
+      assert.isFalse(isHidden(queryAndAssert(element, '.mergedMessage')));
+      assert.isTrue(isHidden(queryAndAssert(element, '.abandonedMessage')));
+    });
+    test('do not show for new', async () => {
+      element.change = {
+        ...createChange(),
+        status: ChangeStatus.NEW,
+      };
+      await flush();
+      assert.isTrue(isHidden(queryAndAssert(element, '.mergedMessage')));
+      assert.isTrue(isHidden(queryAndAssert(element, '.abandonedMessage')));
+    });
+  });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 94295d0..95e4301 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -22,7 +22,6 @@
 import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-formatted-text/gr-formatted-text';
 import '../../../styles/shared-styles';
-import '../../../styles/gr-voting-styles';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-message_html';
 import {MessageTag, SpecialFilePath} from '../../../constants/constants';
@@ -181,7 +180,7 @@
   @property({
     type: String,
     computed:
-      '_computeMessageContentExpanded(message.message,' +
+      '_computeMessageContentExpanded(_expanded, message.message,' +
       ' message.accounts_in_message,' +
       ' message.tag)',
   })
@@ -210,7 +209,7 @@
     this.addEventListener('click', e => this._handleClick(e));
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.restApiService.getConfig().then(config => {
       this.config = config;
@@ -241,10 +240,12 @@
   }
 
   _computeMessageContentExpanded(
+    expanded: boolean,
     content?: string,
     accountsInMessage?: AccountInfo[],
     tag?: ReviewInputTag
   ) {
+    if (!expanded) return '';
     return this._computeMessageContent(true, content, accountsInMessage, tag);
   }
 
@@ -278,9 +279,11 @@
     tag?: ReviewInputTag,
     commentThreads?: CommentThread[]
   ) {
+    // Content is under text-overflow, so it's always shorten
+    const shortenedContent = content?.substring(0, 1000);
     const summary = this._computeMessageContent(
       false,
-      content,
+      shortenedContent,
       accountsInMessage,
       tag
     );
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index 9e24a09..c9680ef 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -17,9 +17,6 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
-  <style include="gr-voting-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
   <style>
     :host {
       display: block;
@@ -248,13 +245,13 @@
       <template is="dom-if" if="[[message.message]]">
         <div class="content messageContent">
           <div class="message hideOnOpen">[[_messageContentCollapsed]]</div>
-          <gr-formatted-text
-            noTrailingMargin
-            class="message hideOnCollapsed"
-            content="[[_messageContentExpanded]]"
-            config="[[_projectConfig.commentlinks]]"
-          ></gr-formatted-text>
           <template is="dom-if" if="[[_expanded]]">
+            <gr-formatted-text
+              noTrailingMargin
+              class="message hideOnCollapsed"
+              content="[[_messageContentExpanded]]"
+              config="[[_projectConfig.commentlinks]]"
+            ></gr-formatted-text>
             <template is="dom-if" if="[[_messageContentExpanded]]">
               <div
                 class="replyActionContainer"
@@ -284,8 +281,8 @@
             </template>
             <gr-thread-list
               change="[[change]]"
-              hidden$="[[!message.commentThreads.length]]"
-              threads="[[message.commentThreads]]"
+              hidden$="[[!commentThreads.length]]"
+              threads="[[commentThreads]]"
               change-num="[[changeNum]]"
               logged-in="[[_loggedIn]]"
               hide-dropdown
@@ -328,8 +325,8 @@
         <template is="dom-if" if="[[!message.id]]">
           <span class="date">
             <gr-date-formatter
-              has-tooltip=""
-              show-date-and-time=""
+              withTooltip
+              showDateAndTime
               date-str="[[message.date]]"
             ></gr-date-formatter>
           </span>
@@ -337,8 +334,8 @@
         <template is="dom-if" if="[[message.id]]">
           <span class="date" on-click="_handleAnchorClick">
             <gr-date-formatter
-              has-tooltip=""
-              show-date-and-time=""
+              withTooltip
+              showDateAndTime
               date-str="[[message.date]]"
             ></gr-date-formatter>
           </span>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index cae7a5a..f87c4c3 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -26,6 +26,7 @@
   createRevisions,
 } from '../../../test/test-data-generators';
 import {
+  mockPromise,
   query,
   queryAll,
   queryAndAssert,
@@ -50,7 +51,7 @@
 } from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {CommentSide} from '../../../constants/constants';
-import {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {SinonStubbedMember} from 'sinon';
 
 const basicFixture = fixtureFromElement('gr-message');
 
@@ -58,13 +59,13 @@
   let element: GrMessage;
 
   suite('when admin and logged in', () => {
-    setup(done => {
+    setup(async () => {
       stubRestApi('getIsAdmin').returns(Promise.resolve(true));
       element = basicFixture.instantiate();
-      flush(done);
+      await flush();
     });
 
-    test('reply event', done => {
+    test('reply event', async () => {
       element.message = {
         ...createChangeMessage(),
         id: '47c43261_55aa2c41' as ChangeMessageId,
@@ -79,18 +80,20 @@
         expanded: true,
       };
 
+      const promise = mockPromise();
       element.addEventListener('reply', (e: CustomEvent<ReplyEventDetail>) => {
         assert.deepEqual(e.detail.message, element.message);
-        done();
+        promise.resolve();
       });
-      flush();
+      await flush();
       assert.isFalse(
         queryAndAssert<HTMLElement>(element, '.replyActionContainer').hidden
       );
       tap(queryAndAssert(element, '.replyBtn'));
+      await promise;
     });
 
-    test('can see delete button', () => {
+    test('can see delete button', async () => {
       element.message = {
         ...createChangeMessage(),
         id: '47c43261_55aa2c41' as ChangeMessageId,
@@ -105,11 +108,11 @@
         expanded: true,
       };
 
-      flush();
+      await flush();
       assert.isFalse(queryAndAssert<HTMLElement>(element, '.deleteBtn').hidden);
     });
 
-    test('delete change message', done => {
+    test('delete change message', async () => {
       element.changeNum = 314159 as NumericChangeId;
       element.message = {
         ...createChangeMessage(),
@@ -125,6 +128,7 @@
         expanded: true,
       };
 
+      const promise = mockPromise();
       element.addEventListener(
         'change-message-deleted',
         (e: CustomEvent<ChangeMessageDeletedEventDetail>) => {
@@ -132,14 +136,15 @@
           assert.isFalse(
             (queryAndAssert(element, '.deleteBtn') as GrButton).disabled
           );
-          done();
+          promise.resolve();
         }
       );
-      flush();
+      await flush();
       tap(queryAndAssert(element, '.deleteBtn'));
       assert.isTrue(
         (queryAndAssert(element, '.deleteBtn') as GrButton).disabled
       );
+      await promise;
     });
 
     test('autogenerated prefix hiding', () => {
@@ -565,11 +570,11 @@
   });
 
   suite('when not logged in', () => {
-    setup(done => {
+    setup(async () => {
       stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       stubRestApi('getIsAdmin').returns(Promise.resolve(false));
       element = basicFixture.instantiate();
-      flush(done);
+      await flush();
     });
 
     test('reply and delete button should be hidden', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index fae624e..fdd7c79 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -90,27 +90,19 @@
  */
 function computeThreads(
   message: CombinedMessage,
-  changeComments?: ChangeComments
+  allThreadsForChange: CommentThread[]
 ): CommentThread[] {
-  if (message._index === undefined || changeComments === undefined) {
+  if (message._index === undefined) {
     return [];
   }
   const messageId = getMessageId(message);
-  return changeComments.getAllThreadsForChange().filter(thread =>
-    thread.comments
-      .map(comment => {
-        // collapse all by default
-        comment.collapsed = true;
-        return comment;
-      })
-      .some(comment => {
-        const condition = comment.change_message_id === messageId;
-        // Since getAllThreadsForChange() always returns a new copy of
-        // all comments we can modify them here without worrying about
-        // polluting other threads.
-        comment.collapsed = !condition;
-        return condition;
-      })
+  return allThreadsForChange.filter(thread =>
+    thread.comments.some(comment => {
+      const matchesMessage = comment.change_message_id === messageId;
+      if (!matchesMessage) return false;
+      comment.collapsed = !matchesMessage;
+      return matchesMessage;
+    })
   );
 }
 
@@ -198,7 +190,6 @@
 }
 
 export const TEST_ONLY = {
-  computeThreads,
   computeTag,
   computeRevision,
   computeIsImportant,
@@ -210,8 +201,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-messages-list')
-export class GrMessagesList extends KeyboardShortcutMixin(PolymerElement) {
+export class GrMessagesList extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -352,14 +346,24 @@
         mDate = null;
       }
     }
-    combinedMessages.forEach(m => {
-      if (m.expanded === undefined) {
-        m.expanded = false;
+
+    const allThreadsForChange = changeComments.getAllThreadsForChange();
+    // collapse all by default
+    for (const thread of allThreadsForChange) {
+      for (const comment of thread.comments) {
+        comment.collapsed = true;
       }
-      m.commentThreads = computeThreads(m, changeComments);
-      m._revision_number = computeRevision(m, combinedMessages);
-      m.tag = computeTag(m);
-    });
+    }
+
+    for (let i = 0; i < combinedMessages.length; i++) {
+      const message = combinedMessages[i];
+      if (message.expanded === undefined) {
+        message.expanded = false;
+      }
+      message.commentThreads = computeThreads(message, allThreadsForChange);
+      message._revision_number = computeRevision(message, combinedMessages);
+      message.tag = computeTag(message);
+    }
     // computeIsImportant() depends on tags and revision numbers already being
     // updated for all messages, so we have to compute this in its own forEach
     // loop.
@@ -369,10 +373,6 @@
     return combinedMessages;
   }
 
-  getCommentThreads(message: CombinedMessage, changeComments?: ChangeComments) {
-    return computeThreads(message, changeComments);
-  }
-
   _updateExpandedStateOfAllMessages(exp: boolean) {
     if (this._combinedMessages) {
       for (let i = 0; i < this._combinedMessages.length; i++) {
@@ -438,24 +438,26 @@
   }
 
   /**
-   * This method is for reporting stats only.
+   * Called when this._combinedMessages has changed.
    */
   _combinedMessagesChanged(combinedMessages?: CombinedMessage[]) {
-    if (combinedMessages) {
-      if (combinedMessages.length === 0) return;
-      const tags = combinedMessages.map(
-        message =>
-          message.tag || (message as FormattedReviewerUpdateInfo).type || 'none'
-      );
-      const tagsCounted = tags.reduce(
-        (acc, val) => {
-          acc[val] = (acc[val] || 0) + 1;
-          return acc;
-        },
-        {all: combinedMessages.length} as TagsCountReportInfo
-      );
-      this.reporting.reportInteraction('messages-count', tagsCounted);
+    if (!combinedMessages) return;
+    if (combinedMessages.length === 0) return;
+    for (let i = 0; i < combinedMessages.length; i++) {
+      this.notifyPath(`_combinedMessages.${i}.commentThreads`);
     }
+    const tags = combinedMessages.map(
+      message =>
+        message.tag || (message as FormattedReviewerUpdateInfo).type || 'none'
+    );
+    const tagsCounted = tags.reduce(
+      (acc, val) => {
+        acc[val] = (acc[val] || 0) + 1;
+        return acc;
+      },
+      {all: combinedMessages.length} as TagsCountReportInfo
+    );
+    this.reporting.reportInteraction('messages-count', tagsCounted);
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
index 93df77e..56fae87 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
@@ -93,7 +93,7 @@
       change="[[change]]"
       change-num="[[changeNum]]"
       message="[[message]]"
-      comment-threads="[[getCommentThreads(message, changeComments)]]"
+      comment-threads="[[message.commentThreads]]"
       project-name="[[projectName]]"
       show-reply-button="[[showReplyButtons]]"
       on-message-anchor-tap="_handleAnchorClick"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index b3730fd..d34600e 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement, property, css} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {
   ChangeInfo,
@@ -27,7 +26,7 @@
 import {isChangeInfo} from '../../../utils/change-util';
 
 @customElement('gr-related-change')
-export class GrRelatedChange extends GrLitElement {
+export class GrRelatedChange extends LitElement {
   @property()
   change?: ChangeInfo | RelatedChangeAndCommitInfo;
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index dd86ea1..74f20f2 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -14,14 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html, nothing} from 'lit-html';
 import './gr-related-change';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
-import {classMap} from 'lit-html/directives/class-map';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement, property, css, state, TemplateResult} from 'lit-element';
+import {classMap} from 'lit/directives/class-map';
+import {LitElement, css, html, nothing, TemplateResult} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {
   SubmittedTogetherInfo,
@@ -61,7 +60,7 @@
 }
 
 @customElement('gr-related-changes-list')
-export class GrRelatedChangesList extends GrLitElement {
+export class GrRelatedChangesList extends LitElement {
   @property()
   change?: ParsedChangeInfo;
 
@@ -666,7 +665,7 @@
 }
 
 @customElement('gr-related-collapse')
-export class GrRelatedCollapse extends GrLitElement {
+export class GrRelatedCollapse extends LitElement {
   @property()
   override title = '';
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
index 15bc6bf..a6dc338f 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {SinonStubbedMember} from 'sinon';
 import {PluginApi} from '../../../api/plugin';
 import {ChangeStatus} from '../../../constants/constants';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
@@ -74,9 +74,9 @@
         v: boolean;
       }>
     ) {
-      return instructions
-        .map(inst => Array.from({length: inst.len}, () => inst.v))
-        .reduce((acc, val) => acc.concat(val), []);
+      return instructions.flatMap(inst =>
+        Array.from({length: inst.len}, () => inst.v)
+      );
     }
 
     function checkShowWhenCollapsed(
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 933cb821..b8c9319 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -127,7 +127,7 @@
     const labelScoreRows = element.getLabelScores().shadowRoot
         .querySelector('gr-label-score-row[name="Code-Review"]');
     const selectedBtn = labelScoreRows.shadowRoot
-        .querySelector('gr-button[data-value="+1"].iron-selected');
+        .querySelector('gr-tooltip-content[data-value="+1"] > gr-button');
     assert.isOk(selectedBtn);
   });
 });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 89df2cb..17036e6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -163,8 +163,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-reply-dialog')
-export class GrReplyDialog extends KeyboardShortcutMixin(PolymerElement) {
+export class GrReplyDialog extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index b571985..1973fe6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -441,23 +441,26 @@
                 ></gr-account-label>
               </template>
             </template>
-            <gr-button
-              class="edit-attention-button"
-              on-click="_handleAttentionModify"
-              disabled="[[_sendDisabled]]"
-              link=""
-              position-below=""
-              data-label="Edit"
-              data-action-type="change"
-              data-action-key="edit"
-              has-tooltip=""
+            <gr-tooltip-content
+              has-tooltip
               title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
-              role="button"
-              tabindex="0"
             >
-              <iron-icon icon="gr-icons:edit"></iron-icon>
-              Modify
-            </gr-button>
+              <gr-button
+                class="edit-attention-button"
+                on-click="_handleAttentionModify"
+                disabled="[[_sendDisabled]]"
+                link=""
+                position-below=""
+                data-label="Edit"
+                data-action-type="change"
+                data-action-key="edit"
+                role="button"
+                tabindex="0"
+              >
+                <iron-icon icon="gr-icons:edit"></iron-icon>
+                Modify
+              </gr-button>
+            </gr-tooltip-content>
           </div>
           <div>
             <a
@@ -612,26 +615,32 @@
             <!-- Use 'Send' here as the change may only about reviewers / ccs
                 and when this button is visible, the next button will always
                 be 'Start review' -->
-            <gr-button
-              link=""
-              disabled="[[_isState(knownLatestState, 'not-latest')]]"
-              class="action save"
+            <gr-tooltip-content
               has-tooltip=""
-              title="[[_saveTooltip]]"
-              on-click="_saveClickHandler"
-              >Send As WIP</gr-button
+              title$="[[_saveTooltip]]"
             >
+              <gr-button
+                link=""
+                disabled="[[_isState(knownLatestState, 'not-latest')]]"
+                class="action save"
+                on-click="_saveClickHandler"
+                >Send As WIP</gr-button
+              >
+            </gr-tooltip-content>
           </template>
-          <gr-button
-            id="sendButton"
-            primary=""
-            disabled="[[_sendDisabled]]"
-            class="action send"
+          <gr-tooltip-content
             has-tooltip=""
             title$="[[_computeSendButtonTooltip(canBeStarted, _commentEditing)]]"
-            on-click="_sendTapHandler"
-            >[[_sendButtonLabel]]</gr-button
           >
+            <gr-button
+              id="sendButton"
+              primary=""
+              disabled="[[_sendDisabled]]"
+              class="action send"
+              on-click="_sendTapHandler"
+              >[[_sendButtonLabel]]
+            </gr-button>
+          </gr-tooltip-content>
         </div>
       </section>
     </div>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index d70c7ba..e57ffc7 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -116,7 +116,7 @@
     return {id: `${lastId++}` as GroupId};
   };
 
-  setup(() => {
+  setup(async () => {
     changeNum = 42 as NumericChangeId;
     patchNum = 1 as PatchSetNum;
 
@@ -168,7 +168,7 @@
     //     .returns(Promise.resolve({isLatest: true}));
 
     // Allow the elements created by dom-repeat to be stamped.
-    flush();
+    await flush();
   });
 
   function stubSaveReview(
@@ -216,6 +216,7 @@
     // which the dom-repeat elements are stamped.
     await flush();
     tap(queryAndAssert(element, '.send'));
+    await flush();
 
     const review = await saveReviewPromise;
     assert.deepEqual(review, {
@@ -1037,45 +1038,40 @@
     });
   });
 
-  test('getlabelValue returns value', done => {
-    flush(() => {
-      const el = queryAndAssert(
-        queryAndAssert(element, 'gr-label-scores'),
-        'gr-label-score-row[name="Verified"]'
-      ) as GrLabelScoreRow;
-      el.setSelectedValue('-1');
-      assert.equal('-1', element.getLabelValue('Verified'));
-      done();
-    });
+  test('getlabelValue returns value', async () => {
+    await flush();
+    const el = queryAndAssert(
+      queryAndAssert(element, 'gr-label-scores'),
+      'gr-label-score-row[name="Verified"]'
+    ) as GrLabelScoreRow;
+    el.setSelectedValue('-1');
+    assert.equal('-1', element.getLabelValue('Verified'));
   });
 
-  test('getlabelValue when no score is selected', done => {
-    flush(() => {
-      const el = queryAndAssert(
-        queryAndAssert(element, 'gr-label-scores'),
-        'gr-label-score-row[name="Code-Review"]'
-      ) as GrLabelScoreRow;
-      el.setSelectedValue('-1');
-      assert.strictEqual(element.getLabelValue('Verified'), ' 0');
-      done();
-    });
+  test('getlabelValue when no score is selected', async () => {
+    await flush();
+    const el = queryAndAssert(
+      queryAndAssert(element, 'gr-label-scores'),
+      'gr-label-score-row[name="Code-Review"]'
+    ) as GrLabelScoreRow;
+    el.setSelectedValue('-1');
+    assert.strictEqual(element.getLabelValue('Verified'), ' 0');
   });
 
-  test('setlabelValue', done => {
+  test('setlabelValue', async () => {
     element._account = {_account_id: 1 as AccountId};
-    flush(() => {
-      const label = 'Verified';
-      const value = '+1';
-      element.setLabelValue(label, value);
+    await flush();
+    const label = 'Verified';
+    const value = '+1';
+    element.setLabelValue(label, value);
+    await flush();
 
-      const labels = (
-        queryAndAssert(element, '#labelScores') as GrLabelScores
-      ).getLabelValues();
-      assert.deepEqual(labels, {
-        'Code-Review': 0,
-        Verified: 1,
-      });
-      done();
+    const labels = (
+      queryAndAssert(element, '#labelScores') as GrLabelScores
+    ).getLabelValues();
+    assert.deepEqual(labels, {
+      'Code-Review': 0,
+      Verified: 1,
     });
   });
 
@@ -1347,7 +1343,7 @@
     assert.isTrue(eraseDraftCommentStub.calledWith(location));
   });
 
-  test('400 converts to human-readable server-error', done => {
+  test('400 converts to human-readable server-error', async () => {
     stubRestApi('saveChangeReview').callsFake(
       (_changeNum, _patchNum, _review, errFn) => {
         errFn!(
@@ -1360,34 +1356,35 @@
       }
     );
 
+    const promise = mockPromise();
     const listener = (event: Event) => {
       if (event.target !== document) return;
       (event as CustomEvent).detail.response.text().then((body: string) => {
         if (body === 'human readable') {
-          done();
+          promise.resolve();
         }
       });
     };
     addListenerForTest(document, 'server-error', listener);
 
-    flush(() => {
-      element.send(false, false);
-    });
+    await flush();
+    element.send(false, false);
+    await promise;
   });
 
-  test('non-json 400 is treated as a normal server-error', done => {
+  test('non-json 400 is treated as a normal server-error', async () => {
     stubRestApi('saveChangeReview').callsFake(
       (_changeNum, _patchNum, _review, errFn) => {
         errFn!(cloneableResponse(400, 'Comment validation error!') as Response);
         return Promise.resolve(new Response());
       }
     );
-
+    const promise = mockPromise();
     const listener = (event: Event) => {
       if (event.target !== document) return;
       (event as CustomEvent).detail.response.text().then((body: string) => {
         if (body === 'Comment validation error!') {
-          done();
+          promise.resolve();
         }
       });
     };
@@ -1395,9 +1392,9 @@
 
     // Async tick is needed because iron-selector content is distributed and
     // distributed content requires an observer to be set up.
-    flush(() => {
-      element.send(false, false);
-    });
+    await flush();
+    element.send(false, false);
+    await promise;
   });
 
   test('filterReviewerSuggestion', () => {
@@ -1494,29 +1491,30 @@
     assert.strictEqual(element._chooseFocusTarget(), element.FocusTarget.BODY);
   });
 
-  test('only send labels that have changed', done => {
-    flush(() => {
-      stubSaveReview((review: ReviewInput) => {
-        assert.deepEqual(review?.labels, {
-          'Code-Review': 0,
-          Verified: -1,
-        });
+  test('only send labels that have changed', async () => {
+    await flush();
+    stubSaveReview((review: ReviewInput) => {
+      assert.deepEqual(review?.labels, {
+        'Code-Review': 0,
+        Verified: -1,
       });
-
-      element.addEventListener('send', () => {
-        done();
-      });
-      // Without wrapping this test in flush(), the below two calls to
-      // tap() cause a race in some situations in shadow DOM.
-      // The send button can be tapped before the others, causing the test to
-      // fail.
-      const el = queryAndAssert(
-        queryAndAssert(element, 'gr-label-scores'),
-        'gr-label-score-row[name="Verified"]'
-      ) as GrLabelScoreRow;
-      el.setSelectedValue('-1');
-      tap(queryAndAssert(element, '.send'));
     });
+
+    const promise = mockPromise();
+    element.addEventListener('send', () => {
+      promise.resolve();
+    });
+    // Without wrapping this test in flush(), the below two calls to
+    // tap() cause a race in some situations in shadow DOM.
+    // The send button can be tapped before the others, causing the test to
+    // fail.
+    const el = queryAndAssert(
+      queryAndAssert(element, 'gr-label-scores'),
+      'gr-label-score-row[name="Verified"]'
+    ) as GrLabelScoreRow;
+    el.setSelectedValue('-1');
+    tap(queryAndAssert(element, '.send'));
+    await promise;
   });
 
   test('moving from cc to reviewer', () => {
@@ -1811,14 +1809,14 @@
     stubSaveReview(() => undefined);
     element.addEventListener('send', () => assert.fail('wrongly called'));
     pressAndReleaseKeyOn(element, 13, null, 'enter');
-    flush();
   });
 
-  test('emit send on ctrl+enter key', done => {
+  test('emit send on ctrl+enter key', async () => {
     stubSaveReview(() => undefined);
-    element.addEventListener('send', () => done());
+    const promise = mockPromise();
+    element.addEventListener('send', () => promise.resolve());
     pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter');
-    flush();
+    await promise;
   });
 
   test('_computeMessagePlaceholder', () => {
@@ -1840,7 +1838,7 @@
     );
   });
 
-  test('_handle400Error reviewers and CCs', done => {
+  test('_handle400Error reviewers and CCs', async () => {
     const error1 = 'error 1';
     const error2 = 'error 2';
     const error3 = 'error 3';
@@ -1862,49 +1860,48 @@
           },
         },
       });
+    const promise = mockPromise();
     const listener = (e: Event) => {
       (e as CustomEvent).detail.response.text().then((text: string) => {
         assert.equal(text, [error1, error2, error3].join(', '));
-        done();
+        promise.resolve();
       });
     };
     addListenerForTest(document, 'server-error', listener);
     element._handle400Error(cloneableResponse(400, text) as Response);
+    await promise;
   });
 
-  test('fires height change when the drafts comments load', done => {
+  test('fires height change when the drafts comments load', async () => {
     // Flush DOM operations before binding to the autogrow event so we don't
     // catch the events fired from the initial layout.
-    flush(() => {
-      const autoGrowHandler = sinon.stub();
-      element.addEventListener('autogrow', autoGrowHandler);
-      element.draftCommentThreads = [];
-      flush(() => {
-        assert.isTrue(autoGrowHandler.called);
-        done();
-      });
-    });
+    await flush();
+    const autoGrowHandler = sinon.stub();
+    element.addEventListener('autogrow', autoGrowHandler);
+    element.draftCommentThreads = [];
+    await flush();
+    assert.isTrue(autoGrowHandler.called);
   });
 
   suite('start review and save buttons', () => {
     let sendStub: sinon.SinonStub;
 
-    setup(() => {
+    setup(async () => {
       sendStub = sinon.stub(element, 'send').callsFake(() => Promise.resolve());
       element.canBeStarted = true;
       // Flush to make both Start/Save buttons appear in DOM.
-      flush();
+      await flush();
     });
 
-    test('start review sets ready', () => {
+    test('start review sets ready', async () => {
       tap(queryAndAssert(element, '.send'));
-      flush();
+      await flush();
       assert.isTrue(sendStub.calledWith(true, true));
     });
 
-    test("save review doesn't set ready", () => {
+    test("save review doesn't set ready", async () => {
       tap(queryAndAssert(element, '.save'));
-      flush();
+      await flush();
       assert.isTrue(sendStub.calledWith(true, false));
     });
   });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index c579a59..b081be7 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -57,6 +57,10 @@
       vertical-align: top;
       display: inline-block;
     }
+    gr-vote-chip {
+      --gr-vote-chip-width: 14px;
+      --gr-vote-chip-height: 14px;
+    }
   </style>
   <div class="container">
     <div>
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
new file mode 100644
index 0000000..2e00034
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -0,0 +1,106 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../styles/gr-font-styles';
+import '../../shared/gr-hovercard/gr-hovercard-shared-style';
+import '../../shared/gr-button/gr-button';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {customElement, property} from '@polymer/decorators';
+import {HovercardBehaviorMixin} from '../../shared/gr-hovercard/gr-hovercard-behavior';
+import {htmlTemplate} from './gr-submit-requirement-hovercard_html';
+import {
+  AccountInfo,
+  SubmitRequirementExpressionInfo,
+  SubmitRequirementResultInfo,
+  SubmitRequirementStatus,
+} from '../../../api/rest-api';
+import {
+  extractAssociatedLabels,
+  iconForStatus,
+} from '../../../utils/label-util';
+import {ParsedChangeInfo} from '../../../types/types';
+import {Label} from '../gr-change-requirements/gr-change-requirements';
+
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardBehaviorMixin(PolymerElement);
+
+@customElement('gr-submit-requirement-hovercard')
+export class GrHovercardRun extends base {
+  static get template() {
+    return htmlTemplate;
+  }
+
+  @property({type: Object})
+  requirement?: SubmitRequirementResultInfo;
+
+  @property({type: Object})
+  change?: ParsedChangeInfo;
+
+  @property({type: Object})
+  account?: AccountInfo;
+
+  @property({type: Boolean})
+  mutable = false;
+
+  @property({type: Boolean})
+  expanded = false;
+
+  @property({type: Array, computed: 'computeLabels(change, requirement)'})
+  _labels: Label[] = [];
+
+  computeLabels(
+    change?: ParsedChangeInfo,
+    requirement?: SubmitRequirementResultInfo
+  ) {
+    if (!requirement) return [];
+    const requirementLabels = extractAssociatedLabels(requirement);
+    const labels = change?.labels ?? {};
+
+    const allLabels: Label[] = [];
+
+    for (const label of Object.keys(labels)) {
+      if (requirementLabels.includes(label)) {
+        allLabels.push({
+          labelName: label,
+          icon: '',
+          style: '',
+          labelInfo: labels[label],
+        });
+      }
+    }
+    return allLabels;
+  }
+
+  computeIcon(status: SubmitRequirementStatus) {
+    return iconForStatus(status);
+  }
+
+  renderCondition(expression?: SubmitRequirementExpressionInfo) {
+    if (!expression) return '';
+
+    return expression.expression;
+  }
+
+  _handleShowConditions() {
+    this.expanded = true;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-submit-requirement-hovercard': GrHovercardRun;
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
new file mode 100644
index 0000000..192a812
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
@@ -0,0 +1,189 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+
+export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
+  <style include="gr-hovercard-shared-style">
+    #container {
+      min-width: 356px;
+      max-width: 356px;
+      padding: var(--spacing-xl) 0 var(--spacing-m) 0;
+    }
+    section.label {
+      display: table-row;
+    }
+    .label-title {
+      min-width: 10em;
+      padding-top: var(--spacing-s);
+    }
+    .label-value {
+      padding-top: var(--spacing-s);
+    }
+    .label-title,
+    .label-value {
+      display: table-cell;
+      vertical-align: top;
+    }
+    .row {
+      display: flex;
+    }
+    .title {
+      color: var(--deemphasized-text-color);
+      margin-right: var(--spacing-m);
+    }
+    div.section {
+      margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
+      display: flex;
+    }
+    div.sectionIcon {
+      flex: 0 0 30px;
+    }
+    div.sectionIcon iron-icon {
+      position: relative;
+      top: 2px;
+      width: 20px;
+      height: 20px;
+    }
+    .condition {
+      background-color: var(--gray-background);
+      padding: var(--spacing-m);
+      flex-grow: 1;
+    }
+    .expression {
+      color: var(--gray-foreground);
+    }
+    iron-icon.check {
+      color: var(--success-foreground);
+    }
+    iron-icon.close {
+      color: var(--warning-foreground);
+    }
+    .showConditions iron-icon {
+      color: inherit;
+    }
+    div.showConditions {
+      border-top: 1px solid var(--border-color);
+      margin-top: var(--spacing-m);
+      padding: var(--spacing-m) var(--spacing-xl) 0;
+    }
+  </style>
+  <div id="container" role="tooltip" tabindex="-1">
+    <div class="section">
+      <div class="sectionIcon">
+        <iron-icon
+          class$="[[computeIcon(requirement.status)]]"
+          icon="gr-icons:[[computeIcon(requirement.status)]]"
+        ></iron-icon>
+      </div>
+      <div class="sectionContent">
+        <h3 class="name heading-3">
+          <span>[[requirement.name]]</span>
+        </h3>
+      </div>
+    </div>
+    <div class="section">
+      <div class="sectionIcon">
+        <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
+      </div>
+      <div class="sectionContent">
+        <div class="row">
+          <div class="title">Status</div>
+          <div>[[requirement.status]]</div>
+        </div>
+      </div>
+    </div>
+    <div class="section">
+      <template is="dom-repeat" items="[[_labels]]">
+        <section class="label">
+          <div class="label-title">
+            <gr-limited-text
+              class="name"
+              limit="25"
+              text="[[item.labelName]]"
+            ></gr-limited-text>
+          </div>
+          <div class="label-value">
+            <gr-label-info
+              change="{{change}}"
+              account="[[account]]"
+              mutable="[[mutable]]"
+              label="[[item.labelName]]"
+              label-info="[[item.labelInfo]]"
+            ></gr-label-info>
+          </div>
+        </section>
+      </template>
+    </div>
+    <template is="dom-if" if="[[!expanded]]">
+      <div class="showConditions">
+        <gr-button
+          link=""
+          class="showConditions"
+          on-click="_handleShowConditions"
+        >
+          View condition
+          <iron-icon icon="gr-icons:expand-more"></iron-icon
+        ></gr-button>
+      </div>
+    </template>
+    <template is="dom-if" if="[[expanded]]">
+      <div class="section">
+        <div class="sectionIcon">
+          <iron-icon icon="gr-icons:description"></iron-icon>
+        </div>
+        <div class="sectionContent">[[requirement.description]]</div>
+      </div>
+      <div class="section">
+        <div class="sectionIcon"></div>
+        <div class="sectionContent condition">
+          Blocking condition:<br />
+          <span class="expression">
+            [[renderCondition(requirement.submittability_expression_result)]]
+          </span>
+        </div>
+      </div>
+      <template
+        is="dom-if"
+        if="[[requirement.applicability_expression_result]]"
+      >
+        <div class="section">
+          <div class="sectionIcon"></div>
+          <div class="sectionContent condition">
+            Application condition:<br />
+            <span class="expression">
+              [[renderCondition(requirement.applicability_expression_result)]]
+            </span>
+          </div>
+        </div>
+      </template>
+      <template is="dom-if" if="[[requirement.override_expression_result]]">
+        <div class="section">
+          <div class="sectionIcon"></div>
+          <div class="sectionContent condition">
+            Override condition:<br />
+            <span class="expression">
+              [[renderCondition(requirement.override_expression_result)]]
+            </span>
+          </div>
+        </div>
+      </template>
+    </template>
+  </div>
+`;
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index 47f6e85..fd85117 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -14,20 +14,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
 import {ParsedChangeInfo} from '../../../types/types';
 import {
   AccountInfo,
+  isDetailedLabelInfo,
+  LabelNameToInfoMap,
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
 } from '../../../api/rest-api';
-import {assertNever} from '../../../utils/common-util';
-import {extractAssociatedLabels} from '../../../utils/change-metadata-util';
-import {Label} from '../gr-change-requirements/gr-change-requirements';
+import {unique} from '../../../utils/common-util';
+import {
+  extractAssociatedLabels,
+  hasVotes,
+  iconForStatus,
+} from '../../../utils/label-util';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {charsOnly, pluralize} from '../../../utils/string-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {
+  allRunsLatestPatchsetLatestAttempt$,
+  CheckRun,
+} from '../../../services/checks/checks-model';
+import {getResultsOf, hasResultsOf} from '../../../services/checks/checks-util';
+import {Category} from '../../../api/checks';
 
 @customElement('gr-submit-requirements')
-export class GrSubmitRequirements extends GrLitElement {
+export class GrSubmitRequirements extends LitElement {
   @property({type: Object})
   change?: ParsedChangeInfo;
 
@@ -37,48 +52,29 @@
   @property({type: Boolean})
   mutable?: boolean;
 
+  @state()
+  runs: CheckRun[] = [];
+
   static override get styles() {
     return [
+      fontStyles,
       css`
-        :host {
-          display: table;
-          width: 100%;
-        }
         .metadata-title {
-          font-size: 100%;
           font-weight: var(--font-weight-bold);
           color: var(--deemphasized-text-color);
           padding-left: var(--metadata-horizontal-padding);
-        }
-        section {
-          display: table-row;
-        }
-        .title {
-          min-width: 10em;
-          padding: var(--spacing-s) 0 0 0;
-        }
-        .value {
-          padding: var(--spacing-s) 0 0 0;
-        }
-        .title,
-        .value,
-        .status {
-          display: table-cell;
-          vertical-align: top;
-        }
-        .status {
-          width: var(--line-height-small);
-          padding: var(--spacing-s) var(--spacing-m) 0
-            var(--requirements-horizontal-padding);
+          margin: 0 0 var(--spacing-s);
+          border-top: 1px solid var(--border-color);
+          padding-top: var(--spacing-s);
         }
         iron-icon {
-          width: var(--line-height-small);
-          height: var(--line-height-small);
+          width: var(--line-height-normal, 20px);
+          height: var(--line-height-normal, 20px);
         }
-        iron-icon.satisfied {
+        iron-icon.check {
           color: var(--success-foreground);
         }
-        iron-icon.unsatisfied {
+        iron-icon.close {
           color: var(--warning-foreground);
         }
         .testing {
@@ -95,79 +91,189 @@
         .testing:hover * {
           visibility: visible;
         }
+        .requirements,
+        section.votes {
+          margin-left: var(--spacing-l);
+        }
+        gr-limited-text.name {
+          font-weight: var(--font-weight-bold);
+        }
+        table {
+          border-collapse: collapse;
+          border-spacing: 0;
+        }
+        td {
+          padding: var(--spacing-s);
+        }
+        .votes-cell {
+          display: flex;
+        }
+        .check-error {
+          margin-right: var(--spacing-l);
+        }
+        .check-error iron-icon {
+          color: var(--error-foreground);
+          vertical-align: top;
+        }
       `,
     ];
   }
 
+  constructor() {
+    super();
+    subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
+  }
+
   override render() {
     const submit_requirements = (this.change?.submit_requirements ?? []).filter(
       req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
     );
-    return html`<h3 class="metadata-title">Submit Requirements</h3>
+    return html` <h2
+        class="metadata-title heading-3"
+        id="submit-requirements-caption"
+      >
+        Submit Requirements
+      </h2>
+      <table class="requirements" aria-labelledby="submit-requirements-caption">
+        <thead hidden>
+          <tr>
+            <th>Status</th>
+            <th>Name</th>
+            <th>Votes</th>
+          </tr>
+        </thead>
+        <tbody>
+          ${submit_requirements.map(
+            requirement => html`<tr
+              id="requirement-${charsOnly(requirement.name)}"
+            >
+              <td>${this.renderStatus(requirement.status)}</td>
+              <td class="name">
+                <gr-limited-text
+                  class="name"
+                  limit="25"
+                  .text="${requirement.name}"
+                ></gr-limited-text>
+              </td>
+              <td>
+                <div class="votes-cell">
+                  ${this.renderVotes(requirement)}
+                  ${this.renderChecks(requirement)}
+                </div>
+              </td>
+            </tr>`
+          )}
+        </tbody>
+      </table>
       ${submit_requirements.map(
-        requirement => html`<section>
-          <div class="status">${this.renderStatus(requirement.status)}</div>
-          <div class="title">
-            <gr-limited-text
-              class="name"
-              limit="25"
-              text="${requirement.name}"
-            ></gr-limited-text>
-          </div>
-          <div class="value">${this.renderLabels(requirement)}</div>
-        </section>`
+        requirement => html`
+          <gr-submit-requirement-hovercard
+            for="requirement-${charsOnly(requirement.name)}"
+            .requirement="${requirement}"
+            .change="${this.change}"
+            .account="${this.account}"
+            .mutable="${this.mutable}"
+          ></gr-submit-requirement-hovercard>
+        `
+      )}
+      ${this.renderTriggerVotes(
+        submit_requirements
       )}${this.renderFakeControls()}`;
   }
 
   renderStatus(status: SubmitRequirementStatus) {
-    let grIcon: string;
-    switch (status) {
-      case SubmitRequirementStatus.SATISFIED:
-        grIcon = 'gr-icons:check';
-        break;
-      case SubmitRequirementStatus.UNSATISFIED:
-        grIcon = 'gr-icons:close';
-        break;
-      case SubmitRequirementStatus.OVERRIDDEN:
-        grIcon = 'gr-icons:warning';
-        break;
-      case SubmitRequirementStatus.NOT_APPLICABLE:
-        grIcon = 'gr-icons:info';
-        break;
-      default:
-        assertNever(status, `Unsupported status: ${status}`);
-    }
+    const icon = iconForStatus(status);
     return html`<iron-icon
-      class=${status.toLowerCase()}
-      icon="${grIcon}"
+      class="${icon}"
+      icon="gr-icons:${icon}"
+      role="img"
+      aria-label="${status.toLowerCase()}"
     ></iron-icon>`;
   }
 
-  renderLabels(requirement: SubmitRequirementResultInfo) {
+  renderVotes(requirement: SubmitRequirementResultInfo) {
     const requirementLabels = extractAssociatedLabels(requirement);
-    const labels = this.change?.labels ?? {};
-
-    const allLabels: Label[] = [];
-
-    for (const label of Object.keys(labels)) {
-      if (requirementLabels.includes(label)) {
-        allLabels.push({
-          labelName: label,
-          icon: '',
-          style: '',
-          labelInfo: labels[label],
-        });
-      }
-    }
-    return allLabels.map(
-      label => html`<gr-label-info
-        .change="${this.change}"
-        .account="${this.account}"
-        .mutable="${this.mutable}"
-        label="${label.labelName}"
-        .labelInfo="${label.labelInfo}"
-      ></gr-label-info>`
+    const allLabels = this.change?.labels ?? {};
+    const associatedLabels = Object.keys(allLabels).filter(label =>
+      requirementLabels.includes(label)
     );
+
+    const everyAssociatedLabelsIsWithoutVotes = associatedLabels.every(
+      label => !hasVotes(allLabels[label])
+    );
+    if (everyAssociatedLabelsIsWithoutVotes) return html`No votes`;
+
+    return associatedLabels.map(label =>
+      this.renderLabelVote(label, allLabels)
+    );
+  }
+
+  renderLabelVote(label: string, labels: LabelNameToInfoMap) {
+    const labelInfo = labels[label];
+    if (!isDetailedLabelInfo(labelInfo)) return;
+    const uniqueApprovals = (labelInfo.all ?? [])
+      .filter(
+        (approvalInfo, index, array) =>
+          index === array.findIndex(other => other.value === approvalInfo.value)
+      )
+      .sort((a, b) => -(a.value ?? 0) + (b.value ?? 0));
+    return uniqueApprovals.map(
+      approvalInfo =>
+        html`<gr-vote-chip
+          .vote="${approvalInfo}"
+          .label="${labelInfo}"
+          .more="${(labelInfo.all ?? []).filter(
+            other => other.value === approvalInfo.value
+          ).length > 1}"
+        ></gr-vote-chip>`
+    );
+  }
+
+  renderChecks(requirement: SubmitRequirementResultInfo) {
+    const requirementLabels = extractAssociatedLabels(requirement);
+    const requirementRuns = this.runs
+      .filter(run => hasResultsOf(run, Category.ERROR))
+      .filter(
+        run => run.labelName && requirementLabels.includes(run.labelName)
+      );
+    const runsCount = requirementRuns.reduce(
+      (sum, run) => sum + getResultsOf(run, Category.ERROR).length,
+      0
+    );
+    if (runsCount > 0) {
+      return html`<span class="check-error"
+        ><iron-icon icon="gr-icons:error"></iron-icon>${pluralize(
+          runsCount,
+          'error'
+        )}</span
+      >`;
+    }
+    return;
+  }
+
+  renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
+    const labels = this.change?.labels ?? {};
+    const allLabels = Object.keys(labels);
+    const labelAssociatedWithSubmitReqs = submitReqs
+      .flatMap(req => extractAssociatedLabels(req))
+      .filter(unique);
+    const triggerVotes = allLabels
+      .filter(label => !labelAssociatedWithSubmitReqs.includes(label))
+      .filter(label => hasVotes(labels[label]));
+    if (!triggerVotes.length) return;
+    return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
+      <section class="votes">
+        ${triggerVotes.map(
+          label => html`${label}:
+            <gr-label-info
+              .change="${this.change}"
+              .account="${this.account}"
+              .mutable="${this.mutable}"
+              label="${label}"
+              .labelInfo="${labels[label]}"
+            ></gr-label-info>`
+        )}
+      </section>`;
   }
 
   renderFakeControls() {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index daf2643..6ed5a2c 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
@@ -102,9 +102,9 @@
         items="[[getCommentsDropdownEntires(threads, loggedIn)]]"
       >
       </gr-dropdown-list>
-      <template is="dom-if" if="[[threads.length]]">
+      <template is="dom-if" if="[[_displayedThreads.length]]">
         <span class="author-text">From:</span>
-        <template is="dom-repeat" items="[[getCommentAuthors(threads, account)]]">
+        <template is="dom-repeat" items="[[getCommentAuthors(_displayedThreads, account)]]">
           <gr-account-label
             account="[[item]]"
             on-click="handleAccountClicked"
@@ -115,7 +115,7 @@
       </template>
     </div>
   </template>
-  <div id="threads">
+  <div id="threads" part="threads">
     <template
       is="dom-if"
       if="[[_showEmptyThreadsMessage(threads, _displayedThreads, unresolvedOnly)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
index fe99918..aab5cee 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
@@ -37,7 +37,7 @@
         .filter(e => e.style.display !== 'none');
   }
 
-  setup(done => {
+  setup(async () => {
     element = basicFixture.instantiate();
     element.changeNum = 123;
     element.change = {
@@ -267,9 +267,7 @@
     ];
 
     // use flush to render all (bypass initial-count set on dom-repeat)
-    flush(() => {
-      done();
-    });
+    await flush();
   });
 
   test('draft dropdown item only appears when logged in', () => {
@@ -289,14 +287,13 @@
     assert.equal(getVisibleThreads().length, element.threads.length);
   });
 
-  test('show unresolved threads if unresolvedOnly is set', done => {
+  test('show unresolved threads if unresolvedOnly is set', async () => {
     element.unresolvedOnly = true;
-    flush();
+    await flush();
     const unresolvedThreads = element.threads.filter(t => t.comments.some(
         c => c.unresolved
     ));
     assert.equal(getVisibleThreads().length, unresolvedThreads.length);
-    done();
   });
 
   test('showing file name takes visible threads into account', () => {
@@ -498,15 +495,18 @@
   test('tapping single author chips', () => {
     element.account = createAccountDetailWithId(1);
     flush();
-    const chips = queryAll(element, 'gr-account-label');
-    const authors = Array.from(chips).map(
+    const chips = Array.from(queryAll(element, 'gr-account-label'));
+    const authors = chips.map(
         chip => accountOrGroupKey(chip.account))
         .sort();
     assert.deepEqual(authors, [1, 1000000, 1000001, 1000002, 1000003]);
     assert.equal(element.threads.length, 9);
     assert.equal(element._displayedThreads.length, 9);
 
-    tap(chips[0]); // accountId 1000001
+    // accountId 1000001
+    const chip = chips.find(chip => chip.account._account_id === 1000001);
+
+    tap(chip);
     flush();
 
     assert.equal(element.threads.length, 9);
@@ -514,7 +514,7 @@
     assert.equal(element._displayedThreads[0].comments[0].author._account_id,
         1000001);
 
-    tap(chips[0]); // tapping again resets
+    tap(chip); // tapping again resets
     flush();
     assert.equal(element.threads.length, 9);
     assert.equal(element._displayedThreads.length, 9);
@@ -523,10 +523,10 @@
   test('tapping multiple author chips', () => {
     element.account = createAccountDetailWithId(1);
     flush();
-    const chips = queryAll(element, 'gr-account-label');
+    const chips = Array.from(queryAll(element, 'gr-account-label'));
 
-    tap(chips[0]); // accountId 1000001
-    tap(chips[2]); // accountId 1000002
+    tap(chips.find(chip => chip.account._account_id === 1000001));
+    tap(chips.find(chip => chip.account._account_id === 1000002));
     flush();
 
     assert.equal(element.threads.length, 9);
@@ -646,11 +646,9 @@
   });
 
   suite('hideDropdown', () => {
-    setup(done => {
+    setup(async () => {
       element.hideDropdown = true;
-      flush(() => {
-        done();
-      });
+      await flush();
     });
 
     test('toggle buttons are hidden', () => {
@@ -660,11 +658,9 @@
   });
 
   suite('empty thread', () => {
-    setup(done => {
+    setup(async () => {
       element.threads = [];
-      flush(() => {
-        done();
-      });
+      await flush();
     });
 
     test('default empty message should show', () => {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index 4633c0e..aed07e0 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -14,20 +14,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {css, customElement, property} from 'lit-element';
-import {GrLitElement} from '../lit/gr-lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {Action} from '../../api/checks';
 import {checkRequiredProperty} from '../../utils/common-util';
-import {fireActionTriggered} from '../../services/checks/checks-util';
+import {appContext} from '../../services/app-context';
 
 @customElement('gr-checks-action')
-export class GrChecksAction extends GrLitElement {
+export class GrChecksAction extends LitElement {
   @property()
   action!: Action;
 
   @property()
-  eventTarget?: EventTarget;
+  eventTarget: HTMLElement | null = null;
+
+  private checksService = appContext.checksService;
 
   override connectedCallback() {
     super.connectedCallback();
@@ -42,20 +43,13 @@
           white-space: nowrap;
         }
         gr-button {
-          /* It is not fully understood why this is needed, but otherwise the
-             paper-tooltip may render under some iron-icons of the content
-             below. Maybe this has to do with a z-index:0 setting for
-             paper-button, such that a stacking context is created. And the high
-             z-index of the paper-tooltip will then only be interpreted within
-             that stacking context. */
-          z-index: 1;
           --padding: var(--spacing-s) var(--spacing-m);
         }
-        gr-button paper-tooltip {
+        paper-tooltip {
           text-transform: none;
           text-align: center;
           white-space: normal;
-          width: 200px;
+          max-width: 200px;
         }
       `,
     ];
@@ -70,19 +64,23 @@
         @click="${(e: Event) => this.handleClick(e)}"
       >
         ${this.action.name}
-        <paper-tooltip
-          ?hidden="${!this.action.tooltip}"
-          offset="5"
-          fit-to-visible-bounds="true"
-          >${this.action.tooltip}</paper-tooltip
-        >
       </gr-button>
+      ${this.renderTooltip()}
+    `;
+  }
+
+  private renderTooltip() {
+    if (!this.action.tooltip) return;
+    return html`
+      <paper-tooltip offset="5" fit-to-visible-bounds="true">
+        ${this.action.tooltip}
+      </paper-tooltip>
     `;
   }
 
   handleClick(e: Event) {
     e.stopPropagation();
-    fireActionTriggered(this.eventTarget ?? this, this.action);
+    this.checksService.triggerAction(this.action);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
index 6de92e1..b4d87ae 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-attempt.ts
@@ -14,14 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {css, customElement, property} from 'lit-element';
-import {GrLitElement} from '../lit/gr-lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {CheckRun} from '../../services/checks/checks-model';
 import {ordinal} from '../../utils/string-util';
 
 @customElement('gr-checks-attempt')
-class GrChecksAttempt extends GrLitElement {
+class GrChecksAttempt extends LitElement {
   @property()
   run?: CheckRun;
 
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 08f3907..68c7957 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -14,20 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {classMap} from 'lit-html/directives/class-map';
-import {repeat} from 'lit-html/directives/repeat';
-import {ifDefined} from 'lit-html/directives/if-defined';
-import {
-  css,
-  customElement,
-  property,
-  PropertyValues,
-  query,
-  state,
-  TemplateResult,
-} from 'lit-element';
-import {GrLitElement} from '../lit/gr-lit-element';
+import {classMap} from 'lit/directives/class-map';
+import {repeat} from 'lit/directives/repeat';
+import {ifDefined} from 'lit/directives/if-defined';
+import {LitElement, css, html, PropertyValues, TemplateResult} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
 import './gr-checks-action';
 import '@polymer/paper-tooltip/paper-tooltip';
 import {
@@ -49,7 +40,6 @@
 } from '../../services/checks/checks-model';
 import {
   allResults,
-  fireActionTriggered,
   firstPrimaryLink,
   hasCompletedWithoutResults,
   iconFor,
@@ -64,7 +54,7 @@
 import {modifierPressed, toggleClass, whenVisible} from '../../utils/dom-util';
 import {durationString} from '../../utils/date-util';
 import {charsOnly} from '../../utils/string-util';
-import {isAttemptSelected} from './gr-checks-util';
+import {isAttemptSelected, matches} from './gr-checks-util';
 import {ChecksTabState} from '../../types/events';
 import {
   ConfigInfo,
@@ -81,9 +71,12 @@
   valueString,
 } from '../../utils/label-util';
 import {GerritNav} from '../core/gr-navigation/gr-navigation';
+import {DropdownLink} from '../shared/gr-dropdown/gr-dropdown';
+import {subscribe} from '../lit/subscription-controller';
+import {fontStyles} from '../../styles/gr-font-styles';
 
 @customElement('gr-result-row')
-class GrResultRow extends GrLitElement {
+class GrResultRow extends LitElement {
   @query('td.nameCol div.name')
   nameEl?: HTMLElement;
 
@@ -102,9 +95,11 @@
   @property()
   labels?: LabelNameToInfoMap;
 
+  private checksService = appContext.checksService;
+
   constructor() {
     super();
-    this.subscribe('labels', labels$);
+    subscribe(this, labels$, x => (this.labels = x));
   }
 
   static override get styles() {
@@ -284,11 +279,10 @@
     ];
   }
 
-  override update(changedProperties: PropertyValues) {
+  override updated(changedProperties: PropertyValues) {
     if (changedProperties.has('result')) {
       this.isExpandable = !!this.result?.summary && !!this.result?.message;
     }
-    super.update(changedProperties);
   }
 
   override focus() {
@@ -501,7 +495,7 @@
   }
 
   private handleAction(e: CustomEvent<Action>) {
-    fireActionTriggered(this, e.detail);
+    this.checksService.triggerAction(e.detail);
   }
 
   private renderAction(action?: Action) {
@@ -531,14 +525,14 @@
     return html`<div class="tag ${tag.color}">
       <span>${tag.name}</span>
       <paper-tooltip offset="5" fit-to-visible-bounds="true">
-        A category tag for this check result
+        ${tag.tooltip ?? 'A category tag for this check result'}
       </paper-tooltip>
     </div>`;
   }
 }
 
 @customElement('gr-result-expanded')
-class GrResultExpanded extends GrLitElement {
+class GrResultExpanded extends LitElement {
   @property()
   result?: RunResult;
 
@@ -570,7 +564,7 @@
 
   constructor() {
     super();
-    this.subscribe('repoConfig', repoConfig$);
+    subscribe(this, repoConfig$, x => (this.repoConfig = x));
   }
 
   override render() {
@@ -678,7 +672,7 @@
 );
 
 @customElement('gr-checks-results')
-export class GrChecksResults extends GrLitElement {
+export class GrChecksResults extends LitElement {
   @query('#filterInput')
   filterInput?: HTMLInputElement;
 
@@ -743,17 +737,26 @@
 
   constructor() {
     super();
-    this.subscribe('actions', topLevelActionsSelected$);
-    this.subscribe('links', topLevelLinksSelected$);
-    this.subscribe('checksPatchsetNumber', checksSelectedPatchsetNumber$);
-    this.subscribe('latestPatchsetNumber', latestPatchNum$);
-    this.subscribe('someProvidersAreLoading', someProvidersAreLoadingSelected$);
+    subscribe(this, topLevelActionsSelected$, x => (this.actions = x));
+    subscribe(this, topLevelLinksSelected$, x => (this.links = x));
+    subscribe(
+      this,
+      checksSelectedPatchsetNumber$,
+      x => (this.checksPatchsetNumber = x)
+    );
+    subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x));
+    subscribe(
+      this,
+      someProvidersAreLoadingSelected$,
+      x => (this.someProvidersAreLoading = x)
+    );
   }
 
   static override get styles() {
     return [
       sharedStyles,
       spinnerStyles,
+      fontStyles,
       css`
         :host {
           display: block;
@@ -974,6 +977,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const style = html`<style>
       .headerTopRow .right .goToLatest gr-button {
         --gr-button: {
@@ -1011,11 +1015,7 @@
         </div>
         <div class="headerBottomRow">
           <div class="left">${this.renderFilter()}</div>
-          <div class="right">
-            ${this.renderLinks()}
-            <div class="space"></div>
-            ${this.renderActions()}
-          </div>
+          <div class="right">${this.renderLinksAndActions()}</div>
         </div>
       </div>
       <div class="body">
@@ -1026,9 +1026,8 @@
       </div>`;
   }
 
-  private renderLinks() {
+  private renderLinksAndActions() {
     const links = this.links ?? [];
-    if (links.length === 0) return;
     const primaryLinks = links
       .filter(a => a.primary)
       // Showing the same icons twice without text is super confusing.
@@ -1038,14 +1037,7 @@
       )
       .slice(0, 4);
     const overflowLinks = links.filter(a => !primaryLinks.includes(a));
-    return html`
-      ${primaryLinks.map(this.renderLink)}
-      ${this.renderOverflowLinks(overflowLinks)}
-    `;
-  }
-
-  private renderOverflowLinks(overflowLinks: Link[]) {
-    const items = overflowLinks.map(link => {
+    const overflowLinkItems = overflowLinks.map(link => {
       return {
         ...link,
         id: link.tooltip,
@@ -1054,18 +1046,27 @@
         tooltip: undefined,
       };
     });
+
+    const actions = this.actions ?? [];
+    const primaryActions = actions.filter(a => a.primary).slice(0, 2);
+    const overflowActions = actions.filter(a => !primaryActions.includes(a));
+    const overflowActionItems = overflowActions.map(action => {
+      return {...action, id: action.name};
+    });
+    const disabledActions = overflowActionItems
+      .filter(action => action.disabled)
+      .map(action => action.id);
+
     return html`
-      <gr-dropdown
-        id="moreLinks"
-        link=""
-        vertical-offset="32"
-        horizontal-align="right"
-        .items="${items}"
-      >
-        <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
-        </iron-icon>
-        <span id="moreMessage">More</span>
-      </gr-dropdown>
+      ${primaryLinks.map(this.renderLink)}
+      ${primaryLinks.length > 0 && primaryActions.length > 0
+        ? html`<div class="space"></div>`
+        : ''}
+      ${primaryActions.map(this.renderAction)}
+      ${this.renderOverflow(
+        [...overflowLinkItems, ...overflowActionItems],
+        disabledActions
+      )}
     `;
   }
 
@@ -1082,26 +1083,8 @@
     >`;
   }
 
-  private renderActions() {
-    const actions = this.actions ?? [];
-    if (actions.length === 0) return;
-    const primaryActions = actions.filter(a => a.primary).slice(0, 2);
-    const overflowActions = actions.filter(a => !primaryActions.includes(a));
-    return html`
-      ${this.renderAction(primaryActions[0])}
-      ${this.renderAction(primaryActions[1])}
-      ${this.renderOverflowActions(overflowActions)}
-    `;
-  }
-
-  private renderOverflowActions(overflowActions: Action[]) {
-    const items = overflowActions.map(action => {
-      return {...action, id: action.name};
-    });
-    if (!items || items.length === 0) return;
-    const disabledItems = items
-      .filter(action => action.disabled)
-      .map(action => action.id);
+  private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
+    if (items.length === 0) return;
     return html`
       <gr-dropdown
         id="moreActions"
@@ -1110,7 +1093,7 @@
         horizontal-align="right"
         @tap-item="${this.handleAction}"
         .items="${items}"
-        .disabledIds="${disabledItems}"
+        .disabledIds="${disabledIds}"
       >
         <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
         </iron-icon>
@@ -1120,7 +1103,7 @@
   }
 
   private handleAction(e: CustomEvent<Action>) {
-    fireActionTriggered(this, e.detail);
+    this.checksService.triggerAction(e.detail);
   }
 
   private renderAction(action?: Action) {
@@ -1202,11 +1185,8 @@
     );
     const isSelection = this.selectedRuns.length > 0;
     const selected = all.filter(result => this.isRunSelected(result));
-    const filtered = selected.filter(
-      result =>
-        this.filterRegExp.test(result.checkName) ||
-        this.filterRegExp.test(result.summary) ||
-        this.filterRegExp.test(result.message ?? '')
+    const filtered = selected.filter(result =>
+      matches(result, this.filterRegExp)
     );
     let expanded = this.isSectionExpanded.get(category);
     const expandedByUser = this.isSectionExpandedByUser.get(category) ?? false;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 250b035..76fa353 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -14,25 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html, nothing} from 'lit-html';
-import {classMap} from 'lit-html/directives/class-map';
+import {classMap} from 'lit/directives/class-map';
 import './gr-hovercard-run';
-import {
-  css,
-  customElement,
-  property,
-  PropertyValues,
-  query,
-  state,
-} from 'lit-element';
-import {GrLitElement} from '../lit/gr-lit-element';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
 import './gr-checks-attempt';
 import {Action, Link, RunStatus} from '../../api/checks';
 import {sharedStyles} from '../../styles/shared-styles';
 import {
   AttemptDetail,
   compareByWorstCategory,
-  fireActionTriggered,
+  headerForStatus,
   iconFor,
   iconForRun,
   PRIMARY_STATUS_ACTIONS,
@@ -43,17 +35,15 @@
   allRunsSelectedPatchset$,
   CheckRun,
   ChecksPatchset,
-  errorMessageLatest$,
+  ErrorMessages,
+  errorMessagesLatest$,
   fakeActions,
   fakeLinks,
   fakeRun0,
   fakeRun1,
   fakeRun2,
   fakeRun3,
-  fakeRun4_1,
-  fakeRun4_2,
-  fakeRun4_3,
-  fakeRun4_4,
+  fakeRun4Att,
   loginCallbackLatest$,
   updateStateSetResults,
 } from '../../services/checks/checks-model';
@@ -68,9 +58,11 @@
 import {charsOnly} from '../../utils/string-util';
 import {appContext} from '../../services/app-context';
 import {KnownExperimentId} from '../../services/flags/flags';
+import {subscribe} from '../lit/subscription-controller';
+import {fontStyles} from '../../styles/gr-font-styles';
 
 @customElement('gr-checks-run')
-export class GrChecksRun extends GrLitElement {
+export class GrChecksRun extends LitElement {
   static override get styles() {
     return [
       sharedStyles,
@@ -92,6 +84,15 @@
           overflow: hidden;
           white-space: nowrap;
           text-overflow: ellipsis;
+          flex-shrink: 1;
+        }
+        .middle {
+          /* extra space must go between middle and right */
+          flex-grow: 1;
+          white-space: nowrap;
+        }
+        .middle gr-checks-attempt {
+          margin-left: var(--spacing-s);
         }
         .name {
           font-weight: var(--font-weight-bold);
@@ -151,6 +152,7 @@
              Alternatively we could have set the vertical padding to 0, but
              that would not have been a nice click target. */
           margin: calc(0px - var(--spacing-s));
+          margin-left: var(--spacing-s);
         }
         .attemptDetails {
           padding-bottom: var(--spacing-s);
@@ -244,6 +246,8 @@
           <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
           ${this.renderAdditionalIcon()}
           <span class="name">${this.run.checkName}</span>
+        </div>
+        <div class="middle">
           <gr-checks-attempt .run="${this.run}"></gr-checks-attempt>
           ${this.renderStatusLink()}
         </div>
@@ -273,16 +277,20 @@
     const checkNameId = charsOnly(this.run.checkName).toLowerCase();
     const id = `attempt-${detail.attempt}`;
     const icon = detail.icon;
+    const wasNotRun = icon === iconFor(RunStatus.RUNNABLE);
     return html`<div class="attemptDetail">
       <input
         type="radio"
         id="${id}"
         name="${`${checkNameId}-attempt-choice`}"
         ?checked="${this.isSelected(detail)}"
+        ?disabled="${!this.isSelected(detail) && wasNotRun}"
         @change="${() => this.handleAttemptChange(detail)}"
       />
       <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
-      <label for="${id}">Attempt ${detail.attempt}</label>
+      <label for="${id}">
+        Attempt ${detail.attempt}${wasNotRun ? ' (not run)' : ''}
+      </label>
     </div>`;
   }
 
@@ -295,9 +303,6 @@
   renderStatusLink() {
     const link = this.run.statusLink;
     if (!link) return;
-    // For COMPLETED we think that the status link are too much clutter.
-    // That could be re-considered.
-    if (this.run.status !== RunStatus.RUNNING) return;
     return html`
       <a href="${link}" target="_blank" @click="${this.onLinkClick}"
         ><iron-icon
@@ -353,7 +358,7 @@
 }
 
 @customElement('gr-checks-runs')
-export class GrChecksRuns extends GrLitElement {
+export class GrChecksRuns extends LitElement {
   @query('#filterInput')
   filterInput?: HTMLInputElement;
 
@@ -380,7 +385,7 @@
   tabState?: ChecksTabState;
 
   @property()
-  errorMessage?: string;
+  errorMessages: ErrorMessages = {};
 
   @property()
   loginCallback?: () => void;
@@ -389,16 +394,19 @@
 
   private flagService = appContext.flagsService;
 
+  private checksService = appContext.checksService;
+
   constructor() {
     super();
-    this.subscribe('runs', allRunsSelectedPatchset$);
-    this.subscribe('errorMessage', errorMessageLatest$);
-    this.subscribe('loginCallback', loginCallbackLatest$);
+    subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x));
+    subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x));
+    subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x));
   }
 
   static override get styles() {
     return [
       sharedStyles,
+      fontStyles,
       css`
         :host {
           display: block;
@@ -469,6 +477,7 @@
           padding: var(--spacing-m);
           color: var(--primary-text-color);
           margin-top: var(--spacing-m);
+          max-width: 400px;
         }
         .error {
           display: flex;
@@ -522,7 +531,7 @@
         <div class="flex-space"></div>
         ${this.renderTitleButtons()} ${this.renderCollapseButton()}
       </h2>
-      ${this.renderError()} ${this.renderSignIn()}
+      ${this.renderErrors()} ${this.renderSignIn()}
       <input
         id="filterInput"
         type="text"
@@ -536,23 +545,26 @@
     `;
   }
 
-  private renderError() {
-    if (!this.errorMessage) return;
-    return html`
-      <div class="error">
-        <div class="left">
-          <iron-icon icon="gr-icons:error"></iron-icon>
-        </div>
-        <div class="right">
-          <div>Error while fetching check results</div>
-          <div>${this.errorMessage}</div>
-        </div>
-      </div>
-    `;
+  private renderErrors() {
+    return Object.entries(this.errorMessages).map(
+      ([plugin, message]) =>
+        html`
+          <div class="error">
+            <div class="left">
+              <iron-icon icon="gr-icons:error"></iron-icon>
+            </div>
+            <div class="right">
+              <div class="message">
+                Error while fetching results for ${plugin}:<br />${message}
+              </div>
+            </div>
+          </div>
+        `
+    );
   }
 
   private renderSignIn() {
-    if (this.errorMessage || !this.loginCallback) return;
+    if (!this.loginCallback) return;
     return html`
       <div class="login">
         <div>
@@ -589,42 +601,48 @@
         @click="${() => fireRunSelectionReset(this)}"
         >Unselect All</gr-button
       >
-      <gr-button
-        class="font-normal"
-        link
+      <gr-tooltip-content
         title="${runButtonDisabled
           ? 'Disabled. Unselect checks without a "Run" action to enable the button.'
           : ''}"
-        has-tooltip="${runButtonDisabled}"
-        ?disabled="${runButtonDisabled}"
-        @click="${() => {
-          actions.forEach(action => fireActionTriggered(this, action));
-        }}"
-        >Run Selected</gr-button
+        ?has-tooltip=${runButtonDisabled}
       >
+        <gr-button
+          class="font-normal"
+          link
+          ?disabled=${runButtonDisabled}
+          @click="${() => {
+            actions.forEach(action => this.checksService.triggerAction(action));
+          }}"
+          >Run Selected</gr-button
+        >
+      </gr-tooltip-content>
     `;
   }
 
   private renderCollapseButton() {
     return html`
-      <gr-button
-        link
-        class="expandButton"
-        role="switch"
-        ?aria-checked="${this.collapsed}"
-        aria-label="${this.collapsed
-          ? 'Expand runs panel'
-          : 'Collapse runs panel'}"
-        has-tooltip="true"
+      <gr-tooltip-content
+        has-tooltip
         title="${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}"
-        @click="${() => (this.collapsed = !this.collapsed)}"
-        ><iron-icon
-          class="expandIcon"
-          icon="${this.collapsed
-            ? 'gr-icons:chevron-right'
-            : 'gr-icons:chevron-left'}"
-        ></iron-icon>
-      </gr-button>
+      >
+        <gr-button
+          link
+          class="expandButton"
+          role="switch"
+          ?aria-checked="${this.collapsed}"
+          aria-label="${this.collapsed
+            ? 'Expand runs panel'
+            : 'Collapse runs panel'}"
+          @click="${() => (this.collapsed = !this.collapsed)}"
+          ><iron-icon
+            class="expandIcon"
+            icon="${this.collapsed
+              ? 'gr-icons:chevron-right'
+              : 'gr-icons:chevron-left'}"
+          ></iron-icon>
+        </gr-button>
+      </gr-tooltip-content>
     `;
   }
 
@@ -652,13 +670,7 @@
     updateStateSetResults('f1', [fakeRun1], [], [], ChecksPatchset.LATEST);
     updateStateSetResults('f2', [fakeRun2], [], [], ChecksPatchset.LATEST);
     updateStateSetResults('f3', [fakeRun3], [], [], ChecksPatchset.LATEST);
-    updateStateSetResults(
-      'f4',
-      [fakeRun4_1, fakeRun4_2, fakeRun4_3, fakeRun4_4],
-      [],
-      [],
-      ChecksPatchset.LATEST
-    );
+    updateStateSetResults('f4', fakeRun4Att, [], [], ChecksPatchset.LATEST);
   }
 
   toggle(
@@ -694,7 +706,7 @@
           @click="${() => this.toggleExpanded(status)}"
         >
           <iron-icon class="expandIcon" icon="${icon}"></iron-icon>
-          <h3 class="heading-3">${status.toLowerCase()}</h3>
+          <h3 class="heading-3">${headerForStatus(status)}</h3>
         </div>
         <div class="sectionRuns">${runs.map(run => this.renderRun(run))}</div>
       </div>
@@ -748,11 +760,7 @@
         <gr-button link @click="${() => this.toggle('f3', [fakeRun3])}"
           >3</gr-button
         >
-        <gr-button
-          link
-          @click="${() => {
-            this.toggle('f4', [fakeRun4_1, fakeRun4_2, fakeRun4_3, fakeRun4_4]);
-          }}"
+        <gr-button link @click="${() => this.toggle('f4', fakeRun4Att)}}"
           >4</gr-button
         >
         <gr-button link @click="${this.all}">all</gr-button>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-styles.ts b/polygerrit-ui/app/elements/checks/gr-checks-styles.ts
index 9a1605e..dbc2bec 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-styles.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-styles.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index 1e98dfa..688667a 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {css, customElement, property, PropertyValues, state} from 'lit-element';
-import {GrLitElement} from '../lit/gr-lit-element';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
 import {Action} from '../../api/checks';
 import {
   CheckResult,
@@ -32,17 +31,15 @@
 import {ActionTriggeredEvent} from '../../services/checks/checks-util';
 import {AttemptSelectedEvent, RunSelectedEvent} from './gr-checks-util';
 import {ChecksTabState} from '../../types/events';
-import {fireAlert, fireEvent} from '../../utils/event-util';
 import {appContext} from '../../services/app-context';
-import {from, timer} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
+import {subscribe} from '../lit/subscription-controller';
 
 /**
  * The "Checks" tab on the Gerrit change page. Gets its data from plugins that
  * have registered with the Checks Plugin API.
  */
 @customElement('gr-checks-tab')
-export class GrChecksTab extends GrLitElement {
+export class GrChecksTab extends LitElement {
   @property()
   runs: CheckRun[] = [];
 
@@ -75,11 +72,15 @@
 
   constructor() {
     super();
-    this.subscribe('runs', allRunsSelectedPatchset$);
-    this.subscribe('results', allResultsSelected$);
-    this.subscribe('checksPatchsetNumber', checksSelectedPatchsetNumber$);
-    this.subscribe('latestPatchsetNumber', latestPatchNum$);
-    this.subscribe('changeNum', changeNum$);
+    subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x));
+    subscribe(this, allResultsSelected$, x => (this.results = x));
+    subscribe(
+      this,
+      checksSelectedPatchsetNumber$,
+      x => (this.checksPatchsetNumber = x)
+    );
+    subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x));
+    subscribe(this, changeNum$, x => (this.changeNum = x));
 
     this.addEventListener('action-triggered', (e: ActionTriggeredEvent) =>
       this.handleActionTriggered(e.detail.action, e.detail.run)
@@ -139,35 +140,7 @@
   }
 
   handleActionTriggered(action: Action, run?: CheckRun) {
-    if (!this.changeNum) return;
-    const patchSet = this.checksPatchsetNumber ?? this.latestPatchsetNumber;
-    if (!patchSet) return;
-    const promise = action.callback(
-      this.changeNum,
-      patchSet,
-      run?.attempt,
-      run?.externalId,
-      run?.checkName,
-      action.name
-    );
-    // If plugins return undefined or not a promise, then show no toast.
-    if (!promise?.then) return;
-
-    fireAlert(this, `Triggering action '${action.name}' ...`);
-    from(promise)
-      // If the action takes longer than 5 seconds, then most likely the
-      // user is either not interested or the result not relevant anymore.
-      .pipe(takeUntil(timer(5000)))
-      .subscribe(result => {
-        if (result.errorMessage || result.message) {
-          fireAlert(this, `${result.message ?? result.errorMessage}`);
-        } else {
-          fireEvent(this, 'hide-alert');
-        }
-        if (result.shouldReload) {
-          this.checksService.reloadForCheck(run?.checkName);
-        }
-      });
+    this.checksService.triggerAction(action, run);
   }
 
   handleRunSelected(e: RunSelectedEvent) {
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util.ts b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
index 05a87a4..e9bbb22 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-util.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {CheckRun} from '../../services/checks/checks-model';
+import {CheckRun, RunResult} from '../../services/checks/checks-model';
 
 export interface AttemptSelectedEventDetail {
   checkName: string;
@@ -85,3 +85,12 @@
     (selected === undefined && run.isLatestAttempt) || selected === run.attempt
   );
 }
+
+export function matches(result: RunResult, regExp: RegExp) {
+  return (
+    regExp.test(result.checkName) ||
+    regExp.test(result.summary) ||
+    (result.tags ?? []).some(tag => regExp.test(tag.name)) ||
+    regExp.test(result.message ?? '')
+  );
+}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-util_test.ts b/polygerrit-ui/app/elements/checks/gr-checks-util_test.ts
new file mode 100644
index 0000000..698a4a1
--- /dev/null
+++ b/polygerrit-ui/app/elements/checks/gr-checks-util_test.ts
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../test/common-test-setup-karma';
+import {createRunResult} from '../../test/test-data-generators';
+import {matches} from './gr-checks-util';
+import {RunResult} from '../../services/checks/checks-model';
+
+suite('gr-checks-util test', () => {
+  test('regexp filter matching results', () => {
+    const result: RunResult = {
+      ...createRunResult(),
+      tags: [{name: 'tag'}],
+    };
+    assert.isTrue(matches(result, new RegExp('message')));
+    assert.isTrue(matches(result, new RegExp('summary')));
+    assert.isTrue(matches(result, new RegExp('name')));
+    assert.isTrue(matches(result, new RegExp('tag')));
+    assert.isFalse(matches(result, new RegExp('qwertyui')));
+  });
+});
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index 2316a05..d26856c 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -15,13 +15,15 @@
  * limitations under the License.
  */
 import './gr-checks-styles';
-import {hovercardBehaviorMixin} from '../shared/gr-hovercard/gr-hovercard-behavior';
+import '../../styles/gr-font-styles';
+import {HovercardBehaviorMixin} from '../shared/gr-hovercard/gr-hovercard-behavior';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-hovercard-run_html';
 import {customElement, property} from '@polymer/decorators';
 import './gr-checks-action';
 import {CheckRun} from '../../services/checks/checks-model';
 import {
+  AttemptDetail,
   iconFor,
   runActions,
   worstCategory,
@@ -30,8 +32,11 @@
 import {RunStatus} from '../../api/checks';
 import {ordinal} from '../../utils/string-util';
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardBehaviorMixin(PolymerElement);
+
 @customElement('gr-hovercard-run')
-export class GrHovercardRun extends hovercardBehaviorMixin(PolymerElement) {
+export class GrHovercardRun extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -56,6 +61,13 @@
     return ordinal(attempt);
   }
 
+  computeAttempts(run?: CheckRun): AttemptDetail[] {
+    const details = run?.attemptDetails ?? [];
+    const more =
+      details.length > 7 ? [{icon: 'more-horiz', attempt: undefined}] : [];
+    return [...more, ...details.slice(-7)];
+  }
+
   computeChipIcon(run?: CheckRun) {
     if (run?.status === RunStatus.COMPLETED) return 'check';
     if (run?.status === RunStatus.RUNNING) return 'timelapse';
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
index 5b2e24a..49a1416 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="gr-checks-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
@@ -30,6 +33,9 @@
       display: flex;
       margin-top: var(--spacing-s);
     }
+    .attempts.row {
+      flex-wrap: wrap;
+    }
     .chipRow {
       display: flex;
       margin-top: var(--spacing-s);
@@ -91,7 +97,7 @@
       margin-right: var(--spacing-s);
       color: var(--deemphasized-text-color);
       text-align: center;
-      width: 20px;
+      width: 24px;
       font-size: var(--font-size-small);
     }
     div.action {
@@ -151,9 +157,9 @@
         <iron-icon class="small" icon="gr-icons:arrow-forward"></iron-icon>
       </div>
       <div class="sectionContent">
-        <div hidden$="[[hideAttempts(run)]]" class="row">
+        <div hidden$="[[hideAttempts(run)]]" class="attempts row">
           <div class="title">Attempt</div>
-          <template is="dom-repeat" items="[[run.attemptDetails]]">
+          <template is="dom-repeat" items="[[computeAttempts(run)]]">
             <div>
               <div class="attemptIcon">
                 <iron-icon
@@ -216,7 +222,10 @@
     </div>
     <template is="dom-repeat" items="[[computeActions(run)]]">
       <div class="action">
-        <gr-checks-action action="[[item]]"></gr-checks-action>
+        <gr-checks-action
+          event-target="[[_target]]"
+          action="[[item]]"
+        ></gr-checks-action>
       </div>
     </template>
   </div>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index 881eb28..c2066d7 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -25,8 +25,8 @@
   DropdownLink,
 } from '../../shared/gr-dropdown/gr-dropdown';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
 
@@ -37,7 +37,7 @@
 }
 
 @customElement('gr-account-dropdown')
-export class GrAccountDropdown extends GrLitElement {
+export class GrAccountDropdown extends LitElement {
   @property({type: Object})
   account?: AccountInfo;
 
@@ -76,7 +76,7 @@
     super.disconnectedCallback();
   }
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -92,9 +92,10 @@
     ];
   }
 
-  render() {
+  override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         gr-dropdown {
@@ -111,17 +112,17 @@
       <gr-dropdown
         link=""
         .items="${this.links}"
-        .top-content="${this.topContent}"
+        .topContent="${this.topContent}"
         @tap-item-shortcuts=${this._handleShortcutsTap}
-        .horizontal-align="right"
+        .horizontalAlign=${'right'}
       >
-        <span ?hidden=${this._hasAvatars}
+        <span ?hidden="${this._hasAvatars}"
           >${this._accountName(this.account)}</span
         >
         <gr-avatar
           .account="${this.account}"
           ?hidden=${!this._hasAvatars}
-          .imageSize="56"
+          .imageSize=${56}
           aria-label="Account avatar"
         ></gr-avatar>
       </gr-dropdown>`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index 3ccc960..80ebf2d 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -30,6 +30,7 @@
 } from '../../../test/test-data-generators';
 import {tap} from '@polymer/iron-test-helpers/mock-interactions';
 import {AccountId} from '../../../types/common';
+import {waitUntil} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-error-manager');
 
@@ -62,7 +63,7 @@
       });
     });
 
-    test('does not show auth error on 403 by default', done => {
+    test('does not show auth error on 403 by default', async () => {
       const showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
       const responseText = Promise.resolve('server says no.');
       element.dispatchEvent(
@@ -79,13 +80,11 @@
           bubbles: true,
         })
       );
-      flush(() => {
-        assert.isFalse(showAuthErrorStub.calledOnce);
-        done();
-      });
+      await flush();
+      assert.isFalse(showAuthErrorStub.calledOnce);
     });
 
-    test('show auth required for 403 with auth error and not authed before', done => {
+    test('show auth required for 403 with auth error and not authed before', async () => {
       const showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
       const responseText = Promise.resolve('Authentication required\n');
       getLoggedInStub.returns(Promise.resolve(true));
@@ -103,10 +102,8 @@
           bubbles: true,
         })
       );
-      flush(() => {
-        assert.isTrue(showAuthErrorStub.calledOnce);
-        done();
-      });
+      await flush();
+      assert.isTrue(showAuthErrorStub.calledOnce);
     });
 
     test('recheck auth for 403 with auth error if authed before', async () => {
@@ -148,7 +145,7 @@
       );
     });
 
-    test('show normal Error', done => {
+    test('show normal Error', async () => {
       const showErrorSpy = sinon.spy(element, '_showErrorDialog');
       const textSpy = sinon.spy(() => Promise.resolve('ZOMG'));
       element.dispatchEvent(
@@ -160,13 +157,9 @@
       );
 
       assert.isTrue(textSpy.called);
-      flush(() => {
-        assert.isTrue(showErrorSpy.calledOnce);
-        assert.isTrue(
-          showErrorSpy.lastCall.calledWithExactly('Error 500: ZOMG')
-        );
-        done();
-      });
+      await flush();
+      assert.isTrue(showErrorSpy.calledOnce);
+      assert.isTrue(showErrorSpy.lastCall.calledWithExactly('Error 500: ZOMG'));
     });
 
     test('constructServerErrorMsg', () => {
@@ -206,7 +199,7 @@
       );
     });
 
-    test('extract trace id from headers if exists', done => {
+    test('extract trace id from headers if exists', async () => {
       const textSpy = sinon.spy(() => Promise.resolve('500'));
       const headers = new Headers();
       headers.set('X-Gerrit-Trace', 'xxxx');
@@ -223,16 +216,14 @@
           bubbles: true,
         })
       );
-      flush(() => {
-        assert.equal(
-          element.$.errorDialog.text,
-          'Error 500: 500\nTrace Id: xxxx'
-        );
-        done();
-      });
+      await flush();
+      assert.equal(
+        element.$.errorDialog.text,
+        'Error 500: 500\nTrace Id: xxxx'
+      );
     });
 
-    test('suppress TOO_MANY_FILES error', done => {
+    test('suppress TOO_MANY_FILES error', async () => {
       const showAlertStub = sinon.stub(element, '_showAlert');
       const textSpy = sinon.spy(() =>
         Promise.resolve('too many files to find conflicts')
@@ -246,13 +237,11 @@
       );
 
       assert.isTrue(textSpy.called);
-      flush(() => {
-        assert.isFalse(showAlertStub.called);
-        done();
-      });
+      await flush();
+      assert.isFalse(showAlertStub.called);
     });
 
-    test('suppress CONFLICTS_OPERATOR_IS_NOT_SUPPORTED error', done => {
+    test('suppress CONFLICTS_OPERATOR_IS_NOT_SUPPORTED error', async () => {
       const showAlertStub = sinon.stub(element, '_showAlert');
       const textSpy = sinon.spy(() =>
         Promise.resolve("'conflicts:' operator is not supported by server")
@@ -266,13 +255,11 @@
       );
 
       assert.isTrue(textSpy.called);
-      flush(() => {
-        assert.isFalse(showAlertStub.called);
-        done();
-      });
+      await flush();
+      assert.isFalse(showAlertStub.called);
     });
 
-    test('show network error', done => {
+    test('show network error', async () => {
       const showAlertStub = sinon.stub(element, '_showAlert');
       element.dispatchEvent(
         new CustomEvent('network-error', {
@@ -281,13 +268,11 @@
           bubbles: true,
         })
       );
-      flush(() => {
-        assert.isTrue(showAlertStub.calledOnce);
-        assert.isTrue(
-          showAlertStub.lastCall.calledWithExactly('Server unavailable')
-        );
-        done();
-      });
+      await flush();
+      assert.isTrue(showAlertStub.calledOnce);
+      assert.isTrue(
+        showAlertStub.lastCall.calledWithExactly('Server unavailable')
+      );
     });
 
     test('_canOverride alerts', () => {
@@ -487,7 +472,7 @@
       assert.include(toast.shadowRoot.textContent, 'second-test');
     });
 
-    test('regular toast should not dismiss auth toast', done => {
+    test('regular toast should not dismiss auth toast', async () => {
       // Set status to AUTHED.
       appContext.authService.authCheck();
       const responseText = Promise.resolve('Authentication required\n');
@@ -509,37 +494,34 @@
         })
       );
       assert.equal(fetchStub.callCount, 1);
-      flush(() => {
-        // here needs two flush as there are two chained
-        // promises on server-error handler and flush only flushes one
-        assert.equal(fetchStub.callCount, 2);
-        flush(() => {
-          let toast = toastSpy.lastCall.returnValue;
-          assert.include(toast.shadowRoot.textContent, 'Credentials expired.');
-          assert.include(toast.shadowRoot.textContent, 'Refresh credentials');
+      await flush();
 
-          // fake an alert
-          element.dispatchEvent(
-            new CustomEvent('show-alert', {
-              detail: {
-                message: 'test-alert',
-                action: 'reload',
-              },
-              composed: true,
-              bubbles: true,
-            })
-          );
-          flush(() => {
-            toast = toastSpy.lastCall.returnValue;
-            assert.isOk(toast);
-            assert.include(
-              toast.shadowRoot.textContent,
-              'Credentials expired.'
-            );
-            done();
-          });
-        });
-      });
+      // here needs two flush as there are two chained
+      // promises on server-error handler and flush only flushes one
+      assert.equal(fetchStub.callCount, 2);
+      await flush();
+      await waitUntil(() => toastSpy.calledOnce);
+      let toast = toastSpy.lastCall.returnValue;
+      assert.include(toast.shadowRoot.textContent, 'Credentials expired.');
+      assert.include(toast.shadowRoot.textContent, 'Refresh credentials');
+
+      // fake an alert
+      element.dispatchEvent(
+        new CustomEvent('show-alert', {
+          detail: {
+            message: 'test-alert',
+            action: 'reload',
+          },
+          composed: true,
+          bubbles: true,
+        })
+      );
+
+      await flush();
+      assert.isTrue(toastSpy.calledOnce);
+      toast = toastSpy.lastCall.returnValue;
+      assert.isOk(toast);
+      assert.include(toast.shadowRoot.textContent, 'Credentials expired.');
     });
 
     test('show alert', () => {
@@ -578,7 +560,7 @@
       assert.equal(element._lastCredentialCheck, 999999);
     });
 
-    test('refreshes with same credentials', done => {
+    test('refreshes with same credentials', async () => {
       const accountPromise = Promise.resolve({
         ...createAccountDetailWithId(1234),
       });
@@ -594,12 +576,10 @@
       element._refreshingCredentials = true;
       element._checkSignedIn();
 
-      flush(() => {
-        assert.isFalse(requestCheckStub.called);
-        assert.isTrue(handleRefreshStub.called);
-        assert.isFalse(reloadStub.called);
-        done();
-      });
+      await flush();
+      assert.isFalse(requestCheckStub.called);
+      assert.isTrue(handleRefreshStub.called);
+      assert.isFalse(reloadStub.called);
     });
 
     test('_showAlert hides existing alerts', () => {
@@ -609,7 +589,7 @@
       // assert.isTrue(hideStub.calledOnce);
     });
 
-    test('show-error', () => {
+    test('show-error', async () => {
       const openStub = sinon.stub(element.$.errorOverlay, 'open');
       const closeStub = sinon.stub(element.$.errorOverlay, 'close');
       const reportStub = stubReporting('reportErrorDialog');
@@ -622,7 +602,7 @@
           bubbles: true,
         })
       );
-      flush();
+      await flush();
 
       assert.isTrue(openStub.called);
       assert.isTrue(reportStub.called);
@@ -634,12 +614,12 @@
           bubbles: true,
         })
       );
-      flush();
+      await flush();
 
       assert.isTrue(closeStub.called);
     });
 
-    test('reloads when refreshed credentials differ', done => {
+    test('reloads when refreshed credentials differ', async () => {
       const accountPromise = Promise.resolve({
         ...createAccountDetailWithId(1234),
       });
@@ -655,12 +635,11 @@
       element._refreshingCredentials = true;
       element._checkSignedIn();
 
-      flush(() => {
-        assert.isFalse(requestCheckStub.called);
-        assert.isFalse(handleRefreshStub.called);
-        assert.isTrue(reloadStub.called);
-        done();
-      });
+      await flush();
+
+      assert.isFalse(requestCheckStub.called);
+      assert.isFalse(handleRefreshStub.called);
+      assert.isTrue(reloadStub.called);
     });
   });
 
@@ -678,7 +657,7 @@
       });
     });
 
-    test('refresh loop continues on credential fail', done => {
+    test('refresh loop continues on credential fail', async () => {
       const requestCheckStub = sinon.stub(element, '_requestCheckLoggedIn');
       const handleRefreshStub = sinon.stub(
         element,
@@ -689,12 +668,10 @@
       element._refreshingCredentials = true;
       element._checkSignedIn();
 
-      flush(() => {
-        assert.isTrue(requestCheckStub.called);
-        assert.isFalse(handleRefreshStub.called);
-        assert.isFalse(reloadStub.called);
-        done();
-      });
+      await flush();
+      assert.isTrue(requestCheckStub.called);
+      assert.isFalse(handleRefreshStub.called);
+      assert.isFalse(reloadStub.called);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
index c820ec4..1ae0992 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from 'lit-html';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -25,7 +24,7 @@
 }
 
 @customElement('gr-key-binding-display')
-export class GrKeyBindingDisplay extends GrLitElement {
+export class GrKeyBindingDisplay extends LitElement {
   static override get styles() {
     return [
       css`
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
index 68b696e..0b191f1 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -17,6 +17,7 @@
 import '../../shared/gr-button/gr-button';
 import '../gr-key-binding-display/gr-key-binding-display';
 import '../../../styles/shared-styles';
+import '../../../styles/gr-font-styles';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html';
 import {
@@ -38,10 +39,11 @@
   shortcuts?: SectionView;
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-keyboard-shortcuts-dialog')
-export class GrKeyboardShortcutsDialog extends KeyboardShortcutMixin(
-  PolymerElement
-) {
+export class GrKeyboardShortcutsDialog extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
index 3576dfe..4992daa 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       display: block;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts
index 4bd048c..b745c3d 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts
@@ -210,17 +210,19 @@
         class="hideOnMobile"
         name="header-browse-source"
       ></gr-endpoint-decorator>
-      <template is="dom-if" if="[[_feedbackURL]]">
-        <a class="feedbackButton"
-          href$="[[_feedbackURL]]"
-          title="File a bug"
-          aria-label="File a bug"
-          target="_blank"
-          role="button"
-        >
-          <iron-icon icon="gr-icons:bug"></iron-icon>
-        </a>
-      </template>
+      <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
+        <template is="dom-if" if="[[_feedbackURL]]">
+          <a
+            href$="[[_feedbackURL]]"
+            title="File a bug"
+            aria-label="File a bug"
+            target="_blank"
+            role="button"
+          >
+            <iron-icon icon="gr-icons:bug"></iron-icon>
+          </a>
+        </template>
+      </gr-endpoint-decorator>
       </div>
       <div class="accountContainer" id="accountContainer">
         <iron-icon
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index 784b440..0f58ac0 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -474,7 +474,7 @@
 
   test('shows feedback icon when URL provided', async () => {
     assert.isEmpty(element._feedbackURL);
-    assert.isNotOk(query(element, '.feedbackButton'));
+    assert.isNotOk(query(element, '.feedbackButton > a'));
 
     const url = 'report_bug_url';
     const config: ServerInfo = {
@@ -488,7 +488,7 @@
     await flush();
 
     assert.equal(element._feedbackURL, url);
-    assert.ok(query(element, '.feedbackButton'));
+    assert.ok(query(element, '.feedbackButton > a'));
   });
 
   test('register URL', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 89e81d4..b8f2630 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -309,8 +309,8 @@
   changeNum: NumericChangeId;
   project: RepoName;
   path?: string;
-  patchNum?: PatchSetNum | null;
-  basePatchNum?: BasePatchSetNum | null;
+  patchNum?: PatchSetNum;
+  basePatchNum?: BasePatchSetNum;
   lineNum?: number | string;
   leftSide?: boolean;
   commentId?: UrlEncodedCommentId;
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 ac4ac64..8e70eb9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -57,6 +57,7 @@
   RepoName,
   ServerInfo,
   UrlEncodedCommentId,
+  ParentPatchSetNum,
 } from '../../../types/common';
 import {
   AppElement,
@@ -176,8 +177,8 @@
 
   CHANGE_ID_QUERY: /^\/id\/(I[0-9a-f]{40})$/,
 
-  // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
-  CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
+  // Matches /c/<changeNum>/[*][/].
+  CHANGE_LEGACY: /^\/c\/(\d+)\/(.*)$/,
   CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
 
   // Matches
@@ -209,10 +210,6 @@
   // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
   DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(#\d+)?$/,
 
-  // Matches non-project-relative
-  // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
-  DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
-
   // Matches diff routes using @\d+ to specify a file name (whether or not
   // the project name is included).
   // eslint-disable-next-line max-len
@@ -286,18 +283,9 @@
 
 type QueryStringItem = [string, string]; // [key, value]
 
-type GenerateUrlLegacyChangeViewParameters = Omit<
-  GenerateUrlChangeViewParameters,
-  'project'
->;
-type GenerateUrlLegacyDiffViewParameters = Omit<
-  GenerateUrlDiffViewParameters,
-  'project'
->;
-
 interface PatchRangeParams {
-  patchNum?: PatchSetNum | null;
-  basePatchNum?: BasePatchSetNum | null;
+  patchNum?: PatchSetNum;
+  basePatchNum?: BasePatchSetNum;
 }
 
 @customElement('gr-router')
@@ -671,66 +659,34 @@
     if (params.patchNum) {
       range = `${params.patchNum}`;
     }
-    if (params.basePatchNum) {
+    if (params.basePatchNum && params.basePatchNum !== ParentPatchSetNum) {
       range = `${params.basePatchNum}..${range}`;
     }
     return range;
   }
 
   /**
-   * Given a set of params without a project, gets the project from the rest
-   * API project lookup and then sets the app params.
-   */
-  _normalizeLegacyRouteParams(
-    params: Readonly<
-      | GenerateUrlLegacyChangeViewParameters
-      | GenerateUrlLegacyDiffViewParameters
-    >
-  ) {
-    if (!params.changeNum) {
-      return Promise.resolve();
-    }
-
-    return this.restApiService
-      .getFromProjectLookup(params.changeNum)
-      .then(project => {
-        // Show a 404 and terminate if the lookup request failed. Attempting
-        // to redirect after failing to get the project loops infinitely.
-        if (!project) {
-          this._show404();
-          return;
-        }
-        const updatedParams:
-          | GenerateUrlChangeViewParameters
-          | GenerateUrlDiffViewParameters = {...params, project};
-        this._normalizePatchRangeParams(updatedParams);
-        this._redirect(this._generateUrl(updatedParams));
-      });
-  }
-
-  /**
    * Normalizes the params object, and determines if the URL needs to be
    * modified to fit the proper schema.
    *
    */
   _normalizePatchRangeParams(params: PatchRangeParams) {
-    if (params.basePatchNum === null || params.basePatchNum === undefined) {
+    if (params.basePatchNum === undefined) {
       return false;
     }
-    const hasPatchNum =
-      params.patchNum !== null && params.patchNum !== undefined;
+    const hasPatchNum = params.patchNum !== undefined;
     let needsRedirect = false;
 
     // Diffing a patch against itself is invalid, so if the base and revision
     // patches are equal clear the base.
     if (params.patchNum && params.basePatchNum === params.patchNum) {
       needsRedirect = true;
-      params.basePatchNum = null;
+      params.basePatchNum = ParentPatchSetNum;
     } else if (!hasPatchNum) {
       // Regexes set basePatchNum instead of patchNum when only one is
       // specified. Redirect is not needed in this case.
       params.patchNum = params.basePatchNum;
-      params.basePatchNum = null;
+      params.basePatchNum = ParentPatchSetNum;
     }
     return needsRedirect;
   }
@@ -1100,8 +1056,6 @@
 
     this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
 
-    this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
-
     this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
 
     this._mapRoute(
@@ -1666,41 +1620,26 @@
   }
 
   _handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
-    // Parameter order is based on the regex group number matched.
-    const params: GenerateUrlLegacyChangeViewParameters = {
-      changeNum: Number(ctx.params[0]) as NumericChangeId,
-      basePatchNum: convertToPatchSetNum(ctx.params[3]) as BasePatchSetNum,
-      patchNum: convertToPatchSetNum(ctx.params[5]),
-      view: GerritView.CHANGE,
-      querystring: ctx.querystring,
-    };
-
-    this._normalizeLegacyRouteParams(params);
+    const changeNum = Number(ctx.params[0]) as NumericChangeId;
+    if (!changeNum) {
+      this._show404();
+      return;
+    }
+    this.restApiService.getFromProjectLookup(changeNum).then(project => {
+      // Show a 404 and terminate if the lookup request failed. Attempting
+      // to redirect after failing to get the project loops infinitely.
+      if (!project) {
+        this._show404();
+        return;
+      }
+      this._redirect(`/c/${project}/+/${changeNum}/${ctx.params[1]}`);
+    });
   }
 
   _handleLegacyLinenum(ctx: PageContextWithQueryMap) {
     this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
   }
 
-  _handleDiffLegacyRoute(ctx: PageContextWithQueryMap) {
-    // Parameter order is based on the regex group number matched.
-    const params: GenerateUrlLegacyDiffViewParameters = {
-      changeNum: Number(ctx.params[0]) as NumericChangeId,
-      basePatchNum: convertToPatchSetNum(ctx.params[2]) as BasePatchSetNum,
-      patchNum: convertToPatchSetNum(ctx.params[4]),
-      path: ctx.params[5],
-      view: GerritView.DIFF,
-    };
-
-    const address = this._parseLineAddress(ctx.hash);
-    if (address) {
-      params.leftSide = address.leftSide;
-      params.lineNum = address.lineNum;
-    }
-
-    this._normalizeLegacyRouteParams(params);
-  }
-
   _handleDiffEditRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index ff27bbd..b91bf0c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -22,6 +22,7 @@
 import {stubBaseUrl, stubRestApi, addListenerForTest} from '../../../test/test-utils.js';
 import {_testOnly_RoutePattern} from './gr-router.js';
 import {GerritView} from '../../../services/router/router-model.js';
+import {ParentPatchSetNum} from '../../../types/common.js';
 
 const basicFixture = fixtureFromElement('gr-router');
 
@@ -191,7 +192,6 @@
       '_handleDiffRoute',
       '_handleDefaultRoute',
       '_handleChangeLegacyRoute',
-      '_handleDiffLegacyRoute',
       '_handleDocumentationRedirectRoute',
       '_handleDocumentationSearchRoute',
       '_handleDocumentationSearchRedirectRoute',
@@ -535,72 +535,12 @@
   });
 
   suite('param normalization', () => {
-    let projectLookupStub;
-    let generateUrlStub;
-
-    setup(() => {
-      projectLookupStub = stubRestApi('getFromProjectLookup');
-      generateUrlStub = sinon.stub(element, '_generateUrl');
-    });
-
-    suite('_normalizeLegacyRouteParams', () => {
-      let rangeStub;
-      let redirectStub;
-      let show404Stub;
-
-      setup(() => {
-        rangeStub = sinon.stub(element, '_normalizePatchRangeParams')
-            .returns(Promise.resolve());
-        redirectStub = sinon.stub(element, '_redirect');
-        show404Stub = sinon.stub(element, '_show404');
-      });
-
-      test('w/o changeNum', () => {
-        projectLookupStub.returns(Promise.resolve('foo/bar'));
-        const params = {};
-        return element._normalizeLegacyRouteParams(params).then(() => {
-          assert.isFalse(generateUrlStub.calledOnce);
-          assert.isFalse(projectLookupStub.called);
-          assert.isFalse(rangeStub.called);
-          assert.isFalse(redirectStub.called);
-          assert.isFalse(show404Stub.called);
-        });
-      });
-
-      test('w/ changeNum', () => {
-        projectLookupStub.returns(Promise.resolve('foo/bar'));
-        const params = {changeNum: 1234};
-
-        return element._normalizeLegacyRouteParams(params).then(() => {
-          assert.isTrue(generateUrlStub.calledOnce);
-          const updatedParams = generateUrlStub.lastCall.args[0];
-          assert.isTrue(projectLookupStub.called);
-          assert.isTrue(rangeStub.called);
-          assert.equal(updatedParams.project, 'foo/bar');
-          assert.isTrue(redirectStub.calledOnce);
-          assert.isFalse(show404Stub.called);
-        });
-      });
-
-      test('halts on project lookup failure', () => {
-        projectLookupStub.returns(Promise.resolve(undefined));
-        const params = {changeNum: 1234};
-        return element._normalizeLegacyRouteParams(params).then(() => {
-          assert.isFalse(generateUrlStub.calledOnce);
-          assert.isTrue(projectLookupStub.called);
-          assert.isFalse(rangeStub.called);
-          assert.isFalse(redirectStub.called);
-          assert.isTrue(show404Stub.calledOnce);
-        });
-      });
-    });
-
     suite('_normalizePatchRangeParams', () => {
       test('range n..n normalizes to n', () => {
         const params = {basePatchNum: 4, patchNum: 4};
         const needsRedirect = element._normalizePatchRangeParams(params);
         assert.isTrue(needsRedirect);
-        assert.isNotOk(params.basePatchNum);
+        assert.equal(params.basePatchNum, ParentPatchSetNum);
         assert.equal(params.patchNum, 4);
       });
 
@@ -608,7 +548,7 @@
         const params = {basePatchNum: 4};
         const needsRedirect = element._normalizePatchRangeParams(params);
         assert.isFalse(needsRedirect);
-        assert.isNotOk(params.basePatchNum);
+        assert.equal(params.basePatchNum, ParentPatchSetNum);
         assert.equal(params.patchNum, 4);
       });
     });
@@ -1366,58 +1306,19 @@
         assert.isTrue(redirectStub.calledWithExactly('/c/12345'));
       });
 
-      test('_handleChangeLegacyRoute', () => {
-        const normalizeRouteStub = sinon.stub(element,
-            '_normalizeLegacyRouteParams');
+      test('_handleChangeLegacyRoute', async () => {
+        stubRestApi('getFromProjectLookup').returns(Promise.resolve('project'));
         const ctx = {
           params: [
             1234, // 0 Change number
-            null, // 1 Unused
-            null, // 2 Unused
-            6, // 3 Base patch number
-            null, // 4 Unused
-            9, // 5 Patch number
+            'comment/6789',
           ],
           querystring: '',
         };
         element._handleChangeLegacyRoute(ctx);
-        assert.isTrue(normalizeRouteStub.calledOnce);
-        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
-          changeNum: 1234,
-          basePatchNum: 6,
-          patchNum: 9,
-          view: GerritView.CHANGE,
-          querystring: '',
-        });
-      });
-
-      test('_handleDiffLegacyRoute', () => {
-        const normalizeRouteStub = sinon.stub(element,
-            '_normalizeLegacyRouteParams');
-        const ctx = {
-          params: [
-            1234, // 0 Change number
-            null, // 1 Unused
-            3, // 2 Base patch number
-            null, // 3 Unused
-            8, // 4 Patch number
-            'foo/bar', // 5 Diff path
-          ],
-          path: '/c/1234/3..8/foo/bar',
-          hash: 'b123',
-        };
-        element._handleDiffLegacyRoute(ctx);
-        assert.isFalse(redirectStub.called);
-        assert.isTrue(normalizeRouteStub.calledOnce);
-        assert.deepEqual(normalizeRouteStub.lastCall.args[0], {
-          changeNum: 1234,
-          basePatchNum: 3,
-          patchNum: 8,
-          view: GerritView.DIFF,
-          path: 'foo/bar',
-          lineNum: 123,
-          leftSide: true,
-        });
+        await flush();
+        assert.isTrue(redirectStub.calledWithExactly('/c/project/+/1234' +
+            '/comment/6789'));
       });
 
       test('_handleLegacyLinenum w/ @321', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 7c65c3f..240bb22 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -67,10 +67,10 @@
   'footer:',
   'from:',
   'has:',
+  'has:attention',
   'has:draft',
   'has:edit',
   'has:star',
-  'has:stars',
   'has:unresolved',
   'hashtag:',
   'inhashtag:',
@@ -78,6 +78,8 @@
   'is:',
   'is:abandoned',
   'is:assigned',
+  'is:attention',
+  'is:cherrypick',
   'is:closed',
   'is:ignored',
   'is:merge',
@@ -110,6 +112,7 @@
   'reviewer:',
   'reviewer:self',
   'reviewerin:',
+  'rule:',
   'size:',
   'star:',
   'status:',
@@ -147,8 +150,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-search-bar')
-export class GrSearchBar extends KeyboardShortcutMixin(PolymerElement) {
+export class GrSearchBar extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -198,7 +204,7 @@
     this.query = (input: string) => this._getSearchSuggestions(input);
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.restApiService.getConfig().then((serverConfig?: ServerInfo) => {
       const mergeability =
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index 3b22d1d..b5b0124 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -22,6 +22,7 @@
 import {
   TestKeyboardShortcutBinder,
   stubRestApi,
+  mockPromise,
 } from '../../../test/test-utils';
 import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util';
@@ -47,9 +48,9 @@
     TestKeyboardShortcutBinder.pop();
   });
 
-  setup(done => {
+  setup(async () => {
     element = basicFixture.instantiate();
-    flush(done);
+    await flush();
   });
 
   test('value is propagated to _inputVal', () => {
@@ -62,10 +63,11 @@
       ? document.activeElement!.shadowRoot.activeElement
       : document.activeElement;
 
-  test('enter in search input fires event', done => {
+  test('enter in search input fires event', async () => {
+    const promise = mockPromise();
     element.addEventListener('handle-search', () => {
       assert.notEqual(getActiveElement(), element.$.searchInput);
-      done();
+      promise.resolve();
     });
     element.value = 'test';
     MockInteractions.pressAndReleaseKeyOn(
@@ -74,6 +76,7 @@
       null,
       'enter'
     );
+    await promise;
   });
 
   test('input blurred after commit', () => {
@@ -176,7 +179,7 @@
       });
     });
 
-    test('Autocompletes groups', done => {
+    test('Autocompletes groups', async () => {
       sinon
         .stub(element, 'groupSuggestions')
         .callsFake(() =>
@@ -185,13 +188,11 @@
             {text: 'ownerin:gerrit'},
           ])
         );
-      element._getSearchSuggestions('ownerin:pol').then(s => {
-        assert.equal(s[0].value, 'ownerin:Polygerrit');
-        done();
-      });
+      const s = await element._getSearchSuggestions('ownerin:pol');
+      assert.equal(s[0].value, 'ownerin:Polygerrit');
     });
 
-    test('Autocompletes projects', done => {
+    test('Autocompletes projects', async () => {
       sinon
         .stub(element, 'projectSuggestions')
         .callsFake(() =>
@@ -201,27 +202,21 @@
             {text: 'project:gerrittest'},
           ])
         );
-      element._getSearchSuggestions('project:pol').then(s => {
-        assert.equal(s[0].value, 'project:Polygerrit');
-        done();
-      });
+      const s = await element._getSearchSuggestions('project:pol');
+      assert.equal(s[0].value, 'project:Polygerrit');
     });
 
-    test('Autocompletes simple searches', done => {
-      element._getSearchSuggestions('is:o').then(s => {
-        assert.equal(s[0].name, 'is:open');
-        assert.equal(s[0].value, 'is:open');
-        assert.equal(s[1].name, 'is:owner');
-        assert.equal(s[1].value, 'is:owner');
-        done();
-      });
+    test('Autocompletes simple searches', async () => {
+      const s = await element._getSearchSuggestions('is:o');
+      assert.equal(s[0].name, 'is:open');
+      assert.equal(s[0].value, 'is:open');
+      assert.equal(s[1].name, 'is:owner');
+      assert.equal(s[1].value, 'is:owner');
     });
 
-    test('Does not autocomplete with no match', done => {
-      element._getSearchSuggestions('asdasdasdasd').then(s => {
-        assert.equal(s.length, 0);
-        done();
-      });
+    test('Does not autocomplete with no match', async () => {
+      const s = await element._getSearchSuggestions('asdasdasdasd');
+      assert.equal(s.length, 0);
     });
 
     test('Autocompletes without is:mergable when disabled', async () => {
@@ -235,7 +230,7 @@
     'REF_UPDATED_AND_CHANGE_REINDEX',
   ].forEach(mergeability => {
     suite(`mergeability as ${mergeability}`, () => {
-      setup(done => {
+      setup(async () => {
         stubRestApi('getConfig').returns(
           Promise.resolve({
             ...createServerInfo(),
@@ -248,24 +243,22 @@
         );
 
         element = basicFixture.instantiate();
-        flush(done);
+        await flush();
       });
 
-      test('Autocompltes with is:mergable when enabled', done => {
-        element._getSearchSuggestions('is:mergeab').then(s => {
-          assert.equal(s.length, 2);
-          assert.equal(s[0].name, 'is:mergeable');
-          assert.equal(s[0].value, 'is:mergeable');
-          assert.equal(s[1].name, '-is:mergeable');
-          assert.equal(s[1].value, '-is:mergeable');
-          done();
-        });
+      test('Autocompltes with is:mergable when enabled', async () => {
+        const s = await element._getSearchSuggestions('is:mergeab');
+        assert.equal(s.length, 2);
+        assert.equal(s[0].name, 'is:mergeable');
+        assert.equal(s[0].value, 'is:mergeable');
+        assert.equal(s[1].name, '-is:mergeable');
+        assert.equal(s[1].value, '-is:mergeable');
       });
     });
   });
 
   suite('doc url', () => {
-    setup(done => {
+    setup(async () => {
       stubRestApi('getConfig').returns(
         Promise.resolve({
           ...createServerInfo(),
@@ -278,7 +271,7 @@
 
       _testOnly_clearDocsBaseUrlCache();
       element = basicFixture.instantiate();
-      flush(done);
+      await flush();
     });
 
     test('compute help doc url with correct path', () => {
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 aa01c5f..f5072b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -38,7 +38,7 @@
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {appContext} from '../../../services/app-context';
 import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
-import {ParsedChangeInfo} from '../../../types/types';
+import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
 import {KnownExperimentId} from '../../../services/flags/flags';
@@ -99,16 +99,25 @@
   })
   _disableApplyFixButton = false;
 
-  layers = appContext.flagsService.isEnabled(
-    KnownExperimentId.TOKEN_HIGHLIGHTING
-  )
-    ? [new TokenHighlightLayer()]
-    : [];
+  @property({type: Array})
+  layers: DiffLayer[] = [];
 
   private refitOverlay?: () => void;
 
   private readonly restApiService = appContext.restApiService;
 
+  constructor() {
+    super();
+    this.restApiService.getPreferences().then(prefs => {
+      if (
+        !prefs?.disable_token_highlighting &&
+        appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
+      ) {
+        this.layers = [new TokenHighlightLayer(this)];
+      }
+    });
+  }
+
   /**
    * Given robot comment CustomEvent object, fetch diffs associated
    * with first robot comment suggested fix and open dialog.
@@ -141,7 +150,7 @@
     });
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.refitOverlay = () => {
       // re-center the dialog as content changed
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 d3d7615..94d37f5 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
@@ -186,6 +186,7 @@
         })
       );
       element._isApplyFixLoading = true;
+      await flush();
       const button = getConfirmButton();
       assert.isTrue(button.hasAttribute('disabled'));
       assert.equal(button.getAttribute('title'), '');
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
index ff275ad..af9c9d4 100644
--- a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
@@ -31,14 +31,8 @@
 import {fire} from '../../../utils/event-util';
 import {DiffInfo} from '../../../types/diff';
 import {assertIsDefined} from '../../../utils/common-util';
-import {
-  css,
-  customElement,
-  html,
-  LitElement,
-  property,
-  TemplateResult,
-} from 'lit-element';
+import {css, html, LitElement, TemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 import {
   ContextButtonType,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 4455bd5..44b0b8b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -53,7 +53,8 @@
   let element;
   let builder;
 
-  const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
+  const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
+  const WBR_HTML = '<wbr class="style-scope gr-diff">';
 
   setup(() => {
     element = basicFixture.instantiate();
@@ -78,59 +79,60 @@
   test('newlines 1', () => {
     let text = 'abcdef';
 
-    assert.equal(builder._formatText(text, 4, 10).innerHTML, text);
+    assert.equal(builder._formatText(text, 'NONE', 4, 10).innerHTML, text);
     text = 'a'.repeat(20);
-    assert.equal(builder._formatText(text, 4, 10).innerHTML,
+    assert.equal(builder._formatText(text, 'NONE', 4, 10).innerHTML,
         'a'.repeat(10) +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         'a'.repeat(10));
   });
 
   test('newlines 2', () => {
     const text = '<span class="thumbsup">👍</span>';
-    assert.equal(builder._formatText(text, 4, 10).innerHTML,
+    assert.equal(builder._formatText(text, 'NONE', 4, 10).innerHTML,
         '&lt;span clas' +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         's="thumbsu' +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         'p"&gt;👍&lt;/span' +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         '&gt;');
   });
 
   test('newlines 3', () => {
     const text = '01234\t56789';
-    assert.equal(builder._formatText(text, 4, 10).innerHTML,
+    assert.equal(builder._formatText(text, 'NONE', 4, 10).innerHTML,
         '01234' + builder._getTabWrapper(3).outerHTML + '56' +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         '789');
   });
 
   test('newlines 4', () => {
     const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍';
-    assert.equal(builder._formatText(text, 4, 20).innerHTML,
+    assert.equal(builder._formatText(text, 'NONE', 4, 20).innerHTML,
         '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' +
-        LINE_FEED_HTML +
+        LINE_BREAK_HTML +
         '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍');
   });
 
-  test('line_length ignored if line_wrapping is true', () => {
+  test('line_length applied with <wbr> if line_wrapping is true', () => {
     builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
     const text = 'a'.repeat(51);
 
     const line = {text, highlights: []};
+    const expected = 'a'.repeat(50) + WBR_HTML + 'a';
     const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
-    assert.equal(result, text);
+    assert.equal(result, expected);
   });
 
-  test('line_length applied if line_wrapping is false', () => {
+  test('line_length applied with line break if line_wrapping is false', () => {
     builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
     const text = 'a'.repeat(51);
 
     const line = {text, highlights: []};
-    const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a';
+    const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
     const result = builder._createTextEl(undefined, line).firstChild.innerHTML;
     assert.equal(result, expected);
   });
@@ -173,22 +175,25 @@
   test('text length with tabs and unicode', () => {
     function expectTextLength(text, tabSize, expected) {
       // Formatting to |expected| columns should not introduce line breaks.
-      const result = builder._formatText(text, tabSize, expected);
+      const result = builder._formatText(text, 'NONE', tabSize, expected);
       assert.isNotOk(result.querySelector('.contentText > .br'),
           `  Expected the result of: \n` +
-          `      _formatText(${text}', ${tabSize}, ${expected})\n` +
+          `      _formatText(${text}', 'NONE',  ${tabSize}, ${expected})\n` +
           `  to not contain a br. But the actual result HTML was:\n` +
           `      '${result.innerHTML}'\nwhereupon`);
 
       // Increasing the line limit should produce the same markup.
-      assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML,
+      assert.equal(
+          builder._formatText(text, 'NONE', tabSize, Infinity).innerHTML,
           result.innerHTML);
-      assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML,
+      assert.equal(
+          builder._formatText(text, 'NONE', tabSize, expected + 1).innerHTML,
           result.innerHTML);
 
       // Decreasing the line limit should introduce line breaks.
       if (expected > 0) {
-        const tooSmall = builder._formatText(text, tabSize, expected - 1);
+        const tooSmall = builder._formatText(text,
+            'NONE', tabSize, expected - 1);
         assert.isOk(tooSmall.querySelector('.contentText > .br'),
             `  Expected the result of: \n` +
             `      _formatText(${text}', ${tabSize}, ${expected - 1})\n` +
@@ -216,7 +221,7 @@
     assert.ok(wrapper);
     assert.equal(wrapper.innerText, '\t');
     assert.equal(
-        builder._formatText(html, tabSize, Infinity).innerHTML,
+        builder._formatText(html, 'NONE', tabSize, Infinity).innerHTML,
         'abc' + wrapper.outerHTML + 'def');
   });
 
@@ -746,7 +751,7 @@
     let outputEl;
     let keyLocations;
 
-    setup(done => {
+    setup(async () => {
       const prefs = {
         line_length: 10,
         show_tabs: true,
@@ -781,11 +786,11 @@
         return builder;
       });
       element.diff = {content};
-      element.render(keyLocations, prefs).then(done);
+      await element.render(keyLocations, prefs);
     });
 
-    test('addColumns is called', done => {
-      element.render(keyLocations, {}).then(done);
+    test('addColumns is called', async () => {
+      await element.render(keyLocations, {});
       assert.isTrue(element._builder.addColumns.called);
     });
 
@@ -807,15 +812,13 @@
       assert.strictEqual(sections[1], section[1]);
     });
 
-    test('render-start and render-content are fired', done => {
+    test('render-start and render-content are fired', async () => {
       const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
-      element.render(keyLocations, {}).then(() => {
-        const firedEventTypes = dispatchEventStub.getCalls()
-            .map(c => c.args[0].type);
-        assert.include(firedEventTypes, 'render-start');
-        assert.include(firedEventTypes, 'render-content');
-        done();
-      });
+      await element.render(keyLocations, {});
+      const firedEventTypes = dispatchEventStub.getCalls()
+          .map(c => c.args[0].type);
+      assert.include(firedEventTypes, 'render-start');
+      assert.include(firedEventTypes, 'render-content');
     });
 
     test('cancel', () => {
@@ -832,7 +835,7 @@
     let prefs;
     let keyLocations;
 
-    setup(done => {
+    setup(async () => {
       element = mockDiffFixture.instantiate();
       diff = getMockDiffResponse();
       element.diff = diff;
@@ -844,10 +847,8 @@
       };
       keyLocations = {left: {}, right: {}};
 
-      element.render(keyLocations, prefs).then(() => {
-        builder = element._builder;
-        done();
-      });
+      await element.render(keyLocations, prefs);
+      builder = element._builder;
     });
 
     test('aria-labels on added line numbers', () => {
@@ -981,34 +982,30 @@
       assert.isTrue(lineNumberEl.classList.contains('right'));
     });
 
-    test('_getLineNumberEl unified left', done => {
+    test('_getLineNumberEl unified left', async () => {
       // Re-render as unified:
       element.viewMode = 'UNIFIED_DIFF';
-      element.render(keyLocations, prefs).then(() => {
-        builder = element._builder;
+      await element.render(keyLocations, prefs);
+      builder = element._builder;
 
-        const contentEl = builder.getContentByLine(5, 'left',
-            element.$.diffTable);
-        const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
-        assert.isTrue(lineNumberEl.classList.contains('lineNum'));
-        assert.isTrue(lineNumberEl.classList.contains('left'));
-        done();
-      });
+      const contentEl = builder.getContentByLine(5, 'left',
+          element.$.diffTable);
+      const lineNumberEl = builder._getLineNumberEl(contentEl, 'left');
+      assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+      assert.isTrue(lineNumberEl.classList.contains('left'));
     });
 
-    test('_getLineNumberEl unified right', done => {
+    test('_getLineNumberEl unified right', async () => {
       // Re-render as unified:
       element.viewMode = 'UNIFIED_DIFF';
-      element.render(keyLocations, prefs).then(() => {
-        builder = element._builder;
+      await element.render(keyLocations, prefs);
+      builder = element._builder;
 
-        const contentEl = builder.getContentByLine(5, 'right',
-            element.$.diffTable);
-        const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
-        assert.isTrue(lineNumberEl.classList.contains('lineNum'));
-        assert.isTrue(lineNumberEl.classList.contains('right'));
-        done();
-      });
+      const contentEl = builder.getContentByLine(5, 'right',
+          element.$.diffTable);
+      const lineNumberEl = builder._getLineNumberEl(contentEl, 'right');
+      assert.isTrue(lineNumberEl.classList.contains('lineNum'));
+      assert.isTrue(lineNumberEl.classList.contains('right'));
     });
 
     test('_getNextContentOnSide side-by-side left', () => {
@@ -1035,44 +1032,38 @@
       assert.equal(nextElem.textContent, expectedNextString);
     });
 
-    test('_getNextContentOnSide unified left', done => {
+    test('_getNextContentOnSide unified left', async () => {
       // Re-render as unified:
       element.viewMode = 'UNIFIED_DIFF';
-      element.render(keyLocations, prefs).then(() => {
-        builder = element._builder;
+      await element.render(keyLocations, prefs);
+      builder = element._builder;
 
-        const startElem = builder.getContentByLine(5, 'left',
-            element.$.diffTable);
-        const expectedStartString = diff.content[2].ab[0];
-        const expectedNextString = diff.content[2].ab[1];
-        assert.equal(startElem.textContent, expectedStartString);
+      const startElem = builder.getContentByLine(5, 'left',
+          element.$.diffTable);
+      const expectedStartString = diff.content[2].ab[0];
+      const expectedNextString = diff.content[2].ab[1];
+      assert.equal(startElem.textContent, expectedStartString);
 
-        const nextElem = builder._getNextContentOnSide(startElem,
-            'left');
-        assert.equal(nextElem.textContent, expectedNextString);
-
-        done();
-      });
+      const nextElem = builder._getNextContentOnSide(startElem,
+          'left');
+      assert.equal(nextElem.textContent, expectedNextString);
     });
 
-    test('_getNextContentOnSide unified right', done => {
+    test('_getNextContentOnSide unified right', async () => {
       // Re-render as unified:
       element.viewMode = 'UNIFIED_DIFF';
-      element.render(keyLocations, prefs).then(() => {
-        builder = element._builder;
+      await element.render(keyLocations, prefs);
+      builder = element._builder;
 
-        const startElem = builder.getContentByLine(5, 'right',
-            element.$.diffTable);
-        const expectedStartString = diff.content[1].b[0];
-        const expectedNextString = diff.content[1].b[1];
-        assert.equal(startElem.textContent, expectedStartString);
+      const startElem = builder.getContentByLine(5, 'right',
+          element.$.diffTable);
+      const expectedStartString = diff.content[1].b[0];
+      const expectedNextString = diff.content[1].b[1];
+      assert.equal(startElem.textContent, expectedStartString);
 
-        const nextElem = builder._getNextContentOnSide(startElem,
-            'right');
-        assert.equal(nextElem.textContent, expectedNextString);
-
-        done();
-      });
+      const nextElem = builder._getNextContentOnSide(startElem,
+          'right');
+      assert.equal(nextElem.textContent, expectedNextString);
     });
 
     test('escaping HTML', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index 057b926..bd7dc29 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -39,6 +39,7 @@
       numberOfCells: 4,
       movedOutIndex: 1,
       movedInIndex: 3,
+      lineNumberCols: [0, 2],
     };
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
index b2dfa61..a7b3a42 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -38,6 +38,7 @@
       numberOfCells: 3,
       movedOutIndex: 2,
       movedInIndex: 2,
+      lineNumberCols: [0, 1],
     };
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 987f808..663ee7e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -89,6 +89,12 @@
   return 'NONE';
 }
 
+export function isResponsive(responsiveMode: DiffResponsiveMode) {
+  return (
+    responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY'
+  );
+}
+
 export abstract class GrDiffBuilder {
   private readonly _diff: DiffInfo;
 
@@ -513,12 +519,11 @@
     const {beforeNumber, afterNumber} = line;
     if (beforeNumber !== 'FILE' && beforeNumber !== 'LOST') {
       const responsiveMode = getResponsiveMode(this._prefs, this._renderPrefs);
-      const lineLimit =
-        responsiveMode === 'NONE' ? this._prefs.line_length : Infinity;
       const contentText = this._formatText(
         line.text,
+        responsiveMode,
         this._prefs.tab_size,
-        lineLimit
+        this._prefs.line_length
       );
 
       if (side) {
@@ -544,6 +549,12 @@
     return td;
   }
 
+  private createLineBreak(responsive: boolean) {
+    return responsive
+      ? this._createElement('wbr')
+      : this._createElement('span', 'br');
+  }
+
   /**
    * Returns a 'div' element containing the supplied |text| as its innerText,
    * with '\t' characters expanded to a width determined by |tabSize|, and the
@@ -554,9 +565,15 @@
    * @param tabSize The width of each tab stop.
    * @param lineLimit The column after which to wrap lines.
    */
-  _formatText(text: string, tabSize: number, lineLimit: number): HTMLElement {
+  _formatText(
+    text: string,
+    responsiveMode: DiffResponsiveMode,
+    tabSize: number,
+    lineLimit: number
+  ): HTMLElement {
     const contentText = this._createElement('div', 'contentText');
-
+    contentText.ariaLabel = text;
+    const responsive = isResponsive(responsiveMode);
     let columnPos = 0;
     let textOffset = 0;
     for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) {
@@ -570,7 +587,7 @@
           contentText.appendChild(
             document.createTextNode(segment.substring(rowStart, rowEnd))
           );
-          contentText.appendChild(this._createElement('span', 'br'));
+          contentText.appendChild(this.createLineBreak(responsive));
           columnPos = 0;
           rowStart = rowEnd;
           rowEnd += lineLimit;
@@ -588,7 +605,7 @@
           // Append a single '\t' character.
           let effectiveTabSize = tabSize - (columnPos % tabSize);
           if (columnPos + effectiveTabSize > lineLimit) {
-            contentText.appendChild(this._createElement('span', 'br'));
+            contentText.appendChild(this.createLineBreak(responsive));
             columnPos = 0;
             effectiveTabSize = tabSize;
           }
@@ -598,7 +615,7 @@
         } else {
           // Append a single surrogate pair.
           if (columnPos >= lineLimit) {
-            contentText.appendChild(this._createElement('span', 'br'));
+            contentText.appendChild(this.createLineBreak(responsive));
             columnPos = 0;
           }
           contentText.appendChild(
@@ -667,6 +684,7 @@
     numberOfCells: number;
     movedOutIndex: number;
     movedInIndex: number;
+    lineNumberCols: number[];
   };
 
   /**
@@ -760,7 +778,7 @@
 
   _buildMoveControls(group: GrDiffGroup) {
     const movedIn = group.adds.length > 0;
-    const {numberOfCells, movedOutIndex, movedInIndex} =
+    const {numberOfCells, movedOutIndex, movedInIndex, lineNumberCols} =
       this._getMoveControlsConfig();
 
     let controlsClass;
@@ -778,6 +796,9 @@
     const cells = [...Array(numberOfCells).keys()].map(() =>
       this._createElement('td')
     );
+    lineNumberCols.forEach(index => {
+      cells[index].classList.add('moveControlsLineNumCol');
+    });
 
     const moveRangeHeader = this._createElement('gr-range-header');
     moveRangeHeader.setAttribute('icon', 'gr-icons:move-item');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index 647f701..de7d007 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -15,9 +15,17 @@
  * limitations under the License.
  */
 import {DiffLayer, DiffLayerListener} from '../../../types/types';
-import {GrDiffLine, Side} from '../../../api/diff';
+import {GrDiffLine, Side, TokenHighlightListener} from '../../../api/diff';
+import {assertIsDefined} from '../../../utils/common-util';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
 import {debounce, DelayedTask} from '../../../utils/async-util';
+
+import {
+  getLineElByChild,
+  getSideByLineEl,
+  getPreviousContentNodes,
+} from '../gr-diff/gr-diff-utils';
+
 import {
   getLineNumberByChild,
   lineNumberToNumber,
@@ -31,7 +39,7 @@
 /** CSS class for the currently hovered token. */
 const CSS_HIGHLIGHT = 'token-highlight';
 
-const HOVER_DELAY_MS = 200;
+export const HOVER_DELAY_MS = 200;
 
 const LINE_LENGTH_LIMIT = 500;
 
@@ -42,11 +50,10 @@
 const TOKEN_OCCURRENCES_LIMIT = 1000;
 
 /**
- * Token highlighting is only useful for code on-screen, so don't bother
- * highlighting tokens that are further away than this threshold from where the
- * user is hovering.
+ * Token highlighting is only useful for code on-screen, so we only highlight
+ * the nearest set of tokens up to this limit.
  */
-const LINE_DISTANCE_THRESHOLD = 100;
+const TOKEN_HIGHLIGHT_LIMIT = 100;
 
 /**
  * When a user hovers over a token in the diff, then this layer makes sure that
@@ -66,6 +73,9 @@
   /** The currently highlighted token. */
   private currentHighlight?: string;
 
+  /** Trigger when a new token starts or stoped being highlighted.*/
+  private readonly tokenHighlightListener?: TokenHighlightListener;
+
   /**
    * The line of the currently highlighted token. We store this in order to
    * re-render only relevant lines of the diff. Only lines visible on the screen
@@ -96,9 +106,13 @@
 
   private updateTokenTask?: DelayedTask;
 
-  constructor() {
-    window.addEventListener('click', _ => {
-      this.handleWindowClick();
+  constructor(
+    container: HTMLElement = document.documentElement,
+    tokenHighlightListener?: TokenHighlightListener
+  ) {
+    this.tokenHighlightListener = tokenHighlightListener;
+    container.addEventListener('click', e => {
+      this.handleContainerClick(e);
     });
   }
 
@@ -137,10 +151,10 @@
       // garbage collected along with the element itself once it is not attached
       // to the DOM anymore and no references exist anymore.
       el.addEventListener('mouseover', e => {
-        this.handleMouseOver(e);
+        this.handleTokenMouseOver(e);
       });
       el.addEventListener('mouseout', e => {
-        this.handleMouseOut(e);
+        this.handleTokenMouseOut(e);
       });
     }
   }
@@ -162,10 +176,10 @@
     numbers.add(Number(lineNumber));
   }
 
-  private handleMouseOut(e: MouseEvent) {
+  private handleTokenMouseOut(e: MouseEvent) {
     // If there's no ongoing hover-task, terminate early.
     if (!this.updateTokenTask?.isActive()) return;
-    if (this.interferesWithSelection(e)) return;
+    if (e.buttons > 0 || this.interferesWithSelection()) return;
     const {element} = this.findTokenAncestor(e?.target);
     if (!element) return;
     if (element === this.hoveredElement) {
@@ -176,8 +190,8 @@
     }
   }
 
-  private handleMouseOver(e: MouseEvent) {
-    if (this.interferesWithSelection(e)) return;
+  private handleTokenMouseOver(e: MouseEvent) {
+    if (e.buttons > 0 || this.interferesWithSelection()) return;
     const {
       line,
       token: newHighlight,
@@ -189,36 +203,26 @@
     this.updateTokenTask = debounce(
       this.updateTokenTask,
       () => {
-        this.updateTokenHighlight(newHighlight, line);
+        this.updateTokenHighlight(newHighlight, line, element);
       },
       HOVER_DELAY_MS
     );
   }
 
-  private interferesWithSelection(e: MouseEvent) {
-    if (e.buttons > 0) return true;
-    if (window.getSelection()?.type === 'Range') return true;
-    return false;
-  }
-
-  private handleWindowClick() {
+  private handleContainerClick(e: MouseEvent) {
+    if (this.interferesWithSelection()) return;
+    // Ignore the click if the click is on a token.
+    // We can't use e.target becauses it gets retargetted to the container as
+    // it's a shadow dom.
+    const {element} = this.findTokenAncestor(e.composedPath()[0]);
+    if (element) return;
     this.hoveredElement = undefined;
     this.updateTokenTask?.cancel();
-    this.updateTokenHighlight(undefined, 0);
+    this.updateTokenHighlight(undefined, 0, undefined);
   }
 
-  private updateTokenHighlight(
-    newHighlight: string | undefined,
-    newLineNumber: number
-  ) {
-    const oldHighlight = this.currentHighlight;
-    const oldLineNumber = this.currentHighlightLineNumber;
-    this.currentHighlight = newHighlight;
-    this.currentHighlightLineNumber = newLineNumber;
-    this.notifyForToken(oldHighlight, oldLineNumber);
-    this.notifyForToken(newHighlight, newLineNumber);
-    // Reset the hovered element.
-    this.hoveredElement = undefined;
+  private interferesWithSelection() {
+    return document.getSelection()?.type === 'Range';
   }
 
   findTokenAncestor(el?: EventTarget | Element | null): {
@@ -250,20 +254,94 @@
     return (linesLeft?.size ?? 0) + (linesRight?.size ?? 0);
   }
 
+  private updateTokenHighlight(
+    newHighlight: string | undefined,
+    newLineNumber: number,
+    newHoveredElement: Element | undefined
+  ) {
+    if (
+      this.currentHighlight === newHighlight &&
+      this.currentHighlightLineNumber === newLineNumber
+    )
+      return;
+    const oldHighlight = this.currentHighlight;
+    const oldLineNumber = this.currentHighlightLineNumber;
+    this.currentHighlight = newHighlight;
+    this.currentHighlightLineNumber = newLineNumber;
+    this.triggerTokenHighlightEvent(
+      newHighlight,
+      newLineNumber,
+      newHoveredElement
+    );
+    this.notifyForToken(oldHighlight, oldLineNumber);
+    this.notifyForToken(newHighlight, newLineNumber);
+  }
+
+  triggerTokenHighlightEvent(
+    token: string | undefined,
+    line: number,
+    element: Element | undefined
+  ) {
+    if (!this.tokenHighlightListener) {
+      return;
+    }
+    if (!token || !element) {
+      this.tokenHighlightListener(undefined);
+      return;
+    }
+    const previousTextLength = getPreviousContentNodes(element)
+      .map(sib => sib.textContent!.length)
+      .reduce((partial_sum, a) => partial_sum + a, 0);
+    const lineEl = getLineElByChild(element);
+    assertIsDefined(lineEl, 'Line element should be found!');
+    const side = getSideByLineEl(lineEl);
+    const range = {
+      start_line: line,
+      start_column: previousTextLength + 1, // 1-based inclusive
+      end_line: line,
+      end_column: previousTextLength + token.length, // 1-based inclusive
+    };
+    this.tokenHighlightListener({token, element, side, range});
+  }
+
+  getSortedLinesForSide(
+    lineMapping: Map<string, Set<number>>,
+    token: string | undefined,
+    lineNumber: number
+  ): Array<number> {
+    if (!token) return [];
+    const lineSet = lineMapping.get(token);
+    if (!lineSet) return [];
+    const lines = [...lineSet];
+    lines.sort((a, b) => {
+      const da = Math.abs(a - lineNumber);
+      const db = Math.abs(b - lineNumber);
+      // For equal distance, prefer lines later in the file over earlier in the
+      // file. This ensures total ordering.
+      if (da === db) return b - a;
+      // Compare the distance to lineNumber.
+      return da - db;
+    });
+    return lines.slice(0, TOKEN_HIGHLIGHT_LIMIT);
+  }
+
   notifyForToken(token: string | undefined, lineNumber: number) {
-    if (!token) return;
-    const linesLeft = this.tokenToLinesLeft.get(token);
-    linesLeft?.forEach(line => {
-      if (Math.abs(line - lineNumber) < LINE_DISTANCE_THRESHOLD) {
-        this.notifyListeners(line, Side.LEFT);
-      }
-    });
-    const linesRight = this.tokenToLinesRight.get(token);
-    linesRight?.forEach(line => {
-      if (Math.abs(line - lineNumber) < LINE_DISTANCE_THRESHOLD) {
-        this.notifyListeners(line, Side.RIGHT);
-      }
-    });
+    const leftLines = this.getSortedLinesForSide(
+      this.tokenToLinesLeft,
+      token,
+      lineNumber
+    );
+    for (const line of leftLines) {
+      this.notifyListeners(line, Side.LEFT);
+    }
+    const rightLines = this.getSortedLinesForSide(
+      this.tokenToLinesRight,
+      token,
+      lineNumber
+    );
+    for (const line of rightLines) {
+      this.notifyListeners(line, Side.RIGHT);
+    }
   }
 
   addListener(listener: DiffLayerListener) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
new file mode 100644
index 0000000..2993d35
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -0,0 +1,339 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {Side, TokenHighlightEventDetails} from '../../../api/diff';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
+import {HOVER_DELAY_MS, TokenHighlightLayer} from './token-highlight-layer';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {html, render} from 'lit';
+import {_testOnly_allTasks} from '../../../utils/async-util';
+import {queryAndAssert} from '../../../test/test-utils';
+
+// MockInteractions.makeMouseEvent always sets buttons to 1.
+function dispatchMouseEvent(
+  type: string,
+  xy: {x: number; y: number},
+  node: Element
+) {
+  const props = {
+    bubbles: true,
+    cancellable: true,
+    composed: true,
+    clientX: xy.x,
+    clientY: xy.y,
+    buttons: 0,
+  };
+  node.dispatchEvent(new MouseEvent(type, props));
+}
+
+class MockListener {
+  private results: any[][] = [];
+
+  notify(...args: any[]) {
+    this.results.push(args);
+  }
+
+  shift() {
+    return this.results.shift();
+  }
+
+  flush() {
+    this.results = [];
+  }
+
+  get pending(): number {
+    return this.results.length;
+  }
+}
+
+suite('token-highlight-layer', () => {
+  let container: HTMLElement;
+  let listener: MockListener;
+  let highlighter: TokenHighlightLayer;
+  let tokenHighlightingCalls: {details?: TokenHighlightEventDetails}[] = [];
+
+  function tokenHighlightListener(
+    highlightDetails?: TokenHighlightEventDetails
+  ) {
+    tokenHighlightingCalls.push({details: highlightDetails});
+  }
+
+  setup(async () => {
+    listener = new MockListener();
+    tokenHighlightingCalls = [];
+    container = document.createElement('div');
+    document.body.appendChild(container);
+    highlighter = new TokenHighlightLayer(container, tokenHighlightListener);
+    highlighter.addListener((...args) => listener.notify(...args));
+  });
+
+  teardown(() => {
+    document.body.removeChild(container);
+  });
+
+  function annotate(el: HTMLElement, side: Side = Side.LEFT, line = 1) {
+    const diffLine = new GrDiffLine(GrDiffLineType.BOTH);
+    diffLine.afterNumber = line;
+    diffLine.beforeNumber = line;
+    highlighter.annotate(el, document.createElement('div'), diffLine, side);
+    return el;
+  }
+
+  let uniqueId = 0;
+  function createLineId() {
+    uniqueId++;
+    return `line-${uniqueId.toString()}`;
+  }
+
+  function createLine(text: string, line = 1): HTMLElement {
+    const lineId = createLineId();
+    const template = html`
+      <div class="line">
+        <div data-value=${line} class="lineNum right"></div>
+        <div class="content">
+          <div id=${lineId} class="contentText">${text}</div>
+        </div>
+      </div>
+    `;
+
+    const div = document.createElement('div');
+    render(template, div);
+    container.appendChild(div);
+    const el = queryAndAssert(container, `#${lineId}`);
+    return el as HTMLElement;
+  }
+
+  suite('annotate', () => {
+    function assertAnnotation(
+      args: any[],
+      el: HTMLElement,
+      start: number,
+      length: number,
+      cssClass: string
+    ) {
+      assert.equal(args[0], el);
+      assert.equal(args[1], start);
+      assert.equal(args[2], length);
+      assert.equal(args[3], cssClass);
+    }
+
+    test('annotate adds css token', () => {
+      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const el = createLine('these are words');
+      annotate(el);
+      assert.isTrue(annotateElementStub.calledThrice);
+      assertAnnotation(annotateElementStub.args[0], el, 0, 5, 'tk-these token');
+      assertAnnotation(annotateElementStub.args[1], el, 6, 3, 'tk-are token');
+      assertAnnotation(
+        annotateElementStub.args[2],
+        el,
+        10,
+        5,
+        'tk-words token'
+      );
+    });
+
+    test('annotate adds mouse handlers', () => {
+      const el = createLine('these are words');
+      const addEventListenerStub = sinon.stub(el, 'addEventListener');
+      annotate(el);
+      assert.isTrue(addEventListenerStub.calledTwice);
+      assert.equal(addEventListenerStub.args[0][0], 'mouseover');
+      assert.equal(addEventListenerStub.args[1][0], 'mouseout');
+    });
+
+    test('annotate does not add mouse handlers without words', () => {
+      const el = createLine('  ');
+      const addEventListenerStub = sinon.stub(el, 'addEventListener');
+      annotate(el);
+      assert.isFalse(addEventListenerStub.called);
+    });
+
+    test('annotate adds mouse handlers for longest word', () => {
+      const el = createLine('w'.repeat(100));
+      const addEventListenerStub = sinon.stub(el, 'addEventListener');
+      annotate(el);
+      assert.isTrue(addEventListenerStub.called);
+    });
+
+    test('annotate does not add mouse handlers for long words', () => {
+      const el = createLine('w'.repeat(101));
+      const addEventListenerStub = sinon.stub(el, 'addEventListener');
+      annotate(el);
+      assert.isFalse(addEventListenerStub.called);
+    });
+  });
+
+  suite('highlight', () => {
+    test('highlighting hover delay', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words');
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+
+      assert.equal(listener.pending, 0);
+      assert.equal(_testOnly_allTasks.size, 1);
+
+      // Too early for hover behavior to trigger.
+      clock.tick(100);
+      assert.equal(listener.pending, 0);
+      assert.equal(_testOnly_allTasks.size, 1);
+
+      // After a total of HOVER_DELAY_MS ms the hover behavior should trigger.
+      clock.tick(HOVER_DELAY_MS - 100);
+      assert.equal(listener.pending, 2);
+      assert.equal(_testOnly_allTasks.size, 0);
+      assert.deepEqual(listener.shift(), [1, 1, Side.LEFT]);
+      assert.deepEqual(listener.shift(), [2, 2, Side.RIGHT]);
+    });
+
+    test('highlighting spans many lines', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words');
+      annotate(line2, Side.RIGHT, 1000);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+
+      assert.equal(listener.pending, 0);
+
+      // After a total of HOVER_DELAY_MS ms the hover behavior should trigger.
+      clock.tick(HOVER_DELAY_MS);
+      assert.equal(listener.pending, 2);
+      assert.equal(_testOnly_allTasks.size, 0);
+      assert.deepEqual(listener.shift(), [1, 1, Side.LEFT]);
+      assert.deepEqual(listener.shift(), [1000, 1000, Side.RIGHT]);
+    });
+
+    test('highlighting mouse out before delay', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words', 2);
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(listener.pending, 0);
+      clock.tick(100);
+      // Mouse out after 100ms but before hover delay.
+      dispatchMouseEvent(
+        'mouseout',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(listener.pending, 0);
+      clock.tick(HOVER_DELAY_MS - 100);
+      assert.equal(listener.pending, 0);
+      assert.equal(_testOnly_allTasks.size, 0);
+    });
+
+    test('triggers listener for applying and clearing highlighting', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words', 2);
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(tokenHighlightingCalls.length, 0);
+      clock.tick(HOVER_DELAY_MS);
+      assert.equal(tokenHighlightingCalls.length, 1);
+      assert.deepEqual(tokenHighlightingCalls[0].details, {
+        token: 'words',
+        side: Side.RIGHT,
+        element: words1,
+        range: {start_line: 1, start_column: 5, end_line: 1, end_column: 9},
+      });
+
+      MockInteractions.click(container);
+      assert.equal(tokenHighlightingCalls.length, 2);
+      assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
+    });
+
+    test('clicking clears highlight', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words', 2);
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(listener.pending, 0);
+      clock.tick(HOVER_DELAY_MS);
+      assert.equal(listener.pending, 2);
+      listener.flush();
+      assert.equal(listener.pending, 0);
+      MockInteractions.click(container);
+      assert.equal(listener.pending, 2);
+      assert.deepEqual(listener.shift(), [1, 1, Side.LEFT]);
+      assert.deepEqual(listener.shift(), [2, 2, Side.RIGHT]);
+    });
+
+    test('clicking on word does not clear highlight', async () => {
+      const clock = sinon.useFakeTimers();
+      const line1 = createLine('two words');
+      annotate(line1);
+      const line2 = createLine('three words', 2);
+      annotate(line2, Side.RIGHT, 2);
+      const words1 = queryAndAssert(line1, '.tk-words');
+      assert.isTrue(words1.classList.contains('token'));
+      dispatchMouseEvent(
+        'mouseover',
+        MockInteractions.middleOfNode(words1),
+        words1
+      );
+      assert.equal(listener.pending, 0);
+      clock.tick(HOVER_DELAY_MS);
+      assert.equal(listener.pending, 2);
+      listener.flush();
+      assert.equal(listener.pending, 0);
+      MockInteractions.click(words1);
+      assert.equal(listener.pending, 0);
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 3e63b0a..3b604eb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -19,7 +19,7 @@
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {listenOnce} from '../../../test/test-utils.js';
+import {listenOnce, mockPromise} from '../../../test/test-utils.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import {createDefaultDiffPrefs} from '../../../constants/constants.js';
 import {GrDiffCursor} from './gr-diff-cursor.js';
@@ -33,7 +33,7 @@
   let diffElement;
   let diff;
 
-  setup(done => {
+  setup(async () => {
     diffElement = basicFixture.instantiate();
     cursor = new GrDiffCursor();
 
@@ -48,17 +48,19 @@
       meta: {patchRange: undefined},
     };
     diffElement.path = 'some/path.ts';
+    const promise = mockPromise();
     const setupDone = () => {
       cursor._updateStops();
       cursor.moveToFirstChunk();
       diffElement.removeEventListener('render', setupDone);
-      done();
+      promise.resolve();
     };
     diffElement.addEventListener('render', setupDone);
 
     diff = getMockDiffResponse();
     diffElement.prefs = createDefaultDiffPrefs();
     diffElement.diff = diff;
+    await promise;
   });
 
   test('diff cursor functionality (side-by-side)', () => {
@@ -215,15 +217,17 @@
   });
 
   suite('unified diff', () => {
-    setup(done => {
+    setup(async () => {
+      const promise = mockPromise();
       // We must allow the diff to re-render after setting the viewMode.
       const renderHandler = function() {
         diffElement.removeEventListener('render', renderHandler);
         cursor.reInitCursor();
-        done();
+        promise.resolve();
       };
       diffElement.addEventListener('render', renderHandler);
       diffElement.viewMode = 'UNIFIED_DIFF';
+      await promise;
     });
 
     test('diff cursor functionality (unified)', () => {
@@ -312,11 +316,12 @@
   });
 
   suite('moved chunks without line range)', () => {
-    setup(done => {
+    setup(async () => {
+      const promise = mockPromise();
       const renderHandler = function() {
         diffElement.removeEventListener('render', renderHandler);
         cursor.reInitCursor();
-        done();
+        promise.resolve();
       };
       diffElement.addEventListener('render', renderHandler);
       diffElement.diff = {...diff, content: [
@@ -352,6 +357,7 @@
           ],
         },
       ]};
+      await promise;
     });
 
     test('renders moveControls with simple descriptions', () => {
@@ -363,11 +369,12 @@
   });
 
   suite('moved chunks (moveDetails)', () => {
-    setup(done => {
+    setup(async () => {
+      const promise = mockPromise();
       const renderHandler = function() {
         diffElement.removeEventListener('render', renderHandler);
         cursor.reInitCursor();
-        done();
+        promise.resolve();
       };
       diffElement.addEventListener('render', renderHandler);
       diffElement.diff = {...diff, content: [
@@ -403,6 +410,7 @@
           ],
         },
       ]};
+      await promise;
     });
 
     test('renders moveControls with simple descriptions', () => {
@@ -412,37 +420,41 @@
       assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
     });
 
-    test('startLineAnchor of movedIn chunk fires events', done => {
+    test('startLineAnchor of movedIn chunk fires events', async () => {
       const [movedIn] = diffElement.root
           .querySelectorAll('.dueToMove .moveControls');
       const [startLineAnchor] = movedIn.querySelectorAll('a');
 
+      const promise = mockPromise();
       const onMovedLinkClicked = e => {
         assert.deepEqual(e.detail, {lineNum: 4, side: 'left'});
-        done();
+        promise.resolve();
       };
       assert.equal(startLineAnchor.textContent, '4');
       startLineAnchor
           .addEventListener('moved-link-clicked', onMovedLinkClicked);
       MockInteractions.click(startLineAnchor);
+      await promise;
     });
 
-    test('endLineAnchor of movedOut fires events', done => {
+    test('endLineAnchor of movedOut fires events', async () => {
       const [, movedOut] = diffElement.root
           .querySelectorAll('.dueToMove .moveControls');
       const [, endLineAnchor] = movedOut.querySelectorAll('a');
 
+      const promise = mockPromise();
       const onMovedLinkClicked = e => {
         assert.deepEqual(e.detail, {lineNum: 4, side: 'right'});
-        done();
+        promise.resolve();
       };
       assert.equal(endLineAnchor.textContent, '4');
       endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
       MockInteractions.click(endLineAnchor);
+      await promise;
     });
   });
 
-  test('initialLineNumber not provided', done => {
+  test('initialLineNumber not provided', async () => {
     let scrollBehaviorDuringMove;
     const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
     const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk')
@@ -450,6 +462,7 @@
           scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
         });
 
+    const promise = mockPromise();
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
       cursor.reInitCursor();
@@ -457,19 +470,21 @@
       assert.isTrue(moveToChunkStub.called);
       assert.equal(scrollBehaviorDuringMove, 'never');
       assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
-      done();
+      promise.resolve();
     }
     diffElement.addEventListener('render', renderHandler);
     diffElement._diffChanged(getMockDiffResponse());
+    await promise;
   });
 
-  test('initialLineNumber provided', done => {
+  test('initialLineNumber provided', async () => {
     let scrollBehaviorDuringMove;
     const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber')
         .callsFake(() => {
           scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
         });
     const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
+    const promise = mockPromise();
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
       cursor.reInitCursor();
@@ -479,13 +494,14 @@
       assert.equal(moveToNumStub.lastCall.args[1], 'right');
       assert.equal(scrollBehaviorDuringMove, 'keep-visible');
       assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
-      done();
+      promise.resolve();
     }
     diffElement.addEventListener('render', renderHandler);
     cursor.initialLineNumber = 10;
     cursor.side = 'right';
 
     diffElement._diffChanged(getMockDiffResponse());
+    await promise;
   });
 
   test('getTargetDiffElement', () => {
@@ -502,31 +518,35 @@
       diffElement.loggedIn = true;
     });
 
-    test('adds new draft for selected line on the left', done => {
+    test('adds new draft for selected line on the left', async () => {
       cursor.moveToLineNumber(2, 'left');
+      const promise = mockPromise();
       diffElement.addEventListener('create-comment', e => {
         const {lineNum, range, side} = e.detail;
         assert.equal(lineNum, 2);
         assert.equal(range, undefined);
         assert.equal(side, 'left');
-        done();
+        promise.resolve();
       });
       cursor.createCommentInPlace();
+      await promise;
     });
 
-    test('adds draft for selected line on the right', done => {
+    test('adds draft for selected line on the right', async () => {
       cursor.moveToLineNumber(4, 'right');
+      const promise = mockPromise();
       diffElement.addEventListener('create-comment', e => {
         const {lineNum, range, side} = e.detail;
         assert.equal(lineNum, 4);
         assert.equal(range, undefined);
         assert.equal(side, 'right');
-        done();
+        promise.resolve();
       });
       cursor.createCommentInPlace();
+      await promise;
     });
 
-    test('creates comment for range if selected', done => {
+    test('creates comment for range if selected', async () => {
       const someRange = {
         start_line: 2,
         start_character: 3,
@@ -537,14 +557,16 @@
         side: 'right',
         range: someRange,
       };
+      const promise = mockPromise();
       diffElement.addEventListener('create-comment', e => {
         const {lineNum, range, side} = e.detail;
         assert.equal(lineNum, 6);
         assert.equal(range, someRange);
         assert.equal(side, 'right');
-        done();
+        promise.resolve();
       });
       cursor.createCommentInPlace();
+      await promise;
     });
 
     test('ignores call if nothing is selected', () => {
@@ -596,15 +618,13 @@
     assert.equal(cursor._findRowByNumberAndFile(5, 'left'), row);
   });
 
-  test('expand context updates stops', done => {
+  test('expand context updates stops', async () => {
     sinon.spy(cursor, '_updateStops');
     MockInteractions.tap(diffElement.shadowRoot
         .querySelector('gr-context-controls').shadowRoot
         .querySelector('.showContext'));
-    flush(() => {
-      assert.isTrue(cursor._updateStops.called);
-      done();
-    });
+    await flush();
+    assert.isTrue(cursor._updateStops.called);
   });
 
   test('updates stops when loading changes', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
index 7420dc8..5e81871 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.ts
@@ -29,6 +29,7 @@
    *
    */
   getLength(node: Node) {
+    if (node instanceof Comment) return 0;
     return this.getStringLength(node.textContent || '');
   },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js
index 65f5e07..d8295a5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.js
@@ -252,6 +252,24 @@
           '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789');
     });
 
+    test('handles comment nodes', () => {
+      const container = document.createElement('div');
+      container.appendChild(document.createComment('comment1'));
+      container.appendChild(document.createTextNode('0123456789'));
+      container.appendChild(document.createComment('comment2'));
+      container.appendChild(document.createElement('span'));
+      container.appendChild(document.createTextNode('0123456789'));
+      GrAnnotation.annotateWithElement(
+          container, 1, 10, {tagName: 'test-wrapper'});
+
+      assert.equal(
+          container.innerHTML,
+          '<!--comment1-->' +
+          '0<test-wrapper>123456789' +
+          '<!--comment2-->' +
+          '<span></span>0</test-wrapper>123456789');
+    });
+
     test('sets sanitized attributes', () => {
       const container = document.createElement('div');
       container.textContent = fullText;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js
index 18fbe9a..4c1295f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.js
@@ -251,7 +251,7 @@
     };
 
     const emulateSelection = (startNode, startOffset, endNode, endOffset) => {
-      const selection = window.getSelection();
+      const selection = document.getSelection();
       const range = document.createRange();
       range.setStart(startNode, startOffset);
       range.setEnd(endNode, endOffset);
@@ -281,7 +281,7 @@
 
     teardown(() => {
       contentStubs = null;
-      window.getSelection().removeAllRanges();
+      document.getSelection().removeAllRanges();
     });
 
     test('single first line', () => {
@@ -389,7 +389,7 @@
     test('collapsed', () => {
       const content = stubContent(138, 'left');
       emulateSelection(content.firstChild, 5, content.firstChild, 5);
-      assert.isOk(window.getSelection().getRangeAt(0).startContainer);
+      assert.isOk(document.getSelection().getRangeAt(0).startContainer);
       assert.isFalse(!!element.selectedRange);
     });
 
@@ -556,7 +556,7 @@
           content.querySelectorAll('hl')[3], 0,
           content.querySelectorAll('span')[1], 0);
       const spyCall = spy.getCall(0);
-      const range = window.getSelection().getRangeAt(0);
+      const range = document.getSelection().getRangeAt(0);
       assert.notDeepEqual(spyCall.returnValue, range);
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 06d31b3..90d6218 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -50,7 +50,6 @@
   Base64ImageFile,
   BlameInfo,
   ChangeInfo,
-  CommentRange,
   EditPatchSetNum,
   NumericChangeId,
   ParentPatchSetNum,
@@ -93,7 +92,7 @@
 import {Subject} from 'rxjs';
 import {RenderPreferences} from '../../../api/diff';
 
-const MSG_EMPTY_BLAME = 'No blame information for this diff.';
+const EMPTY_BLAME = 'No blame information for this diff.';
 
 const EVENT_AGAINST_PARENT = 'diff-against-parent';
 const EVENT_ZERO_REBASE = 'rebase-percent-zero';
@@ -328,18 +327,21 @@
     super.disconnectedCallback();
   }
 
-  initLayers() {
-    return getPluginLoader()
-      .awaitPluginsLoaded()
-      .then(() => {
-        assertIsDefined(this.path, 'path');
-        this._layers = this._getLayers(this.path);
-        this._coverageRanges = [];
-        // We kick off fetching the data here, but we don't return the promise,
-        // so awaiting initLayers() will not wait for coverage data to be
-        // completely loaded.
-        this._getCoverageData();
-      });
+  async initLayers() {
+    const preferencesPromise = appContext.restApiService.getPreferences();
+    await getPluginLoader().awaitPluginsLoaded();
+    const prefs = await preferencesPromise;
+    const enableTokenHighlight =
+      appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
+      !prefs?.disable_token_highlighting;
+
+    assertIsDefined(this.path, 'path');
+    this._layers = this.getLayers(this.path, enableTokenHighlight);
+    this._coverageRanges = [];
+    // We kick off fetching the data here, but we don't return the promise,
+    // so awaiting initLayers() will not wait for coverage data to be
+    // completely loaded.
+    this._getCoverageData();
   }
 
   diffChanged(diff?: DiffInfo) {
@@ -411,12 +413,10 @@
     }
   }
 
-  private _getLayers(path: string): DiffLayer[] {
+  private getLayers(path: string, enableTokenHighlight: boolean): DiffLayer[] {
     const layers = [];
-    if (
-      appContext.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)
-    ) {
-      layers.push(new TokenHighlightLayer());
+    if (enableTokenHighlight) {
+      layers.push(new TokenHighlightLayer(this));
     }
     layers.push(this.syntaxLayer);
     // Get layers from plugins (if any).
@@ -569,8 +569,8 @@
       .getBlame(this.changeNum, this.patchRange.patchNum, this.path, true)
       .then(blame => {
         if (!blame || !blame.length) {
-          fireAlert(this, MSG_EMPTY_BLAME);
-          return Promise.reject(MSG_EMPTY_BLAME);
+          fireAlert(this, EMPTY_BLAME);
+          return Promise.reject(EMPTY_BLAME);
         }
 
         this._blame = blame;
@@ -730,14 +730,15 @@
   }
 
   _threadsChanged(threads: CommentThread[]) {
-    // Currently, the only way this is ever changed here is when the initial
-    // threads are loaded, so it's okay performance wise to clear the threads
-    // and recreate them. If this changes in future, we might want to reuse
-    // some DOM nodes here.
-    this._clearThreads();
+    const threadEls = new Set<Object>();
     for (const thread of threads) {
-      const threadEl = this._createThreadElement(thread);
-      this._attachThreadElement(threadEl);
+      threadEls.add(this._getOrCreateThread(thread));
+    }
+    // Remove all threads that are no longer existing.
+    for (const threadEl of this.getThreadEls()) {
+      if (threadEls.has(threadEl)) continue;
+      const parent = threadEl.parentNode;
+      if (parent) parent.removeChild(threadEl);
     }
     const portedThreadsCount = threads.filter(thread => thread.ported).length;
     const portedThreadsWithoutRange = threads.filter(
@@ -786,14 +787,15 @@
         ? CommentSide.PARENT
         : CommentSide.REVISION;
     if (!this.canCommentOnPatchSetNum(patchNum)) return;
-    const threadEl = this._getOrCreateThread(
-      patchNum,
-      lineNum,
-      side,
-      commentSide,
+    const threadEl = this._getOrCreateThread({
+      comments: [],
       path,
-      range
-    );
+      diffSide: side,
+      commentSide,
+      patchNum,
+      line: lineNum,
+      range,
+    });
     threadEl.addOrEditDraft(lineNum, range);
 
     this.reporting.recordDraftInteraction();
@@ -829,26 +831,13 @@
    * Gets or creates a comment thread at a given location.
    * May provide a range, to get/create a range comment.
    */
-  _getOrCreateThread(
-    patchNum: PatchSetNum,
-    lineNum: LineNumber | undefined,
-    diffSide: Side,
-    commentSide: CommentSide,
-    path: string,
-    range?: CommentRange
-  ): GrCommentThread {
-    let threadEl = this._getThreadEl(lineNum, diffSide, range);
+  _getOrCreateThread(thread: CommentThread): GrCommentThread {
+    let threadEl = this._getThreadEl(thread);
     if (!threadEl) {
-      threadEl = this._createThreadElement({
-        comments: [],
-        path,
-        diffSide,
-        commentSide,
-        patchNum,
-        line: lineNum,
-        range,
-      });
+      threadEl = this._createThreadElement(thread);
       this._attachThreadElement(threadEl);
+    } else {
+      this._updateThreadElement(threadEl, thread);
     }
     return threadEl;
   }
@@ -871,6 +860,11 @@
       'slot',
       `${thread.diffSide}-${thread.line || 'LOST'}`
     );
+    this._updateThreadElement(threadEl, thread);
+    return threadEl;
+  }
+
+  _updateThreadElement(threadEl: GrCommentThread, thread: CommentThread) {
     threadEl.comments = thread.comments;
     threadEl.diffSide = thread.diffSide;
     threadEl.isOnParent = thread.commentSide === CommentSide.PARENT;
@@ -896,34 +890,29 @@
     else threadEl.lineNum = thread.line !== 'FILE' ? thread.line : undefined;
     threadEl.projectName = this.projectName;
     threadEl.range = thread.range;
-    return threadEl;
   }
 
   /**
    * Gets a comment thread element at a given location.
    * May provide a range, to get a range comment.
    */
-  _getThreadEl(
-    lineNum: LineNumber | undefined,
-    commentSide: Side,
-    range?: CommentRange
-  ): GrCommentThread | null {
+  _getThreadEl(thread: CommentThread): GrCommentThread | null {
     let line: LineInfo;
-    if (commentSide === Side.LEFT) {
-      line = {beforeNumber: lineNum};
-    } else if (commentSide === Side.RIGHT) {
-      line = {afterNumber: lineNum};
+    if (thread.diffSide === Side.LEFT) {
+      line = {beforeNumber: thread.line};
+    } else if (thread.diffSide === Side.RIGHT) {
+      line = {afterNumber: thread.line};
     } else {
-      throw new Error(`Unknown side: ${commentSide}`);
+      throw new Error(`Unknown side: ${thread.diffSide}`);
     }
     function matchesRange(threadEl: GrCommentThread) {
-      return rangesEqual(getRange(threadEl), range);
+      return rangesEqual(getRange(threadEl), thread.range);
     }
 
     const filteredThreadEls = this._filterThreadElsForLocation(
       this.getThreadEls(),
       line,
-      commentSide
+      thread.diffSide
     ).filter(matchesRange);
     return filteredThreadEls.length ? filteredThreadEls[0] : null;
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 42aa160..344f9d8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -23,7 +23,11 @@
 import {Side, createDefaultDiffPrefs} from '../../../constants/constants.js';
 import {createChange} from '../../../test/test-data-generators.js';
 import {CoverageType} from '../../../types/types.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
+import {
+  addListenerForTest,
+  mockPromise,
+  stubRestApi,
+} from '../../../test/test-utils.js';
 import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
 import {_testOnly_resetState} from '../../../services/comments/comments-model.js';
 
@@ -64,14 +68,13 @@
   });
 
   suite('render reporting', () => {
-    test('starts total and content timer on render-start', done => {
+    test('starts total and content timer on render-start', () => {
       element.dispatchEvent(
           new CustomEvent('render-start', {bubbles: true, composed: true}));
       assert.isTrue(element.reporting.time.calledWithExactly(
           'Diff Total Render'));
       assert.isTrue(element.reporting.time.calledWithExactly(
           'Diff Content Render'));
-      done();
     });
 
     test('ends content timer on render-content', () => {
@@ -215,25 +218,23 @@
     });
   });
 
-  test('prefetch getDiff', done => {
+  test('prefetch getDiff', async () => {
     const diffRestApiStub = stubRestApi('getDiff')
         .returns(Promise.resolve({content: []}));
     element.changeNum = 123;
     element.patchRange = {basePatchNum: 1, patchNum: 2};
     element.path = 'file.txt';
     element.prefetchDiff();
-    element._getDiff().then(() =>{
-      assert.isTrue(diffRestApiStub.calledOnce);
-      done();
-    });
+    await element._getDiff();
+    assert.isTrue(diffRestApiStub.calledOnce);
   });
 
-  test('_getDiff handles null diff responses', done => {
+  test('_getDiff handles null diff responses', async () => {
     stubRestApi('getDiff').returns(Promise.resolve(null));
     element.changeNum = 123;
     element.patchRange = {basePatchNum: 1, patchNum: 2};
     element.path = 'file.txt';
-    element._getDiff().then(done);
+    await element._getDiff();
   });
 
   test('reload resolves on error', () => {
@@ -311,7 +312,7 @@
       };
     });
 
-    test('renders image diffs with same file name', done => {
+    test('renders image diffs with same file name', async () => {
       const mockDiff = {
         meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
         meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
@@ -342,6 +343,7 @@
         },
       }));
 
+      const promise = mockPromise();
       const rendered = () => {
         // Recognizes that it should be an image diff.
         assert.isTrue(element.isImageDiff);
@@ -377,7 +379,7 @@
           leftLoaded = true;
           if (rightLoaded) {
             element.removeEventListener('render', rendered);
-            done();
+            promise.resolve();
           }
         });
 
@@ -390,7 +392,7 @@
           rightLoaded = true;
           if (leftLoaded) {
             element.removeEventListener('render', rendered);
-            done();
+            promise.resolve();
           }
         });
       };
@@ -398,9 +400,10 @@
       element.addEventListener('render', rendered);
       element.prefs = createDefaultDiffPrefs();
       element.reload();
+      await promise;
     });
 
-    test('renders image diffs with a different file name', done => {
+    test('renders image diffs with a different file name', async () => {
       const mockDiff = {
         meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
         meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
@@ -431,6 +434,7 @@
         },
       }));
 
+      const promise = mockPromise();
       const rendered = () => {
         // Recognizes that it should be an image diff.
         assert.isTrue(element.isImageDiff);
@@ -468,7 +472,7 @@
           leftLoaded = true;
           if (rightLoaded) {
             element.removeEventListener('render', rendered);
-            done();
+            promise.resolve();
           }
         });
 
@@ -481,7 +485,7 @@
           rightLoaded = true;
           if (leftLoaded) {
             element.removeEventListener('render', rendered);
-            done();
+            promise.resolve();
           }
         });
       };
@@ -489,9 +493,10 @@
       element.addEventListener('render', rendered);
       element.prefs = createDefaultDiffPrefs();
       element.reload();
+      await promise;
     });
 
-    test('renders added image', done => {
+    test('renders added image', async () => {
       const mockDiff = {
         meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
           lines: 560},
@@ -517,6 +522,7 @@
         },
       }));
 
+      const promise = mockPromise();
       element.addEventListener('render', () => {
         // Recognizes that it should be an image diff.
         assert.isTrue(element.isImageDiff);
@@ -530,14 +536,15 @@
 
         assert.isNotOk(leftImage);
         assert.isOk(rightImage);
-        done();
+        promise.resolve();
       });
 
       element.prefs = createDefaultDiffPrefs();
       element.reload();
+      await promise;
     });
 
-    test('renders removed image', done => {
+    test('renders removed image', async () => {
       const mockDiff = {
         meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
           lines: 560},
@@ -563,6 +570,7 @@
         revisionImage: null,
       }));
 
+      const promise = mockPromise();
       element.addEventListener('render', () => {
         // Recognizes that it should be an image diff.
         assert.isTrue(element.isImageDiff);
@@ -576,14 +584,15 @@
 
         assert.isOk(leftImage);
         assert.isNotOk(rightImage);
-        done();
+        promise.resolve();
       });
 
       element.prefs = createDefaultDiffPrefs();
       element.reload();
+      await promise;
     });
 
-    test('does not render disallowed image type', done => {
+    test('does not render disallowed image type', async () => {
       const mockDiff = {
         meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
           lines: 560},
@@ -611,6 +620,7 @@
         revisionImage: null,
       }));
 
+      const promise = mockPromise();
       element.addEventListener('render', () => {
         // Recognizes that it should be an image diff.
         assert.isTrue(element.isImageDiff);
@@ -619,11 +629,12 @@
         const leftImage =
             element.$.diff.$.diffTable.querySelector('td.left img');
         assert.isNotOk(leftImage);
-        done();
+        promise.resolve();
       });
 
       element.prefs = createDefaultDiffPrefs();
       element.reload();
+      await promise;
     });
   });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index cf825ff..496d6bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -28,18 +28,10 @@
 import {GrLibLoader} from '../../shared/gr-lib-loader/gr-lib-loader';
 import {RESEMBLEJS_LIBRARY_CONFIG} from '../../shared/gr-lib-loader/resemblejs_config';
 
-import {
-  css,
-  customElement,
-  html,
-  LitElement,
-  property,
-  PropertyValues,
-  query,
-  state,
-} from 'lit-element';
-import {classMap} from 'lit-html/directives/class-map';
-import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {classMap} from 'lit/directives/class-map';
+import {StyleInfo, styleMap} from 'lit/directives/style-map';
 
 import {
   createEvent,
@@ -437,6 +429,11 @@
           id="highlight-image"
           style="${styleMap({
             opacity: this.showHighlight ? '1' : '0',
+            // When the highlight layer is not being shown, saving the image or
+            // opening it in a new tab from the context menu, e.g. for external
+            // comparison, should give back the source image, not the highlight
+            // layer.
+            'pointer-events': this.showHighlight ? 'auto' : 'none',
           })}"
           src="${this.diffHighlightSrc}"
         />
@@ -582,6 +579,7 @@
 
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         paper-item {
@@ -651,7 +649,9 @@
   // We don't want property changes in updateSizes() to trigger infinite update
   // loops, so we perform this in update() instead of updated().
   override update(changedProperties: PropertyValues) {
+    // eslint-disable-next-line lit/no-property-change-update
     if (!this.baseUrl) this.baseSelected = false;
+    // eslint-disable-next-line lit/no-property-change-update
     if (!this.revisionUrl) this.baseSelected = true;
     this.updateSizes();
     super.update(changedProperties);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
index b1ea72f..28e6d82 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-overview-image.ts
@@ -14,17 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {
-  css,
-  customElement,
-  html,
-  LitElement,
-  property,
-  PropertyValues,
-  query,
-  state,
-} from 'lit-element';
-import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {StyleInfo, styleMap} from 'lit/directives/style-map';
 import {ImageDiffAction} from '../../../api/diff';
 
 import {createEvent, Dimensions, fitToFrame, Point, Rect} from './util';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
index 671b858..66d4671 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-zoomed-image.ts
@@ -14,16 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {
-  css,
-  customElement,
-  html,
-  LitElement,
-  property,
-  PropertyValues,
-  state,
-} from 'lit-element';
-import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {StyleInfo, styleMap} from 'lit/directives/style-map';
 import {Rect} from './util';
 
 /**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
index 4e2b6a1..8a6d95d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
@@ -30,28 +30,34 @@
       width: 1.3rem;
     }
   </style>
-  <gr-button
-    id="sideBySideBtn"
-    link=""
+  <gr-tooltip-content
     has-tooltip=""
-    position-below="[[showTooltipBelow]]"
-    class$="[[_computeSideBySideSelected(mode)]]"
     title="Side-by-side diff"
-    aria-pressed$="[[isSideBySideSelected(mode)]]"
-    on-click="_handleSideBySideTap"
+    position-below="[[showTooltipBelow]]"
   >
-    <iron-icon icon="gr-icons:side-by-side"></iron-icon>
-  </gr-button>
-  <gr-button
-    id="unifiedBtn"
-    link=""
+    <gr-button
+      id="sideBySideBtn"
+      link=""
+      class$="[[_computeSideBySideSelected(mode)]]"
+      aria-pressed$="[[isSideBySideSelected(mode)]]"
+      on-click="_handleSideBySideTap"
+    >
+      <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+    </gr-button>
+  </gr-tooltip-content>
+  <gr-tooltip-content
     has-tooltip=""
     position-below="[[showTooltipBelow]]"
     title="Unified diff"
-    class$="[[_computeUnifiedSelected(mode)]]"
-    aria-pressed$="[[isUnifiedSelected(mode)]]"
-    on-click="_handleUnifiedTap"
   >
-    <iron-icon icon="gr-icons:unified"></iron-icon>
-  </gr-button>
+    <gr-button
+      id="unifiedBtn"
+      link=""
+      class$="[[_computeUnifiedSelected(mode)]]"
+      aria-pressed$="[[isUnifiedSelected(mode)]]"
+      on-click="_handleUnifiedTap"
+    >
+      <iron-icon icon="gr-icons:unified"></iron-icon>
+    </gr-button>
+  </gr-tooltip-content>
 `;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts
index 787fe30..85edc12 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts
@@ -49,12 +49,12 @@
   </style>
   <gr-overlay id="diffPrefsOverlay" with-backdrop="">
     <div role="dialog" aria-labelledby="diffPreferencesTitle">
-      <h1
-        class$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]"
+      <h3
+        class$="heading-3 diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]"
         id="diffPreferencesTitle"
       >
         Diff Preferences
-      </h1>
+      </h3>
       <gr-diff-preferences
         id="diffPreferences"
         diff-prefs="{{_editableDiffPrefs}}"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts
index 0051b8b..2665ef0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.ts
@@ -195,7 +195,7 @@
 
   _getSelection() {
     const diffHosts = querySelectorAll(document.body, 'gr-diff');
-    if (!diffHosts.length) return window.getSelection();
+    if (!diffHosts.length) return document.getSelection();
 
     const curDiffHost = diffHosts.find(diffHost => {
       if (!diffHost?.shadowRoot?.getSelection) return false;
@@ -205,9 +205,9 @@
       return selection && selection.type !== 'None';
     });
 
-    return curDiffHost
-      ? curDiffHost.shadowRoot!.getSelection()
-      : window.getSelection();
+    return curDiffHost?.shadowRoot?.getSelection
+      ? curDiffHost.shadowRoot.getSelection()
+      : document.getSelection();
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js
index 8d7264c..15454f9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.js
@@ -247,7 +247,7 @@
     element.classList.add('selected-left');
     element.classList.remove('selected-right');
 
-    const selection = window.getSelection();
+    const selection = document.getSelection();
     selection.removeAllRanges();
     const range = document.createRange();
     range.setStart(element.querySelector('div.contentText').firstChild, 3);
@@ -261,7 +261,7 @@
     element.classList.add('selected-left');
     element.classList.add('selected-comment');
     element.classList.remove('selected-right');
-    const selection = window.getSelection();
+    const selection = document.getSelection();
     selection.removeAllRanges();
     const range = document.createRange();
     range.setStart(
@@ -277,7 +277,7 @@
     element.classList.add('selected-left');
     element.classList.add('selected-comment');
     element.classList.remove('selected-right');
-    const selection = window.getSelection();
+    const selection = document.getSelection();
     selection.removeAllRanges();
     const range = document.createRange();
     const nodes = element.querySelectorAll('.gr-formatted-text *');
@@ -307,7 +307,7 @@
     element.classList.add('selected-right');
     element.classList.remove('selected-left');
 
-    const selection = window.getSelection();
+    const selection = document.getSelection();
     selection.removeAllRanges();
     const range = document.createRange();
     range.setStart(
@@ -329,7 +329,7 @@
     };
     element.classList.add('selected-left');
     element.classList.remove('selected-right');
-    const selection = window.getSelection();
+    const selection = document.getSelection();
     selection.removeAllRanges();
     const range = document.createRange();
     range.setStart(element.querySelector('div.contentText').firstChild, 3);
@@ -348,7 +348,7 @@
       element.classList.add('selected-left');
       element.classList.add('selected-comment');
       element.classList.remove('selected-right');
-      selection = window.getSelection();
+      selection = document.getSelection();
       selection.removeAllRanges();
       range = document.createRange();
       nodes = element.querySelectorAll('.gr-formatted-text *');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 61259d1..bb5ce94 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -16,6 +16,7 @@
  */
 import '@polymer/iron-dropdown/iron-dropdown';
 import '@polymer/iron-input/iron-input';
+import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-dropdown/gr-dropdown';
@@ -112,8 +113,8 @@
 import {Subject} from 'rxjs';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
-const MSG_LOADING_BLAME = 'Loading blame...';
-const MSG_LOADED_BLAME = 'Blame loaded';
+const LOADING_BLAME = 'Loading blame...';
+const LOADED_BLAME = 'Blame loaded';
 
 // Time in which pressing n key again after the toast navigates to next file
 const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
@@ -140,8 +141,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-diff-view')
-export class GrDiffView extends KeyboardShortcutMixin(PolymerElement) {
+export class GrDiffView extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -1664,12 +1668,12 @@
 
   _loadBlame() {
     this._isBlameLoading = true;
-    fireAlert(this, MSG_LOADING_BLAME);
+    fireAlert(this, LOADING_BLAME);
     this.$.diffHost
       .loadBlame()
       .then(() => {
         this._isBlameLoading = false;
-        fireAlert(this, MSG_LOADED_BLAME);
+        fireAlert(this, LOADED_BLAME);
       })
       .catch(() => {
         this._isBlameLoading = false;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 7e0ca10..4f1047f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       display: block;
@@ -351,15 +354,15 @@
           hidden=""
         >
           <span class="preferences desktop">
-            <gr-button
-              link=""
-              class="prefsButton"
+            <gr-tooltip-content
               has-tooltip=""
               position-below=""
               title="Diff preferences"
-              on-click="_handlePrefsTap"
-              ><iron-icon icon="gr-icons:settings"></iron-icon
-            ></gr-button>
+            >
+              <gr-button link="" class="prefsButton" on-click="_handlePrefsTap"
+                ><iron-icon icon="gr-icons:settings"></iron-icon
+              ></gr-button>
+            </gr-tooltip-content>
           </span>
         </span>
         <gr-endpoint-decorator name="annotation-toggler">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 033b886..a3de30a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -29,7 +29,6 @@
   createComment,
 } from '../../../test/test-data-generators.js';
 import {EditPatchSetNum} from '../../../types/common.js';
-import sinon from 'sinon/pkg/sinon-esm';
 import {CursorMoveResult} from '../../../api/core.js';
 
 const basicFixture = fixtureFromElement('gr-diff-view');
@@ -678,23 +677,21 @@
       assert.isNotOk(args[3]);
     });
 
-    test('A fires an error event when not logged in', done => {
+    test('A fires an error event when not logged in', async () => {
       const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
       sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
-      flush(() => {
-        assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
-          'should only work when the user is logged in.');
-        assert.isNull(window.sessionStorage.getItem(
-            'changeView.showReplyDialog'));
-        assert.isTrue(loggedInErrorSpy.called);
-        done();
-      });
+      await flush();
+      assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' +
+        'should only work when the user is logged in.');
+      assert.isNull(window.sessionStorage.getItem(
+          'changeView.showReplyDialog'));
+      assert.isTrue(loggedInErrorSpy.called);
     });
 
-    test('A navigates to change with logged in', done => {
+    test('A navigates to change with logged in', async () => {
       element._changeNum = '42';
       element._patchRange = {
         basePatchNum: 5,
@@ -712,41 +709,38 @@
       const loggedInErrorSpy = sinon.spy();
       element.addEventListener('show-auth-required', loggedInErrorSpy);
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
-      flush(() => {
-        assert.isTrue(element.changeViewState.showReplyDialog);
-        assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
-            5), 'Should navigate to /c/42/5..10');
-        assert.isFalse(loggedInErrorSpy.called);
-        done();
-      });
+      await flush();
+      assert.isTrue(element.changeViewState.showReplyDialog);
+      assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
+          5), 'Should navigate to /c/42/5..10');
+      assert.isFalse(loggedInErrorSpy.called);
     });
 
-    test('A navigates to change with old patch number with logged in', done => {
-      element._changeNum = '42';
-      element._patchRange = {
-        basePatchNum: PARENT,
-        patchNum: 1,
-      };
-      element._change = {
-        _number: 42,
-        revisions: {
-          a: {_number: 1, commit: {parents: []}},
-          b: {_number: 2, commit: {parents: []}},
-        },
-      };
-      const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
-      sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-      const loggedInErrorSpy = sinon.spy();
-      element.addEventListener('show-auth-required', loggedInErrorSpy);
-      MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
-      flush(() => {
-        assert.isTrue(element.changeViewState.showReplyDialog);
-        assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
-            PARENT), 'Should navigate to /c/42/1');
-        assert.isFalse(loggedInErrorSpy.called);
-        done();
-      });
-    });
+    test('A navigates to change with old patch number with logged in',
+        async () => {
+          element._changeNum = '42';
+          element._patchRange = {
+            basePatchNum: PARENT,
+            patchNum: 1,
+          };
+          element._change = {
+            _number: 42,
+            revisions: {
+              a: {_number: 1, commit: {parents: []}},
+              b: {_number: 2, commit: {parents: []}},
+            },
+          };
+          const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
+          sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+          const loggedInErrorSpy = sinon.spy();
+          element.addEventListener('show-auth-required', loggedInErrorSpy);
+          MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
+          await flush();
+          assert.isTrue(element.changeViewState.showReplyDialog);
+          assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
+              PARENT), 'Should navigate to /c/42/1');
+          assert.isFalse(loggedInErrorSpy.called);
+        });
 
     test('keyboard shortcuts with patch range', () => {
       element._changeNum = '42';
@@ -860,7 +854,7 @@
       assert.isTrue(changeNavStub.calledOnce);
     });
 
-    test('edit should redirect to edit page', done => {
+    test('edit should redirect to edit page', async () => {
       element._loggedIn = true;
       element._path = 't.txt';
       element._patchRange = {
@@ -877,23 +871,21 @@
         },
       };
       const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
-      flush(() => {
-        const editBtn = element.shadowRoot
-            .querySelector('.editButton gr-button');
-        assert.isTrue(!!editBtn);
-        MockInteractions.tap(editBtn);
-        assert.isTrue(redirectStub.called);
-        assert.isTrue(redirectStub.lastCall.calledWithExactly(
-            GerritNav.getEditUrlForDiff(
-                element._change,
-                element._path,
-                element._patchRange.patchNum
-            )));
-        done();
-      });
+      await flush();
+      const editBtn = element.shadowRoot
+          .querySelector('.editButton gr-button');
+      assert.isTrue(!!editBtn);
+      MockInteractions.tap(editBtn);
+      assert.isTrue(redirectStub.called);
+      assert.isTrue(redirectStub.lastCall.calledWithExactly(
+          GerritNav.getEditUrlForDiff(
+              element._change,
+              element._path,
+              element._patchRange.patchNum
+          )));
     });
 
-    test('edit should redirect to edit page with line number', done => {
+    test('edit should redirect to edit page with line number', async () => {
       const lineNumber = 42;
       element._loggedIn = true;
       element._path = 't.txt';
@@ -913,21 +905,19 @@
       sinon.stub(element.cursor, 'getAddress')
           .returns({number: lineNumber, isLeftSide: false});
       const redirectStub = sinon.stub(GerritNav, 'navigateToRelativeUrl');
-      flush(() => {
-        const editBtn = element.shadowRoot
-            .querySelector('.editButton gr-button');
-        assert.isTrue(!!editBtn);
-        MockInteractions.tap(editBtn);
-        assert.isTrue(redirectStub.called);
-        assert.isTrue(redirectStub.lastCall.calledWithExactly(
-            GerritNav.getEditUrlForDiff(
-                element._change,
-                element._path,
-                element._patchRange.patchNum,
-                lineNumber
-            )));
-        done();
-      });
+      await flush();
+      const editBtn = element.shadowRoot
+          .querySelector('.editButton gr-button');
+      assert.isTrue(!!editBtn);
+      MockInteractions.tap(editBtn);
+      assert.isTrue(redirectStub.called);
+      assert.isTrue(redirectStub.lastCall.calledWithExactly(
+          GerritNav.getEditUrlForDiff(
+              element._change,
+              element._path,
+              element._patchRange.patchNum,
+              lineNumber
+          )));
     });
 
     function isEditVisibile({loggedIn, changeStatus}) {
@@ -1294,7 +1284,7 @@
       assert.isFalse(saveReviewedStub.called);
     });
 
-    test('hash is determined from params', done => {
+    test('hash is determined from params', async () => {
       sinon.stub(element.$.diffHost, 'reload');
       sinon.stub(element, '_initLineOfInterestAndCursor');
 
@@ -1308,10 +1298,8 @@
         hash: 10,
       };
 
-      flush(() => {
-        assert.isTrue(element._initLineOfInterestAndCursor.calledOnce);
-        done();
-      });
+      await flush();
+      assert.isTrue(element._initLineOfInterestAndCursor.calledOnce);
     });
 
     test('diff mode selector correctly toggles the diff', () => {
@@ -1360,15 +1348,13 @@
       assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
     });
 
-    test('diff mode selector should be hidden for binary', done => {
+    test('diff mode selector should be hidden for binary', async () => {
       element._diff = {binary: true, content: []};
 
-      flush(() => {
-        const diffModeSelector = element.shadowRoot
-            .querySelector('.diffModeSelector');
-        assert.isTrue(diffModeSelector.classList.contains('hide'));
-        done();
-      });
+      await flush();
+      const diffModeSelector = element.shadowRoot
+          .querySelector('.diffModeSelector');
+      assert.isTrue(diffModeSelector.classList.contains('hide'));
     });
 
     suite('_commitRange', () => {
@@ -1400,7 +1386,7 @@
             change));
       });
 
-      test('uses the patchNum and basePatchNum ', done => {
+      test('uses the patchNum and basePatchNum ', async () => {
         element.params = {
           view: GerritNav.View.DIFF,
           changeNum: '42',
@@ -1409,16 +1395,14 @@
           path: '/COMMIT_MSG',
         };
         element._change = change;
-        flush(() => {
-          assert.deepEqual(element._commitRange, {
-            baseCommit: 'commit-sha-2',
-            commit: 'commit-sha-4',
-          });
-          done();
+        await flush();
+        assert.deepEqual(element._commitRange, {
+          baseCommit: 'commit-sha-2',
+          commit: 'commit-sha-4',
         });
       });
 
-      test('uses the parent when there is no base patch num ', done => {
+      test('uses the parent when there is no base patch num ', async () => {
         element.params = {
           view: GerritNav.View.DIFF,
           changeNum: '42',
@@ -1426,12 +1410,10 @@
           path: '/COMMIT_MSG',
         };
         element._change = change;
-        flush(() => {
-          assert.deepEqual(element._commitRange, {
-            commit: 'commit-sha-5',
-            baseCommit: 'sha-5-parent',
-          });
-          done();
+        await flush();
+        assert.deepEqual(element._commitRange, {
+          commit: 'commit-sha-5',
+          baseCommit: 'sha-5-parent',
         });
       });
     });
@@ -1921,7 +1903,7 @@
       ]);
     });
 
-    test('File change should trigger navigateToDiff once', done => {
+    test('File change should trigger navigateToDiff once', async () => {
       element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
       sinon.stub(element, '_initLineOfInterestAndCursor');
       sinon.stub(GerritNav, 'navigateToDiff');
@@ -1942,7 +1924,7 @@
         ...createChange(),
         revisions: createRevisions(1),
       };
-      flush();
+      await flush();
       assert.isTrue(GerritNav.navigateToDiff.notCalled);
 
       // Switch to file2
@@ -1964,7 +1946,6 @@
 
       // No extra call
       assert.isTrue(GerritNav.navigateToDiff.calledOnce);
-      done();
     });
 
     test('_computeDownloadDropdownLinks', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
index fada9cb..7393606 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
@@ -129,6 +129,29 @@
   rootId: string;
 }
 
+const VISIBLE_TEXT_NODE_TYPES = [Node.TEXT_NODE, Node.ELEMENT_NODE];
+
+export function getPreviousContentNodes(node?: Node | null) {
+  const sibs = [];
+  while (node) {
+    const {parentNode, previousSibling} = node;
+    const topContentLevel =
+      parentNode &&
+      (parentNode as HTMLElement).classList.contains('contentText');
+    let previousEl: Node | undefined | null;
+    if (previousSibling) {
+      previousEl = previousSibling;
+    } else if (!topContentLevel) {
+      previousEl = parentNode?.previousSibling;
+    }
+    if (previousEl && VISIBLE_TEXT_NODE_TYPES.includes(previousEl.nodeType)) {
+      sibs.push(previousEl);
+    }
+    node = previousEl;
+  }
+  return sibs;
+}
+
 export function isThreadEl(node: Node): node is GrDiffThreadElement {
   return (
     node.nodeType === Node.ELEMENT_NODE &&
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 9fb2a19..7efd2f8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -85,6 +85,7 @@
 import {
   DiffContextExpandedEventDetail,
   getResponsiveMode,
+  isResponsive,
 } from '../gr-diff-builder/gr-diff-builder';
 
 const NO_NEWLINE_BASE = 'No newline at end of base file.';
@@ -754,8 +755,7 @@
     const stylesToUpdate: {[key: string]: string} = {};
 
     const responsiveMode = getResponsiveMode(prefs, renderPrefs);
-    const responsive =
-      responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY';
+    const responsive = isResponsive(responsiveMode);
     this._diffTableClass = responsive ? 'responsive' : '';
     const lineLimit = `${lineLength}ch`;
     stylesToUpdate['--line-limit-marker'] =
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index 79ef38a..83b0aad 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -69,10 +69,9 @@
     /* Provides the option to add side borders (left and right) to the line number column. */
     td.left,
     td.right,
+    td.moveControlsLineNumCol,
     td.contextLineNum {
-      border-width: var(--line-number-border-width, 0);
-      border-style: solid;
-      border-color: var(--line-number-border-color, var(--border-color, unset));
+      box-shadow: var(--line-number-box-shadow, unset);
     }
 
     /*
@@ -112,6 +111,7 @@
       width: 100%;
       height: 100%;
       background-color: var(--diff-blank-background-color);
+      box-shadow: var(--line-number-box-shadow, unset);
     }
     td.lineNum {
       vertical-align: top;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index c59ceb7..9ee779c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -23,7 +23,7 @@
 import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
 import {runA11yAudit} from '../../../test/a11y-test-utils.js';
 import '@polymer/paper-button/paper-button.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-diff');
 
@@ -232,7 +232,9 @@
         };
       });
 
-      test('renders image diffs with same file name', done => {
+      test('renders image diffs with same file name', async () => {
+        const leftRendered = mockPromise();
+        const rightRendered = mockPromise();
         const rendered = () => {
           // Recognizes that it should be an image diff.
           assert.isTrue(element.isImageDiff);
@@ -256,19 +258,12 @@
           assert.isNotOk(rightLabelName);
           assert.isNotOk(leftLabelName);
 
-          let leftLoaded = false;
-          let rightLoaded = false;
-
           leftImage.addEventListener('load', () => {
             assert.isOk(leftImage);
             assert.equal(leftImage.getAttribute('src'),
                 'data:image/bmp;base64,' + mockFile1.body);
             assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
-            leftLoaded = true;
-            if (rightLoaded) {
-              element.removeEventListener('render', rendered);
-              done();
-            }
+            leftRendered.resolve();
           });
 
           rightImage.addEventListener('load', () => {
@@ -277,11 +272,7 @@
                 'data:image/bmp;base64,' + mockFile2.body);
             assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
 
-            rightLoaded = true;
-            if (leftLoaded) {
-              element.removeEventListener('render', rendered);
-              done();
-            }
+            rightRendered.resolve();
           });
         };
 
@@ -305,9 +296,11 @@
           content: [{skip: 66}],
           binary: true,
         };
+        await Promise.all([leftRendered, rightRendered]);
+        element.removeEventListener('render', rendered);
       });
 
-      test('renders image diffs with a different file name', done => {
+      test('renders image diffs with a different file name', async () => {
         const mockDiff = {
           meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
           meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
@@ -324,7 +317,8 @@
           content: [{skip: 66}],
           binary: true,
         };
-
+        const leftRendered = mockPromise();
+        const rightRendered = mockPromise();
         const rendered = () => {
           // Recognizes that it should be an image diff.
           assert.isTrue(element.isImageDiff);
@@ -350,19 +344,12 @@
           assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
           assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
 
-          let leftLoaded = false;
-          let rightLoaded = false;
-
           leftImage.addEventListener('load', () => {
             assert.isOk(leftImage);
             assert.equal(leftImage.getAttribute('src'),
                 'data:image/bmp;base64,' + mockFile1.body);
             assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
-            leftLoaded = true;
-            if (rightLoaded) {
-              element.removeEventListener('render', rendered);
-              done();
-            }
+            leftRendered.resolve();
           });
 
           rightImage.addEventListener('load', () => {
@@ -371,11 +358,7 @@
                 'data:image/bmp;base64,' + mockFile2.body);
             assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
 
-            rightLoaded = true;
-            if (leftLoaded) {
-              element.removeEventListener('render', rendered);
-              done();
-            }
+            rightRendered.resolve();
           });
         };
 
@@ -386,9 +369,11 @@
         element.revisionImage = mockFile2;
         element.revisionImage._name = mockDiff.meta_b.name;
         element.diff = mockDiff;
+        await Promise.all([leftRendered, rightRendered]);
+        element.removeEventListener('render', rendered);
       });
 
-      test('renders added image', done => {
+      test('renders added image', async () => {
         const mockDiff = {
           meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
             lines: 560},
@@ -405,27 +390,27 @@
           binary: true,
         };
 
-        function rendered() {
-          // Recognizes that it should be an image diff.
-          assert.isTrue(element.isImageDiff);
-          assert.instanceOf(
-              element.$.diffBuilder._builder, GrDiffBuilderImage);
-
-          const leftImage = element.$.diffTable.querySelector('td.left img');
-          const rightImage = element.$.diffTable.querySelector('td.right img');
-
-          assert.isNotOk(leftImage);
-          assert.isOk(rightImage);
-          done();
-          element.removeEventListener('render', rendered);
-        }
+        const promise = mockPromise();
+        function rendered() { promise.resolve(); }
         element.addEventListener('render', rendered);
 
         element.revisionImage = mockFile2;
         element.diff = mockDiff;
+        await promise;
+        element.removeEventListener('render', rendered);
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(
+            element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+        const leftImage = element.$.diffTable.querySelector('td.left img');
+        const rightImage = element.$.diffTable.querySelector('td.right img');
+
+        assert.isNotOk(leftImage);
+        assert.isOk(rightImage);
       });
 
-      test('renders removed image', done => {
+      test('renders removed image', async () => {
         const mockDiff = {
           meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
             lines: 560},
@@ -441,28 +426,27 @@
           content: [{skip: 66}],
           binary: true,
         };
-
-        function rendered() {
-          // Recognizes that it should be an image diff.
-          assert.isTrue(element.isImageDiff);
-          assert.instanceOf(
-              element.$.diffBuilder._builder, GrDiffBuilderImage);
-
-          const leftImage = element.$.diffTable.querySelector('td.left img');
-          const rightImage = element.$.diffTable.querySelector('td.right img');
-
-          assert.isOk(leftImage);
-          assert.isNotOk(rightImage);
-          done();
-          element.removeEventListener('render', rendered);
-        }
+        const promise = mockPromise();
+        function rendered() { promise.resolve(); }
         element.addEventListener('render', rendered);
 
         element.baseImage = mockFile1;
         element.diff = mockDiff;
+        await promise;
+        element.removeEventListener('render', rendered);
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(
+            element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+        const leftImage = element.$.diffTable.querySelector('td.left img');
+        const rightImage = element.$.diffTable.querySelector('td.right img');
+
+        assert.isOk(leftImage);
+        assert.isNotOk(rightImage);
       });
 
-      test('does not render disallowed image type', done => {
+      test('does not render disallowed image type', async () => {
         const mockDiff = {
           meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
             lines: 560},
@@ -480,50 +464,54 @@
         };
         mockFile1.type = 'image/jpeg-evil';
 
-        function rendered() {
-          // Recognizes that it should be an image diff.
-          assert.isTrue(element.isImageDiff);
-          assert.instanceOf(
-              element.$.diffBuilder._builder, GrDiffBuilderImage);
-          const leftImage = element.$.diffTable.querySelector('td.left img');
-          assert.isNotOk(leftImage);
-          done();
-          element.removeEventListener('render', rendered);
-        }
+        const promise = mockPromise();
+        function rendered() { promise.resolve(); }
         element.addEventListener('render', rendered);
 
         element.baseImage = mockFile1;
         element.diff = mockDiff;
+        await promise;
+        element.removeEventListener('render', rendered);
+        // Recognizes that it should be an image diff.
+        assert.isTrue(element.isImageDiff);
+        assert.instanceOf(
+            element.$.diffBuilder._builder, GrDiffBuilderImage);
+        const leftImage = element.$.diffTable.querySelector('td.left img');
+        assert.isNotOk(leftImage);
       });
     });
 
-    test('_handleTap lineNum', done => {
+    test('_handleTap lineNum', async () => {
       const addDraftStub = sinon.stub(element, 'addDraftAtLine');
       const el = document.createElement('div');
       el.className = 'lineNum';
+      const promise = mockPromise();
       el.addEventListener('click', e => {
         element._handleTap(e);
         assert.isTrue(addDraftStub.called);
         assert.equal(addDraftStub.lastCall.args[0], el);
-        done();
+        promise.resolve();
       });
       el.click();
+      await promise;
     });
 
-    test('_handleTap context', done => {
+    test('_handleTap context', async () => {
       const showContextStub =
           sinon.stub(element.$.diffBuilder, 'showContext');
       const el = document.createElement('div');
       el.className = 'showContext';
+      const promise = mockPromise();
       el.addEventListener('click', e => {
         element._handleDiffContextExpanded(e);
         assert.isTrue(showContextStub.called);
-        done();
+        promise.resolve();
       });
       el.click();
+      await promise;
     });
 
-    test('_handleTap content', done => {
+    test('_handleTap content', async () => {
       const content = document.createElement('div');
       const lineEl = document.createElement('div');
       lineEl.className = 'lineNum';
@@ -534,13 +522,15 @@
       const selectStub = sinon.stub(element, '_selectLine');
 
       content.className = 'content';
+      const promise = mockPromise();
       content.addEventListener('click', e => {
         element._handleTap(e);
         assert.isTrue(selectStub.called);
         assert.equal(selectStub.lastCall.args[0], lineEl);
-        done();
+        promise.resolve();
       });
       content.click();
+      await promise;
     });
 
     suite('getCursorStops', () => {
@@ -816,41 +806,47 @@
       element.noRenderOnPrefsChange = true;
     });
 
-    test('large render w/ context = 10', done => {
+    test('large render w/ context = 10', async () => {
       element.prefs = {...MINIMAL_PREFS, context: 10};
+      const promise = mockPromise();
       function rendered() {
         assert.isTrue(renderStub.called);
         assert.isFalse(element._showWarning);
-        done();
+        promise.resolve();
         element.removeEventListener('render', rendered);
       }
       element.addEventListener('render', rendered);
       element._renderDiffTable();
+      await promise;
     });
 
-    test('large render w/ whole file and bypass', done => {
+    test('large render w/ whole file and bypass', async () => {
       element.prefs = {...MINIMAL_PREFS, context: -1};
       element._safetyBypass = 10;
+      const promise = mockPromise();
       function rendered() {
         assert.isTrue(renderStub.called);
         assert.isFalse(element._showWarning);
-        done();
+        promise.resolve();
         element.removeEventListener('render', rendered);
       }
       element.addEventListener('render', rendered);
       element._renderDiffTable();
+      await promise;
     });
 
-    test('large render w/ whole file and no bypass', done => {
+    test('large render w/ whole file and no bypass', async () => {
       element.prefs = {...MINIMAL_PREFS, context: -1};
+      const promise = mockPromise();
       function rendered() {
         assert.isFalse(renderStub.called);
         assert.isTrue(element._showWarning);
-        done();
+        promise.resolve();
         element.removeEventListener('render', rendered);
       }
       element.addEventListener('render', rendered);
       element._renderDiffTable();
+      await promise;
     });
 
     test('toggles expand context using bypass', async () => {
@@ -1219,16 +1215,18 @@
     assert.equal(element.getDiffLength(diff), 52);
   });
 
-  test('`render` event has contentRendered field in detail', done => {
+  test('`render` event has contentRendered field in detail', async () => {
     element = basicFixture.instantiate();
     element.prefs = {};
     sinon.stub(element.$.diffBuilder, 'render')
         .returns(Promise.resolve());
+    const promise = mockPromise();
     element.addEventListener('render', event => {
       assert.isTrue(event.detail.contentRendered);
-      done();
+      promise.resolve();
     });
     element._renderDiffTable();
+    await promise;
   });
 
   test('_prefsEqual', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 3544834..0d6cadc 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-dropdown-list/gr-dropdown-list';
 import '../../shared/gr-select/gr-select';
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
index 5ab8449..ebbb0d6 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       align-items: center;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
index 89b8b4a..0fe1fe2 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
@@ -196,7 +196,7 @@
   });
 
   test('_computeBaseDropdownContent called when changeComments update',
-      done => {
+      async () => {
         element.revisions = [
           {commit: {parents: []}},
           {commit: {parents: []}},
@@ -212,15 +212,14 @@
         ];
         element.patchNum = 2;
         element.basePatchNum = 'PARENT';
-        flush();
+        await flush();
 
         // Should be recomputed for each available patch
         sinon.stub(element, '_computeBaseDropdownContent');
         assert.equal(element._computeBaseDropdownContent.callCount, 0);
         element.changeComments = new ChangeComments();
-        flush();
+        await flush();
         assert.equal(element._computeBaseDropdownContent.callCount, 1);
-        done();
       });
 
   test('_computePatchDropdownContent called when basePatchNum updates', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
index 776b954..1c2c074 100644
--- a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 /**
  * Represents a header (label) for a code chunk whenever showing
@@ -25,7 +24,7 @@
  * like long comments and moved in/out chunks.
  */
 @customElement('gr-range-header')
-export class GrRangeHeader extends GrLitElement {
+export class GrRangeHeader extends LitElement {
   @property({type: String})
   icon?: string;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts b/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
index f82290b..3f2258d 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-hint/gr-ranged-comment-hint.ts
@@ -17,13 +17,13 @@
 
 import '../gr-range-header/gr-range-header';
 import {CommentRange} from '../../../types/common';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grRangedCommentTheme} from '../gr-ranged-comment-themes/gr-ranged-comment-theme';
 
 @customElement('gr-ranged-comment-hint')
-export class GrRangedCommentHint extends GrLitElement {
+export class GrRangedCommentHint extends LitElement {
   @property({type: Object})
   range?: CommentRange;
 
@@ -45,6 +45,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         .row {
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
index c3fadf5..fb20a91 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
index b8c3c16..c907a80 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
@@ -120,7 +120,7 @@
     assert.isFalse(annotationSpy.called);
   });
 
-  test('process on empty diff does nothing', done => {
+  test('process on empty diff does nothing', async () => {
     element.diff = {
       meta_a: {content_type: 'application/json'},
       meta_b: {content_type: 'application/json'},
@@ -128,17 +128,14 @@
     };
     const processNextSpy = sinon.spy(element, '_processNextLine');
 
-    const processPromise = element.process();
+    await element.process();
 
-    processPromise.then(() => {
-      assert.isFalse(processNextSpy.called);
-      assert.equal(element.baseRanges.length, 0);
-      assert.equal(element.revisionRanges.length, 0);
-      done();
-    });
+    assert.isFalse(processNextSpy.called);
+    assert.equal(element.baseRanges.length, 0);
+    assert.equal(element.revisionRanges.length, 0);
   });
 
-  test('process for unsupported languages does nothing', done => {
+  test('process for unsupported languages does nothing', async () => {
     element.diff = {
       meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
       meta_b: {content_type: 'application/not-a-real-language'},
@@ -146,33 +143,27 @@
     };
     const processNextSpy = sinon.spy(element, '_processNextLine');
 
-    const processPromise = element.process();
+    await element.process();
 
-    processPromise.then(() => {
-      assert.isFalse(processNextSpy.called);
-      assert.equal(element.baseRanges.length, 0);
-      assert.equal(element.revisionRanges.length, 0);
-      done();
-    });
+    assert.isFalse(processNextSpy.called);
+    assert.equal(element.baseRanges.length, 0);
+    assert.equal(element.revisionRanges.length, 0);
   });
 
-  test('process while disabled does nothing', done => {
+  test('process while disabled does nothing', async () => {
     const processNextSpy = sinon.spy(element, '_processNextLine');
     element.enabled = false;
     const loadHLJSSpy = sinon.spy(element, '_loadHLJS');
 
-    const processPromise = element.process();
+    await element.process();
 
-    processPromise.then(() => {
-      assert.isFalse(processNextSpy.called);
-      assert.equal(element.baseRanges.length, 0);
-      assert.equal(element.revisionRanges.length, 0);
-      assert.isFalse(loadHLJSSpy.called);
-      done();
-    });
+    assert.isFalse(processNextSpy.called);
+    assert.equal(element.baseRanges.length, 0);
+    assert.equal(element.revisionRanges.length, 0);
+    assert.isFalse(loadHLJSSpy.called);
   });
 
-  test('process highlight ipsum', done => {
+  test('process highlight ipsum', async () => {
     element.diff.meta_a.content_type = 'application/json';
     element.diff.meta_b.content_type = 'application/json';
 
@@ -180,65 +171,61 @@
     window.hljs = mockHLJS;
     const highlightSpy = sinon.spy(mockHLJS, 'highlight');
     const processNextSpy = sinon.spy(element, '_processNextLine');
-    const processPromise = element.process();
+    await element.process();
 
-    processPromise.then(() => {
-      const linesA = diff.meta_a.lines;
-      const linesB = diff.meta_b.lines;
+    const linesA = diff.meta_a.lines;
+    const linesB = diff.meta_b.lines;
 
-      assert.isTrue(processNextSpy.called);
-      assert.equal(element.baseRanges.length, linesA);
-      assert.equal(element.revisionRanges.length, linesB);
+    assert.isTrue(processNextSpy.called);
+    assert.equal(element.baseRanges.length, linesA);
+    assert.equal(element.revisionRanges.length, linesB);
 
-      assert.equal(highlightSpy.callCount, linesA + linesB);
+    assert.equal(highlightSpy.callCount, linesA + linesB);
 
-      // The first line of both sides have a range.
-      let ranges = [element.baseRanges[0], element.revisionRanges[0]];
-      for (const range of ranges) {
-        assert.equal(range.length, 1);
-        assert.equal(range[0].className,
-            'gr-diff gr-syntax gr-syntax-string');
-        assert.equal(range[0].start, 'lorem '.length);
-        assert.equal(range[0].length, 'ipsum'.length);
-      }
-
-      // There are no ranges from ll.1-12 on the left and ll.1-11 on the
-      // right.
-      ranges = element.baseRanges.slice(1, 12)
-          .concat(element.revisionRanges.slice(1, 11));
-
-      for (const range of ranges) {
-        assert.equal(range.length, 0);
-      }
-
-      // There should be another pair of ranges on l.13 for the left and
-      // l.12 for the right.
-      ranges = [element.baseRanges[13], element.revisionRanges[12]];
-
-      for (const range of ranges) {
-        assert.equal(range.length, 1);
-        assert.equal(range[0].className,
-            'gr-diff gr-syntax gr-syntax-string');
-        assert.equal(range[0].start, 32);
-        assert.equal(range[0].length, 'ipsum'.length);
-      }
-
-      // The next group should have a similar instance on either side.
-
-      let range = element.baseRanges[15];
+    // The first line of both sides have a range.
+    let ranges = [element.baseRanges[0], element.revisionRanges[0]];
+    for (const range of ranges) {
       assert.equal(range.length, 1);
-      assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
-      assert.equal(range[0].start, 34);
+      assert.equal(range[0].className,
+          'gr-diff gr-syntax gr-syntax-string');
+      assert.equal(range[0].start, 'lorem '.length);
       assert.equal(range[0].length, 'ipsum'.length);
+    }
 
-      range = element.revisionRanges[14];
+    // There are no ranges from ll.1-12 on the left and ll.1-11 on the
+    // right.
+    ranges = element.baseRanges.slice(1, 12)
+        .concat(element.revisionRanges.slice(1, 11));
+
+    for (const range of ranges) {
+      assert.equal(range.length, 0);
+    }
+
+    // There should be another pair of ranges on l.13 for the left and
+    // l.12 for the right.
+    ranges = [element.baseRanges[13], element.revisionRanges[12]];
+
+    for (const range of ranges) {
       assert.equal(range.length, 1);
-      assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
-      assert.equal(range[0].start, 35);
+      assert.equal(range[0].className,
+          'gr-diff gr-syntax gr-syntax-string');
+      assert.equal(range[0].start, 32);
       assert.equal(range[0].length, 'ipsum'.length);
+    }
 
-      done();
-    });
+    // The next group should have a similar instance on either side.
+
+    let range = element.baseRanges[15];
+    assert.equal(range.length, 1);
+    assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+    assert.equal(range[0].start, 34);
+    assert.equal(range[0].length, 'ipsum'.length);
+
+    range = element.revisionRanges[14];
+    assert.equal(range.length, 1);
+    assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
+    assert.equal(range[0].start, 35);
+    assert.equal(range[0].length, 'ipsum'.length);
   });
 
   test('init calls cancel', () => {
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index 90c8383..3adb0f3 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -19,18 +19,15 @@
 import '../../shared/gr-list-view/gr-list-view';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-documentation-search_html';
-import {
-  ListViewMixin,
-  ListViewParams,
-} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {getBaseUrl} from '../../../utils/url-util';
 import {customElement, property} from '@polymer/decorators';
 import {DocResult} from '../../../types/common';
 import {fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
+import {ListViewParams} from '../../gr-app-types';
 
 @customElement('gr-documentation-search')
-export class GrDocumentationSearch extends ListViewMixin(PolymerElement) {
+export class GrDocumentationSearch extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -59,7 +56,7 @@
 
   _paramsChanged(params: ListViewParams) {
     this._loading = true;
-    this._filter = this.getFilterValue(params);
+    this._filter = params?.filter ?? '';
 
     return this._getDocumentationSearches(this._filter);
   }
@@ -84,6 +81,10 @@
     }
     return `${getBaseUrl()}/${url}`;
   }
+
+  computeLoadingClass(loading: boolean) {
+    return loading ? 'loading' : '';
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
index 9c196c7..bf6a0d5 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
@@ -21,8 +21,8 @@
 import {page} from '../../../utils/page-wrapper-utils';
 import 'lodash/lodash';
 import {stubRestApi} from '../../../test/test-utils';
-import {ListViewParams} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin';
 import {DocResult} from '../../../types/common';
+import {ListViewParams} from '../../gr-app-types';
 
 const basicFixture = fixtureFromElement('gr-documentation-search');
 
@@ -47,28 +47,24 @@
   });
 
   suite('list with searches for documentation', () => {
-    setup(done => {
+    setup(async () => {
       documentationSearches = _.times(26, documentationGenerator);
       stubRestApi('getDocumentationSearches').returns(
         Promise.resolve(documentationSearches)
       );
-      element._paramsChanged(value).then(() => {
-        flush(done);
-      });
+      await element._paramsChanged(value);
+      await flush();
     });
 
-    test('test for test repo in the list', done => {
-      flush(() => {
-        assert.equal(
-          element._documentationSearches![0].title,
-          'Gerrit Code Review - REST API Developers Notes1'
-        );
-        assert.equal(
-          element._documentationSearches![0].url,
-          'Documentation/dev-rest-api.html'
-        );
-        done();
-      });
+    test('test for test repo in the list', async () => {
+      assert.equal(
+        element._documentationSearches![0].title,
+        'Gerrit Code Review - REST API Developers Notes1'
+      );
+      assert.equal(
+        element._documentationSearches![0].url,
+        'Documentation/dev-rest-api.html'
+      );
     });
   });
 
@@ -88,14 +84,14 @@
   });
 
   suite('loading', () => {
-    test('correct contents are displayed', () => {
+    test('correct contents are displayed', async () => {
       assert.isTrue(element._loading);
       assert.equal(element.computeLoadingClass(element._loading), 'loading');
       assert.equal(getComputedStyle(element.$.loading).display, 'block');
 
       element._loading = false;
 
-      flush();
+      await flush();
       assert.equal(element.computeLoadingClass(element._loading), '');
       assert.equal(getComputedStyle(element.$.loading).display, 'none');
     });
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
index b04e6cf..480b8fe 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.ts
@@ -16,8 +16,8 @@
  */
 
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -26,7 +26,7 @@
 }
 
 @customElement('gr-default-editor')
-export class GrDefaultEditor extends GrLitElement {
+export class GrDefaultEditor extends LitElement {
   /**
    * Fired when the content of the editor changes.
    *
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts
index 8a483fc..6b7ce34 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.ts
@@ -18,7 +18,7 @@
 import '../../../test/common-test-setup-karma';
 import './gr-default-editor';
 import {GrDefaultEditor} from './gr-default-editor';
-import {queryAndAssert} from '../../../test/test-utils';
+import {mockPromise, queryAndAssert} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-default-editor');
 
@@ -31,13 +31,15 @@
     await flush();
   });
 
-  test('fires content-change event', done => {
+  test('fires content-change event', async () => {
     const textarea = queryAndAssert<HTMLTextAreaElement>(element, '#textarea');
+    const promise = mockPromise();
     element.addEventListener('content-change', e => {
       assert.equal((e as CustomEvent).detail.value, 'test');
-      done();
+      promise.resolve();
     });
     textarea.value = 'test';
     textarea.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
+    await promise;
   });
 });
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 18e6e75..2e1fc21 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -402,15 +402,13 @@
     });
   });
 
-  test('openOpenDialog', done => {
-    element.openOpenDialog('test/path.cpp').then(() => {
-      assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
-      assert.equal(
-        element.$.openDialog!.querySelector('gr-autocomplete')!.text,
-        'test/path.cpp'
-      );
-      done();
-    });
+  test('openOpenDialog', async () => {
+    await element.openOpenDialog('test/path.cpp');
+    assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
+    assert.equal(
+      element.$.openDialog!.querySelector('gr-autocomplete')!.text,
+      'test/path.cpp'
+    );
   });
 
   test('_getDialogFromEvent', () => {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
index ce4c313..db45c33 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
@@ -18,8 +18,8 @@
 import '../../shared/gr-dropdown/gr-dropdown';
 import {GrEditConstants} from '../gr-edit-constants';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 interface EditAction {
   label: string;
@@ -27,7 +27,7 @@
 }
 
 @customElement('gr-edit-file-controls')
-export class GrEditFileControls extends GrLitElement {
+export class GrEditFileControls extends LitElement {
   /**
    * Fired when an action in the overflow menu is tapped.
    *
@@ -59,6 +59,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         gr-button,
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index f5dc610..6f2d27e 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -68,8 +68,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-editor-view')
-export class GrEditorView extends KeyboardShortcutMixin(PolymerElement) {
+export class GrEditorView extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index 5b04a81..f591ab2 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -19,7 +19,7 @@
 import {GrEditorView} from './gr-editor-view';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {HttpMethod} from '../../../constants/constants';
-import {stubRestApi, stubStorage} from '../../../test/test-utils';
+import {mockPromise, stubRestApi, stubStorage} from '../../../test/test-utils';
 import {
   EditPatchSetNum,
   NumericChangeId,
@@ -348,14 +348,16 @@
     });
   });
 
-  test('_showAlert', done => {
+  test('_showAlert', async () => {
+    const promise = mockPromise();
     element.addEventListener('show-alert', e => {
       assert.deepEqual(e.detail, {message: 'test message'});
       assert.isTrue(e.bubbles);
-      done();
+      promise.resolve();
     });
 
     element._showAlert('test message');
+    await promise;
   });
 
   test('_viewEditInChangeView', () => {
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 88ed0dc..b6fe60b 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -102,9 +102,12 @@
   restamp: boolean;
 };
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 // TODO(TS): implement AppElement interface from gr-app-types.ts
 @customElement('gr-app-element')
-export class GrAppElement extends KeyboardShortcutMixin(PolymerElement) {
+export class GrAppElement extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -349,6 +352,8 @@
     this.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's:keydown');
     this.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
     this.bindShortcut(Shortcut.EDIT_TOPIC, 't');
+    this.bindShortcut(Shortcut.OPEN_SUBMIT_DIALOG, 'shift+s');
+    this.bindShortcut(Shortcut.TOGGLE_ATTENTION_SET, 'shift+t');
 
     this.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a:keyup');
     this.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd:keyup');
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index e5096c0..6c8bdb9 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -52,20 +52,21 @@
   groupId: GroupId;
 }
 
-export interface AppElementAdminParams {
+export interface ListViewParams {
+  filter?: string | null;
+  offset?: number | string;
+}
+
+export interface AppElementAdminParams extends ListViewParams {
   view: GerritView.ADMIN;
   adminView: string;
-  offset?: string | number;
-  filter?: string | null;
   openCreateModal?: boolean;
 }
 
-export interface AppElementRepoParams {
+export interface AppElementRepoParams extends ListViewParams {
   view: GerritView.REPO;
   detail?: RepoDetailView;
   repo: RepoName;
-  offset?: string | number;
-  filter?: string | null;
 }
 
 export interface AppElementDocSearchParams {
diff --git a/polygerrit-ui/app/elements/lit/gr-lit-element.ts b/polygerrit-ui/app/elements/lit/gr-lit-element.ts
deleted file mode 100644
index d0b68e3..0000000
--- a/polygerrit-ui/app/elements/lit/gr-lit-element.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the 'License');
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an 'AS IS' BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {LitElement} from 'lit-element';
-import {Observable, Subject} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
-
-/**
- * Base class for Gerrit's lit-elements.
- *
- * Adds basic functionality that we want to have available in all Gerrit's
- * components.
- */
-export abstract class GrLitElement extends LitElement {
-  disconnected$ = new Subject();
-
-  /**
-   * Hooks up an element property with an observable. Apart from subscribing it
-   * makes sure that you are unsubscribed when the component is disconnected.
-   * And it requests a template check when a new value comes in.
-   *
-   * Should be called from connectedCallback() such that you will be
-   * re-subscribed when the component is re-connected.
-   *
-   * TODO: Maybe distinctUntilChanged should be applied to obs$?
-   */
-  subscribe<Key extends keyof this>(prop: Key, obs$: Observable<this[Key]>) {
-    obs$.pipe(takeUntil(this.disconnected$)).subscribe(value => {
-      const oldValue = this[prop];
-      this[prop] = value;
-      this.requestUpdate(prop, oldValue);
-    });
-  }
-
-  override disconnectedCallback() {
-    this.disconnected$.next();
-    super.disconnectedCallback();
-  }
-}
diff --git a/polygerrit-ui/app/elements/lit/gr-lit-element_test.ts b/polygerrit-ui/app/elements/lit/gr-lit-element_test.ts
deleted file mode 100644
index 9b7b0e2..0000000
--- a/polygerrit-ui/app/elements/lit/gr-lit-element_test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma';
-import {html, customElement} from 'lit-element';
-import {GrLitElement} from './gr-lit-element';
-
-@customElement('test-gr-lit-element')
-export class TestGrLitElement extends GrLitElement {
-  render() {
-    return html`<span>test</span>`;
-  }
-}
-
-declare global {
-  interface HTMLElementTagNameMap {
-    'test-gr-lit-element': GrLitElement;
-  }
-}
-
-suite('gr-lit-element test', () => {
-  test('is defined', () => {
-    const el = document.createElement('test-gr-lit-element');
-    assert.instanceOf(el, TestGrLitElement);
-  });
-});
diff --git a/polygerrit-ui/app/elements/lit/subscription-controller.ts b/polygerrit-ui/app/elements/lit/subscription-controller.ts
new file mode 100644
index 0000000..ab4ed64
--- /dev/null
+++ b/polygerrit-ui/app/elements/lit/subscription-controller.ts
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ReactiveController, ReactiveControllerHost} from 'lit';
+import {Observable, Subscription} from 'rxjs';
+
+/**
+ * Enables components to simply hook up a property with an Observable like so:
+ *
+ * subscribe(this, obs$, x => (this.prop = x));
+ */
+export function subscribe<T>(
+  host: ReactiveControllerHost,
+  obs$: Observable<T>,
+  setProp: (t: T) => void
+) {
+  host.addController(new SubscriptionController(obs$, setProp));
+}
+
+export class SubscriptionController<T> implements ReactiveController {
+  private sub?: Subscription;
+
+  constructor(
+    private readonly obs$: Observable<T>,
+    private readonly setProp: (t: T) => void
+  ) {}
+
+  hostConnected() {
+    this.sub = this.obs$.subscribe(this.setProp);
+  }
+
+  hostDisconnected() {
+    this.sub?.unsubscribe();
+  }
+}
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
index 8138ff0..1be5e82 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
@@ -51,7 +51,7 @@
   let decorationHookWithSlot;
   let replacementHook;
 
-  setup(done => {
+  setup(async () => {
     resetPlugins();
     container = basicFixture.instantiate();
     pluginApi.install(p => plugin = p, '0.1',
@@ -68,7 +68,7 @@
         'second', 'other-module', {replace: true});
     // Mimic all plugins loaded.
     getPluginLoader().loadPlugins([]);
-    flush(done);
+    await flush();
   });
 
   teardown(() => {
@@ -135,58 +135,52 @@
         });
   });
 
-  test('late registration', done => {
+  test('late registration', async () => {
     plugin.registerCustomComponent('banana', 'noob-noob');
-    flush(() => {
-      const element =
-          container.querySelector('gr-endpoint-decorator[name="banana"]');
-      const module = Array.from(element.root.children).find(
-          element => element.nodeName === 'NOOB-NOOB');
-      assert.isOk(module);
-      done();
-    });
+    await flush();
+    const element =
+        container.querySelector('gr-endpoint-decorator[name="banana"]');
+    const module = Array.from(element.root.children).find(
+        element => element.nodeName === 'NOOB-NOOB');
+    assert.isOk(module);
   });
 
-  test('two modules', done => {
+  test('two modules', async () => {
     plugin.registerCustomComponent('banana', 'mod-one');
     plugin.registerCustomComponent('banana', 'mod-two');
-    flush(() => {
-      const element =
-          container.querySelector('gr-endpoint-decorator[name="banana"]');
-      const module1 = Array.from(element.root.children).find(
-          element => element.nodeName === 'MOD-ONE');
-      assert.isOk(module1);
-      const module2 = Array.from(element.root.children).find(
-          element => element.nodeName === 'MOD-TWO');
-      assert.isOk(module2);
-      done();
-    });
+    await flush();
+    const element =
+        container.querySelector('gr-endpoint-decorator[name="banana"]');
+    const module1 = Array.from(element.root.children).find(
+        element => element.nodeName === 'MOD-ONE');
+    assert.isOk(module1);
+    const module2 = Array.from(element.root.children).find(
+        element => element.nodeName === 'MOD-TWO');
+    assert.isOk(module2);
   });
 
-  test('late param setup', done => {
+  test('late param setup', async () => {
     const element =
         container.querySelector('gr-endpoint-decorator[name="banana"]');
     const param = element.querySelector('gr-endpoint-param');
     param['value'] = undefined;
     plugin.registerCustomComponent('banana', 'noob-noob');
-    flush(() => {
-      let module = Array.from(element.root.children).find(
-          element => element.nodeName === 'NOOB-NOOB');
-      // Module waits for param to be defined.
-      assert.isNotOk(module);
-      const value = {abc: 'def'};
-      param.value = value;
-      flush(() => {
-        module = Array.from(element.root.children).find(
-            element => element.nodeName === 'NOOB-NOOB');
-        assert.isOk(module);
-        assert.strictEqual(module['someParam'], value);
-        done();
-      });
-    });
+    await flush();
+    let module = Array.from(element.root.children).find(
+        element => element.nodeName === 'NOOB-NOOB');
+    // Module waits for param to be defined.
+    assert.isNotOk(module);
+    const value = {abc: 'def'};
+    param.value = value;
+
+    await flush();
+    module = Array.from(element.root.children).find(
+        element => element.nodeName === 'NOOB-NOOB');
+    assert.isOk(module);
+    assert.strictEqual(module['someParam'], value);
   });
 
-  test('param is bound', done => {
+  test('param is bound', async () => {
     const element =
         container.querySelector('gr-endpoint-decorator[name="banana"]');
     const param = element.querySelector('gr-endpoint-param');
@@ -194,13 +188,11 @@
     const value2 = {def: 'abc'};
     param.value = value1;
     plugin.registerCustomComponent('banana', 'noob-noob');
-    flush(() => {
-      const module = Array.from(element.root.children).find(
-          element => element.nodeName === 'NOOB-NOOB');
-      assert.strictEqual(module['someParam'], value1);
-      param.value = value2;
-      assert.strictEqual(module['someParam'], value2);
-      done();
-    });
+    await flush();
+    const module = Array.from(element.root.children).find(
+        element => element.nodeName === 'NOOB-NOOB');
+    assert.strictEqual(module['someParam'], value1);
+    param.value = value2;
+    assert.strictEqual(module['someParam'], value2);
   });
 });
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
index 6291e64..4e3d657 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
@@ -19,6 +19,7 @@
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {mockPromise} from '../../../test/test-utils.js';
 
 Polymer({
   is: 'gr-event-helper-some-element',
@@ -47,11 +48,13 @@
     instance = plugin.eventHelper(element);
   });
 
-  test('onTap()', done => {
+  test('onTap()', async () => {
+    const promise = mockPromise();
     instance.onTap(() => {
-      done();
+      promise.resolve();
     });
     MockInteractions.tap(element);
+    await promise;
   });
 
   test('onTap() cancel', () => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
index e4d84e3..7fee4a0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.ts
@@ -14,13 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement, property, PropertyValues} from 'lit-element';
+import {LitElement, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 import {ServerInfo} from '../../../types/common';
 
 @customElement('gr-plugin-host')
-export class GrPluginHost extends GrLitElement {
+export class GrPluginHost extends LitElement {
   @property({type: Object})
   config?: ServerInfo;
 
@@ -37,7 +37,7 @@
     getPluginLoader().loadPlugins(pluginsPending);
   }
 
-  updated(changedProperties: PropertyValues<GrPluginHost>) {
+  override updated(changedProperties: PropertyValues<GrPluginHost>) {
     if (changedProperties.has('config') && this.config) {
       this._configChanged(this.config);
     }
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts
index a186f70..342cf83 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.ts
@@ -36,11 +36,9 @@
     assert.isOk(element);
   });
 
-  test('open uses open() from gr-overlay', done => {
-    element.open().then(() => {
-      assert.isTrue(overlayOpen.called);
-      done();
-    });
+  test('open uses open() from gr-overlay', async () => {
+    await element.open();
+    assert.isTrue(overlayOpen.called);
   });
 
   test('close uses close() from gr-overlay', () => {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
index 8ed6611..45a93bf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
@@ -51,6 +51,11 @@
     return dom(this.popup) as unknown as HTMLElement;
   }
 
+  appendContent(el: HTMLElement) {
+    if (!this.popup) throw new Error('popup element not (yet) available');
+    this.popup.appendChild(el);
+  }
+
   /**
    * Opens the popup, inserts it into DOM over current UI.
    * Creates the popup if not previously created. Creates popup content element,
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
index 8a7788f..2889333 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
@@ -56,27 +56,23 @@
       instance = new GrPopupInterface(plugin);
     });
 
-    test('open', done => {
-      instance.open().then(api => {
-        assert.strictEqual(api, instance);
-        const manual = document.createElement('div');
-        manual.id = 'foobar';
-        manual.innerHTML = 'manual content';
-        api._getElement().appendChild(manual);
-        flush();
-        assert.equal(
-            container.querySelector('#foobar').textContent, 'manual content');
-        done();
-      });
+    test('open', async () => {
+      const api = await instance.open();
+      assert.strictEqual(api, instance);
+      const manual = document.createElement('div');
+      manual.id = 'foobar';
+      manual.innerHTML = 'manual content';
+      api._getElement().appendChild(manual);
+      await flush();
+      assert.equal(
+          container.querySelector('#foobar').textContent, 'manual content');
     });
 
-    test('close', done => {
-      instance.open().then(api => {
-        assert.isTrue(api._getElement().node.opened);
-        api.close();
-        assert.isFalse(api._getElement().node.opened);
-        done();
-      });
+    test('close', async () => {
+      const api = await instance.open();
+      assert.isTrue(api._getElement().node.opened);
+      api.close();
+      assert.isFalse(api._getElement().node.opened);
     });
   });
 
@@ -85,21 +81,16 @@
       instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
     });
 
-    test('open', done => {
-      instance.open().then(api => {
-        assert.isNotNull(
-            container.querySelector('gr-user-test-popup'));
-        done();
-      });
+    test('open', async () => {
+      await instance.open();
+      assert.isNotNull(container.querySelector('gr-user-test-popup'));
     });
 
-    test('close', done => {
-      instance.open().then(api => {
-        assert.isTrue(api._getElement().node.opened);
-        api.close();
-        assert.isFalse(api._getElement().node.opened);
-        done();
-      });
+    test('close', async () => {
+      const api = await instance.open();
+      assert.isTrue(api._getElement().node.opened);
+      api.close();
+      assert.isFalse(api._getElement().node.opened);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
index f913cf6..6580ad6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+/* eslint-disable lit/no-legacy-template-syntax,lit/prefer-static-styles */
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 import {customElement, property} from '@polymer/decorators';
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
index 514f00e..51259c8 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
@@ -56,7 +56,7 @@
       <span class="title">Registered</span>
       <span class="value">
         <gr-date-formatter
-          has-tooltip=""
+          withTooltip
           date-str="[[_account.registered_on]]"
         ></gr-date-formatter>
       </span>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
index 4a4862b..f1813a4 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -27,7 +27,7 @@
   createServerInfo,
 } from '../../../test/test-data-generators';
 import {IronInputElement} from '@polymer/iron-input';
-import {SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {SinonStubbedMember} from 'sinon';
 import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
 
 const basicFixture = fixtureFromElement('gr-account-info');
@@ -157,7 +157,7 @@
       statusStub = stubRestApi('setAccountStatus').returns(Promise.resolve());
     });
 
-    test('name', done => {
+    test('name', async () => {
       assert.isTrue(element.nameMutable);
       assert.isFalse(element.hasUnsavedChanges);
 
@@ -167,18 +167,15 @@
       assert.isFalse(statusChangedSpy.called);
       assert.isTrue(element.hasUnsavedChanges);
 
-      element.save().then(() => {
-        assert.isFalse(usernameStub.called);
-        assert.isTrue(nameStub.called);
-        assert.isFalse(statusStub.called);
-        nameStub.lastCall.returnValue.then(() => {
-          assert.equal(nameStub.lastCall.args[0], 'new name');
-          done();
-        });
-      });
+      await element.save();
+      assert.isFalse(usernameStub.called);
+      assert.isTrue(nameStub.called);
+      assert.isFalse(statusStub.called);
+      await nameStub.lastCall.returnValue;
+      assert.equal(nameStub.lastCall.args[0], 'new name');
     });
 
-    test('username', done => {
+    test('username', async () => {
       element.set('_account.username', '');
       element._hasUsernameChange = false;
       assert.isTrue(element.usernameMutable);
@@ -189,18 +186,15 @@
       assert.isFalse(statusChangedSpy.called);
       assert.isTrue(element.hasUnsavedChanges);
 
-      element.save().then(() => {
-        assert.isTrue(usernameStub.called);
-        assert.isFalse(nameStub.called);
-        assert.isFalse(statusStub.called);
-        usernameStub.lastCall.returnValue.then(() => {
-          assert.equal(usernameStub.lastCall.args[0], 'new username');
-          done();
-        });
-      });
+      await element.save();
+      assert.isTrue(usernameStub.called);
+      assert.isFalse(nameStub.called);
+      assert.isFalse(statusStub.called);
+      await usernameStub.lastCall.returnValue;
+      assert.equal(usernameStub.lastCall.args[0], 'new username');
     });
 
-    test('status', done => {
+    test('status', async () => {
       assert.isFalse(element.hasUnsavedChanges);
 
       element.set('_account.status', 'new status');
@@ -209,15 +203,12 @@
       assert.isTrue(statusChangedSpy.called);
       assert.isTrue(element.hasUnsavedChanges);
 
-      element.save().then(() => {
-        assert.isFalse(usernameStub.called);
-        assert.isTrue(statusStub.called);
-        assert.isFalse(nameStub.called);
-        statusStub.lastCall.returnValue.then(() => {
-          assert.equal(statusStub.lastCall.args[0], 'new status');
-          done();
-        });
-      });
+      await element.save();
+      assert.isFalse(usernameStub.called);
+      assert.isTrue(statusStub.called);
+      assert.isFalse(nameStub.called);
+      await statusStub.lastCall.returnValue;
+      assert.equal(statusStub.lastCall.args[0], 'new status');
     });
   });
 
@@ -239,7 +230,7 @@
       stubRestApi('setAccountUsername').returns(Promise.resolve());
     });
 
-    test('set name and status', done => {
+    test('set name and status', async () => {
       assert.isTrue(element.nameMutable);
       assert.isFalse(element.hasUnsavedChanges);
 
@@ -253,16 +244,13 @@
 
       assert.isTrue(element.hasUnsavedChanges);
 
-      element.save().then(() => {
-        assert.isTrue(statusStub.called);
-        assert.isTrue(nameStub.called);
+      await element.save();
+      assert.isTrue(statusStub.called);
+      assert.isTrue(nameStub.called);
 
-        assert.equal(nameStub.lastCall.args[0], 'new name');
+      assert.equal(nameStub.lastCall.args[0], 'new name');
 
-        assert.equal(statusStub.lastCall.args[0], 'new status');
-
-        done();
-      });
+      assert.equal(statusStub.lastCall.args[0], 'new status');
     });
   });
 
@@ -277,7 +265,7 @@
       statusStub = stubRestApi('setAccountStatus').returns(Promise.resolve());
     });
 
-    test('read full name but set status', done => {
+    test('read full name but set status', async () => {
       const section = element.$.nameSection;
       const displaySpan = section.querySelectorAll('.value')[0];
       const inputSpan = section.querySelectorAll('.value')[1];
@@ -296,13 +284,10 @@
 
       assert.isTrue(element.hasUnsavedChanges);
 
-      element.save().then(() => {
-        assert.isTrue(statusStub.called);
-        statusStub.lastCall.returnValue.then(() => {
-          assert.equal(statusStub.lastCall.args[0], 'new status');
-          done();
-        });
-      });
+      await element.save();
+      assert.isTrue(statusStub.called);
+      await statusStub.lastCall.returnValue;
+      assert.equal(statusStub.lastCall.args[0], 'new status');
     });
   });
 
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
index 41fc71a..a972db3 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
@@ -20,7 +20,8 @@
 import {appContext} from '../../../services/app-context';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {css, customElement, html, property, LitElement} from 'lit-element';
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 @customElement('gr-agreements-list')
 export class GrAgreementsList extends LitElement {
@@ -40,7 +41,7 @@
     });
   }
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -70,7 +71,7 @@
     `;
   }
 
-  render() {
+  override render() {
     return html` <div class="gr-form-styles">
       <table id="agreements">
         <thead>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 6c942b0..96b1ded 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -15,19 +15,18 @@
  * limitations under the License.
  */
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../../styles/shared-styles';
 import '../../../styles/gr-form-styles';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-change-table-editor_html';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {customElement, property, observe} from '@polymer/decorators';
 import {ServerInfo} from '../../../types/common';
 import {appContext} from '../../../services/app-context';
+import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
 
 @customElement('gr-change-table-editor')
-export class GrChangeTableEditor extends ChangeTableMixin(PolymerElement) {
+export class GrChangeTableEditor extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -48,18 +47,33 @@
 
   @observe('serverConfig')
   _configChanged(config: ServerInfo) {
-    this.defaultColumns = this.getEnabledColumns(
-      this.columnNames,
-      config,
-      this.flagsService.enabledExperiments
+    this.defaultColumns = columnNames.filter(col =>
+      this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
     );
     if (!this.displayedColumns) return;
     this.displayedColumns = this.displayedColumns.filter(column =>
-      this.isColumnEnabled(column, config, this.flagsService.enabledExperiments)
+      this._isColumnEnabled(
+        column,
+        config,
+        this.flagsService.enabledExperiments
+      )
     );
   }
 
   /**
+   * Is the column disabled by a server config or experiment? For example the
+   * assignee feature might be disabled and thus the corresponding column is
+   * also disabled.
+   *
+   */
+  _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+    if (!config || !config.change) return true;
+    if (column === 'Assignee') return !!config.change.enable_assignee;
+    if (column === 'Comments') return experiments.includes('comments-column');
+    return true;
+  }
+
+  /**
    * Get the list of enabled column names from whichever checkboxes are
    * checked (excluding the number checkbox).
    */
@@ -76,6 +90,13 @@
       .map(checkbox => checkbox.name);
   }
 
+  _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+    if (!columnsToDisplay || !columnToCheck) {
+      return false;
+    }
+    return !columnsToDisplay.includes(columnToCheck);
+  }
+
   /**
    * Handle a click on a checkbox container and relay the click to the checkbox it
    * contains.
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
index a05ec73..e756a20 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
@@ -74,7 +74,7 @@
                 type="checkbox"
                 name="[[item]]"
                 on-click="_handleTargetClick"
-                checked$="[[!isColumnHidden(item, displayedColumns)]]"
+                checked$="[[!_computeIsColumnHidden(item, displayedColumns)]]"
               />
             </td>
           </tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index 4f61972..4f8d0a0 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -117,7 +117,7 @@
 
   test('_getDisplayedColumns', () => {
     const enabledColumns = columns.filter(column =>
-      element.isColumnEnabled(column, element.serverConfig!, [])
+      element._isColumnEnabled(column, element.serverConfig!, [])
     );
     assert.deepEqual(element._getDisplayedColumns(), enabledColumns);
     const input = queryAndAssert<HTMLInputElement>(
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
index 76047f4..5b757e6 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
@@ -16,6 +16,7 @@
  */
 
 import '@polymer/iron-input/iron-input';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/shared-styles';
 import '../../shared/gr-button/gr-button';
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
index 4800e5b..ce95ccb 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     h1 {
       margin-bottom: var(--spacing-m);
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
index 8820748..ba736a2 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
@@ -17,7 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-gpg-editor.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-gpg-editor');
 
@@ -25,7 +25,7 @@
   let element;
   let keys;
 
-  setup(done => {
+  setup(async () => {
     const fingerprint1 = '0192 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B';
     const fingerprint2 = '0196 723D 42D1 0C5B 32A6  E1E0 9350 9E4B AFC8 A49B';
     keys = {
@@ -55,7 +55,8 @@
 
     element = basicFixture.instantiate();
 
-    element.loadData().then(() => { flush(done); });
+    await element.loadData();
+    await flush();
   });
 
   test('renders', () => {
@@ -70,7 +71,7 @@
     assert.equal(cells[0].textContent, 'AED9B59C');
   });
 
-  test('remove key', done => {
+  test('remove key', async () => {
     const lastKey = keys[Object.keys(keys)[1]];
 
     const saveStub = stubRestApi('deleteAccountGPGKey')
@@ -91,13 +92,11 @@
     assert.isTrue(element.hasUnsavedChanges);
     assert.isFalse(saveStub.called);
 
-    element.save().then(() => {
-      assert.isTrue(saveStub.called);
-      assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
-      assert.equal(element._keysToRemove.length, 0);
-      assert.isFalse(element.hasUnsavedChanges);
-      done();
-    });
+    await element.save();
+    assert.isTrue(saveStub.called);
+    assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
+    assert.equal(element._keysToRemove.length, 0);
+    assert.isFalse(element.hasUnsavedChanges);
   });
 
   test('show key', () => {
@@ -113,7 +112,7 @@
     assert.isTrue(openSpy.called);
   });
 
-  test('add key', done => {
+  test('add key', async () => {
     const newKeyString =
         '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
         '\nVersion: BCPG v1.52\n\t<key 3>';
@@ -138,11 +137,12 @@
     assert.isFalse(element.$.addButton.disabled);
     assert.isFalse(element.$.newKey.disabled);
 
+    const promise = mockPromise();
     element._handleAddKey().then(() => {
       assert.isTrue(element.$.addButton.disabled);
       assert.isFalse(element.$.newKey.disabled);
       assert.equal(element._keys.length, 2);
-      done();
+      promise.resolve();
     });
 
     assert.isTrue(element.$.addButton.disabled);
@@ -150,9 +150,10 @@
 
     assert.isTrue(addStub.called);
     assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+    await promise;
   });
 
-  test('add invalid key', done => {
+  test('add invalid key', async () => {
     const newKeyString = 'not even close to valid';
 
     const addStub = stubRestApi(
@@ -164,11 +165,12 @@
     assert.isFalse(element.$.addButton.disabled);
     assert.isFalse(element.$.newKey.disabled);
 
+    const promise = mockPromise();
     element._handleAddKey().then(() => {
       assert.isFalse(element.$.addButton.disabled);
       assert.isFalse(element.$.newKey.disabled);
       assert.equal(element._keys.length, 2);
-      done();
+      promise.resolve();
     });
 
     assert.isTrue(element.$.addButton.disabled);
@@ -176,6 +178,7 @@
 
     assert.isTrue(addStub.called);
     assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+    await promise;
   });
 });
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
index 2757454..8f1706d 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
@@ -20,8 +20,8 @@
 import {appContext} from '../../../services/app-context';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, state} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, state} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -29,7 +29,7 @@
   }
 }
 @customElement('gr-group-list')
-export class GrGroupList extends GrLitElement {
+export class GrGroupList extends LitElement {
   @state()
   protected _groups: GroupInfo[] = [];
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
index 3dce295..a1534af 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.ts
@@ -28,7 +28,7 @@
   let element: GrGroupList;
   let groups: GroupInfo[];
 
-  setup(done => {
+  setup(async () => {
     groups = [
       {
         url: 'some url',
@@ -58,9 +58,8 @@
 
     element = basicFixture.instantiate();
 
-    element.loadData().then(() => {
-      flush(done);
-    });
+    await element.loadData();
+    await flush();
   });
 
   test('renders', async () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
index ccf8a01..c62cff4 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.ts
@@ -21,8 +21,8 @@
 import {appContext} from '../../../services/app-context';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, query} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, query} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -31,7 +31,7 @@
 }
 
 @customElement('gr-http-password')
-export class GrHttpPassword extends GrLitElement {
+export class GrHttpPassword extends LitElement {
   @query('#generatedPasswordOverlay')
   generatedPasswordOverlay?: GrOverlay;
 
@@ -75,7 +75,7 @@
     return Promise.all(promises);
   }
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       formStyles,
@@ -113,7 +113,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <div class="gr-form-styles">
         <div ?hidden=${this._passwordUrl}>
           <section>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
index 004f18f..eab8d2e 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.ts
@@ -35,7 +35,7 @@
   let account: AccountDetailInfo;
   let config: ServerInfo;
 
-  setup(done => {
+  setup(async () => {
     account = {...createAccountDetailWithId(), username: 'user name'};
     config = createServerInfo();
 
@@ -43,9 +43,8 @@
     stubRestApi('getConfig').returns(Promise.resolve(config));
 
     element = basicFixture.instantiate();
-    element.loadData().then(() => {
-      flush(done);
-    });
+    await element.loadData();
+    await flush();
   });
 
   test('generate password', () => {
@@ -76,12 +75,10 @@
     assert.isNull(element._passwordUrl);
   });
 
-  test('with http_password_url', done => {
+  test('with http_password_url', async () => {
     config.auth.http_password_url = 'http://example.com/';
-    element.loadData().then(() => {
-      assert.isNotNull(element._passwordUrl);
-      assert.equal(element._passwordUrl, config.auth.http_password_url);
-      done();
-    });
+    await element.loadData();
+    assert.isNotNull(element._passwordUrl);
+    assert.equal(element._passwordUrl, config.auth.http_password_url);
   });
 });
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
index d1a4f8f..9d8dcc5 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.ts
@@ -88,13 +88,11 @@
     assert.isTrue(element.filterIdentities(ids[1]));
   });
 
-  test('delete id', done => {
+  test('delete id', async () => {
     element._idName = 'mailto:gerrit2@example.com';
     const loadDataStub = sinon.stub(element, 'loadData');
-    element._handleDeleteItemConfirm().then(() => {
-      assert.isTrue(loadDataStub.called);
-      done();
-    });
+    await element._handleDeleteItemConfirm();
+    assert.isTrue(loadDataStub.called);
   });
 
   test('_handleDeleteItem opens modal', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
index ace1e1a..4096b02 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.ts
@@ -16,7 +16,6 @@
  */
 import '@polymer/iron-input/iron-input';
 import '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../../styles/shared-styles';
 import '../../../styles/gr-form-styles';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js
index 19852d9..1ca4852 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.js
@@ -46,7 +46,7 @@
     MockInteractions.tap(button);
   }
 
-  setup(done => {
+  setup(async () => {
     element = basicFixture.instantiate();
     menu = [
       {url: '/first/url', name: 'first name', target: '_blank'},
@@ -55,7 +55,7 @@
     ];
     element.set('menuItems', menu);
     flush$0();
-    flush(done);
+    await flush();
   });
 
   test('renders', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
index 6c21e58..07a2e51 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
@@ -93,38 +93,34 @@
     return promise;
   }
 
-  test('fires the close event on close', done => {
-    close().then(done);
+  test('fires the close event on close', async () => {
+    await close();
   });
 
-  test('fires the close event on save', done => {
-    close(() =>
+  test('fires the close event on save', async () => {
+    await close(() =>
       MockInteractions.tap(queryAndAssert(element, '#saveButton'))
-    ).then(done);
+    );
   });
 
-  test('saves account details', done => {
-    flush(() => {
-      element.$.name.value = 'new name';
+  test('saves account details', async () => {
+    await flush();
+    element.$.name.value = 'new name';
 
-      element.set('_account.username', '');
-      element._hasUsernameChange = false;
-      assert.isTrue(element._usernameMutable);
+    element.set('_account.username', '');
+    element._hasUsernameChange = false;
+    assert.isTrue(element._usernameMutable);
 
-      element.set('_username', 'new username');
+    element.set('_username', 'new username');
 
-      // Nothing should be committed yet.
-      assert.equal(account.name, 'name');
-      assert.isNotOk(account.username);
+    // Nothing should be committed yet.
+    assert.equal(account.name, 'name');
+    assert.isNotOk(account.username);
 
-      // Save and verify new values are committed.
-      save()
-        .then(() => {
-          assert.equal(account.name, 'new name');
-          assert.equal(account.username, 'new username');
-        })
-        .then(done);
-    });
+    // Save and verify new values are committed.
+    await save();
+    assert.equal(account.name, 'new name');
+    assert.equal(account.username, 'new username');
   });
 
   test('save btn disabled', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
index ecb02eb..64409d5 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.ts
@@ -14,9 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {formStyles} from '../../../styles/gr-form-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -25,7 +25,7 @@
 }
 
 @customElement('gr-settings-item')
-export class GrSettingsItem extends GrLitElement {
+export class GrSettingsItem extends LitElement {
   @property({type: String})
   anchor?: string;
 
@@ -34,6 +34,7 @@
 
   static override get styles() {
     return [
+      formStyles,
       css`
         :host {
           display: block;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
index 0748379..9e4ea0a 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.ts
@@ -16,8 +16,8 @@
  */
 import {pageNavStyles} from '../../../styles/gr-page-nav-styles';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement, html, property} from 'lit-element';
+import {LitElement, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -26,7 +26,7 @@
 }
 
 @customElement('gr-settings-menu-item')
-export class GrSettingsMenuItem extends GrLitElement {
+export class GrSettingsMenuItem extends LitElement {
   @property({type: String})
   href?: string;
 
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index 91b2ed6..94333c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -16,6 +16,7 @@
  */
 import '@polymer/iron-input/iron-input';
 import '@polymer/paper-toggle-button/paper-toggle-button';
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-form-styles';
 import '../../../styles/gr-menu-page-styles';
 import '../../../styles/gr-page-nav-styles';
@@ -28,7 +29,6 @@
 import '../gr-change-table-editor/gr-change-table-editor';
 import '../../shared/gr-button/gr-button';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import '../../shared/gr-date-formatter/gr-date-formatter';
 import '../../shared/gr-diff-preferences/gr-diff-preferences';
 import '../../shared/gr-page-nav/gr-page-nav';
 import '../../shared/gr-select/gr-select';
@@ -46,7 +46,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-settings-view_html';
 import {getDocsBaseUrl} from '../../../utils/url-util';
-import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin';
 import {customElement, property, observe} from '@polymer/decorators';
 import {AppElementParams} from '../../gr-app-types';
 import {GrAccountInfo} from '../gr-account-info/gr-account-info';
@@ -75,6 +74,7 @@
   EmailStrategy,
   TimeFormat,
 } from '../../../constants/constants';
+import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
 
 const PREFS_SECTION_FIELDS: Array<keyof PreferencesInput> = [
   'changes_per_page',
@@ -84,6 +84,7 @@
   'diff_view',
   'publish_comments_on_push',
   'disable_keyboard_shortcuts',
+  'disable_token_highlighting',
   'work_in_progress_by_default',
   'default_base_for_merges',
   'signed_off_by',
@@ -123,6 +124,7 @@
     showSizeBarsInFileList: HTMLInputElement;
     publishCommentsOnPush: HTMLInputElement;
     disableKeyboardShortcuts: HTMLInputElement;
+    disableTokenHighlighting: HTMLInputElement;
     relativeDateInChangeTable: HTMLInputElement;
     changesPerPageSelect: HTMLInputElement;
     dateTimeFormatSelect: HTMLInputElement;
@@ -137,7 +139,7 @@
 }
 
 @customElement('gr-settings-view')
-export class GrSettingsView extends ChangeTableMixin(PolymerElement) {
+export class GrSettingsView extends PolymerElement {
   static get template() {
     return htmlTemplate;
   }
@@ -256,8 +258,10 @@
         this._localMenu = this._cloneMenu(prefs.my);
         this._localChangeTableColumns =
           prefs.change_table.length === 0
-            ? this.columnNames
-            : this.renameProjectToRepoColumn(prefs.change_table);
+            ? columnNames
+            : prefs.change_table.map(column =>
+                column === 'Project' ? 'Repo' : column
+              );
       })
     );
 
@@ -404,6 +408,13 @@
     );
   }
 
+  _handleDisableTokenHighlightingChanged() {
+    this.set(
+      '_localPrefs.disable_token_highlighting',
+      this.$.disableTokenHighlighting.checked
+    );
+  }
+
   _handleWorkInProgressByDefault() {
     this.set(
       '_localPrefs.work_in_progress_by_default',
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 167417d..78c4a62 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       color: var(--primary-text-color);
@@ -306,6 +309,19 @@
           </span>
         </section>
         <section>
+          <label for="disableTokenHighlighting" class="title"
+            >Disable token highlighting on hover</label
+          >
+          <span class="value">
+            <input
+              id="disableTokenHighlighting"
+              type="checkbox"
+              checked$="[[_localPrefs.disable_token_highlighting]]"
+              on-change="_handleDisableTokenHighlightingChanged"
+            />
+          </span>
+        </section>
+        <section>
           <label for="insertSignedOff" class="title">
             Insert Signed-off-by Footer For Inline Edit Changes
           </label>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index 19aeccb..8194d5b 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -93,7 +93,7 @@
     );
   }
 
-  setup(done => {
+  setup(async () => {
     account = {
       ...createAccountDetailWithId(123),
       name: 'user name',
@@ -126,7 +126,8 @@
     element = basicFixture.instantiate();
 
     // Allow the element to render.
-    element._testOnly_loadingPromise?.then(done);
+    if (element._testOnly_loadingPromise)
+      await element._testOnly_loadingPromise;
   });
 
   test('theme changing', () => {
@@ -164,7 +165,7 @@
     assert.equal(titleChangedStub.getCall(0).args[0].detail.title, 'Settings');
   });
 
-  test('user preferences', done => {
+  test('user preferences', async () => {
     // Rendered with the expected preferences selected.
     assert.equal(
       Number(
@@ -243,6 +244,13 @@
     );
     assert.equal(
       (
+        valueOf('Disable token highlighting on hover', 'preferences')!
+          .firstElementChild as HTMLInputElement
+      ).checked,
+      false
+    );
+    assert.equal(
+      (
         valueOf(
           'Insert Signed-off-by Footer For Inline Edit Changes',
           'preferences'
@@ -269,14 +277,12 @@
     });
 
     // Save the change.
-    element._handleSavePreferences().then(() => {
-      assert.isFalse(element._prefsChanged);
-      assert.isFalse(element._menuChanged);
-      done();
-    });
+    await element._handleSavePreferences();
+    assert.isFalse(element._prefsChanged);
+    assert.isFalse(element._menuChanged);
   });
 
-  test('publish comments on push', done => {
+  test('publish comments on push', async () => {
     const publishCommentsOnPush = valueOf(
       'Publish comments on push',
       'preferences'
@@ -292,14 +298,12 @@
     });
 
     // Save the change.
-    element._handleSavePreferences().then(() => {
-      assert.isFalse(element._prefsChanged);
-      assert.isFalse(element._menuChanged);
-      done();
-    });
+    await element._handleSavePreferences();
+    assert.isFalse(element._prefsChanged);
+    assert.isFalse(element._menuChanged);
   });
 
-  test('set new changes work-in-progress', done => {
+  test('set new changes work-in-progress', async () => {
     const newChangesWorkInProgress = valueOf(
       'Set new changes to "work in progress" by default',
       'preferences'
@@ -315,14 +319,12 @@
     });
 
     // Save the change.
-    element._handleSavePreferences().then(() => {
-      assert.isFalse(element._prefsChanged);
-      assert.isFalse(element._menuChanged);
-      done();
-    });
+    await element._handleSavePreferences();
+    assert.isFalse(element._prefsChanged);
+    assert.isFalse(element._menuChanged);
   });
 
-  test('menu', done => {
+  test('menu', async () => {
     assert.isFalse(element._menuChanged);
     assert.isFalse(element._prefsChanged);
 
@@ -349,12 +351,10 @@
       return Promise.resolve(new Response());
     });
 
-    element._handleSaveMenu().then(() => {
-      assert.isFalse(element._menuChanged);
-      assert.isFalse(element._prefsChanged);
-      assertMenusEqual(element.prefs.my, element._localMenu);
-      done();
-    });
+    await element._handleSaveMenu();
+    assert.isFalse(element._menuChanged);
+    assert.isFalse(element._prefsChanged);
+    assertMenusEqual(element.prefs.my, element._localMenu);
   });
 
   test('add email validation', () => {
@@ -388,7 +388,7 @@
     assert.isFalse(addEmailStub.called);
   });
 
-  test('add email does save valid', done => {
+  test('add email does save valid', async () => {
     const addEmailStub = stubAddAccountEmail(201);
 
     assert.isFalse(element._addingEmail);
@@ -401,13 +401,11 @@
     assert.isTrue(addEmailStub.called);
 
     assert.isTrue(addEmailStub.called);
-    addEmailStub.lastCall.returnValue.then(() => {
-      assert.isOk(element._lastSentVerificationEmail);
-      done();
-    });
+    await addEmailStub.lastCall.returnValue;
+    assert.isOk(element._lastSentVerificationEmail);
   });
 
-  test('add email does not set last-email if error', done => {
+  test('add email does not set last-email if error', async () => {
     const addEmailStub = stubAddAccountEmail(500);
 
     assert.isNotOk(element._lastSentVerificationEmail);
@@ -416,10 +414,8 @@
     element._handleAddEmailButton();
 
     assert.isTrue(addEmailStub.called);
-    addEmailStub.lastCall.returnValue.then(() => {
-      assert.isNotOk(element._lastSentVerificationEmail);
-      done();
-    });
+    await addEmailStub.lastCall.returnValue;
+    assert.isNotOk(element._lastSentVerificationEmail);
   });
 
   test('emails are loaded without emailToken', () => {
@@ -450,7 +446,7 @@
     assert.isTrue(element.prefs.legacycid_in_change_table);
   });
 
-  test('reset menu item back to default', done => {
+  test('reset menu item back to default', async () => {
     const originalMenu = {
       ...createDefaultPreferences(),
       my: [
@@ -471,10 +467,8 @@
 
     element.set('_localMenu', updatedMenu);
 
-    element._handleResetMenuButton().then(() => {
-      assertMenusEqual(element._localMenu, originalMenu.my);
-      done();
-    });
+    await element._handleResetMenuButton();
+    assertMenusEqual(element._localMenu, originalMenu.my);
   });
 
   test('test that reset button is called', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
index 8f99baa..cd2c1df 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
@@ -17,7 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-ssh-editor.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-ssh-editor');
 
@@ -25,7 +25,7 @@
   let element;
   let keys;
 
-  setup(done => {
+  setup(async () => {
     keys = [{
       seq: 1,
       ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one',
@@ -46,7 +46,8 @@
 
     element = basicFixture.instantiate();
 
-    element.loadData().then(() => { flush(done); });
+    await element.loadData();
+    await flush();
   });
 
   test('renders', () => {
@@ -61,7 +62,7 @@
     assert.equal(cells[0].textContent, keys[1].comment);
   });
 
-  test('remove key', done => {
+  test('remove key', async () => {
     const lastKey = keys[1];
 
     const saveStub = stubRestApi('deleteAccountSSHKey')
@@ -82,13 +83,11 @@
     assert.isTrue(element.hasUnsavedChanges);
     assert.isFalse(saveStub.called);
 
-    element.save().then(() => {
-      assert.isTrue(saveStub.called);
-      assert.equal(saveStub.lastCall.args[0], lastKey.seq);
-      assert.equal(element._keysToRemove.length, 0);
-      assert.isFalse(element.hasUnsavedChanges);
-      done();
-    });
+    await element.save();
+    assert.isTrue(saveStub.called);
+    assert.equal(saveStub.lastCall.args[0], lastKey.seq);
+    assert.equal(element._keysToRemove.length, 0);
+    assert.isFalse(element.hasUnsavedChanges);
   });
 
   test('show key', () => {
@@ -104,7 +103,7 @@
     assert.isTrue(openSpy.called);
   });
 
-  test('add key', done => {
+  test('add key', async () => {
     const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three';
     const newKeyObject = {
       seq: 3,
@@ -124,11 +123,12 @@
     assert.isFalse(element.$.addButton.disabled);
     assert.isFalse(element.$.newKey.disabled);
 
+    const promise = mockPromise();
     element._handleAddKey().then(() => {
       assert.isTrue(element.$.addButton.disabled);
       assert.isFalse(element.$.newKey.disabled);
       assert.equal(element._keys.length, 3);
-      done();
+      promise.resolve();
     });
 
     assert.isTrue(element.$.addButton.disabled);
@@ -136,9 +136,10 @@
 
     assert.isTrue(addStub.called);
     assert.equal(addStub.lastCall.args[0], newKeyString);
+    await promise;
   });
 
-  test('add invalid key', done => {
+  test('add invalid key', async () => {
     const newKeyString = 'not even close to valid';
 
     const addStub = stubRestApi(
@@ -150,11 +151,12 @@
     assert.isFalse(element.$.addButton.disabled);
     assert.isFalse(element.$.newKey.disabled);
 
+    const promise = mockPromise();
     element._handleAddKey().then(() => {
       assert.isFalse(element.$.addButton.disabled);
       assert.isFalse(element.$.newKey.disabled);
       assert.equal(element._keys.length, 2);
-      done();
+      promise.resolve();
     });
 
     assert.isTrue(element.$.addButton.disabled);
@@ -162,6 +164,7 @@
 
     assert.isTrue(addStub.called);
     assert.equal(addStub.lastCall.args[0], newKeyString);
+    await promise;
   });
 });
 
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
index cb4b86d..c0580f6 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.ts
@@ -28,7 +28,7 @@
 suite('gr-watched-projects-editor tests', () => {
   let element: GrWatchedProjectsEditor;
 
-  setup(done => {
+  setup(async () => {
     const projects = [
       {
         project: 'project a',
@@ -69,9 +69,8 @@
 
     element = basicFixture.instantiate();
 
-    element.loadData().then(() => {
-      flush(done);
-    });
+    await element.loadData();
+    await flush();
   });
 
   test('renders', () => {
@@ -102,27 +101,21 @@
     assert.equal(checkedKeys[2], 'notify_all_comments');
   });
 
-  test('_getProjectSuggestions empty', done => {
-    element._getProjectSuggestions('nonexistent').then(projects => {
-      assert.equal(projects.length, 0);
-      done();
-    });
+  test('_getProjectSuggestions empty', async () => {
+    const projects = await element._getProjectSuggestions('nonexistent');
+    assert.equal(projects.length, 0);
   });
 
-  test('_getProjectSuggestions non-empty', done => {
-    element._getProjectSuggestions('the project').then(projects => {
-      assert.equal(projects.length, 1);
-      assert.equal(projects[0].name, 'the project');
-      done();
-    });
+  test('_getProjectSuggestions non-empty', async () => {
+    const projects = await element._getProjectSuggestions('the project');
+    assert.equal(projects.length, 1);
+    assert.equal(projects[0].name, 'the project');
   });
 
-  test('_getProjectSuggestions non-empty with two letter project', done => {
-    element._getProjectSuggestions('th').then(projects => {
-      assert.equal(projects.length, 1);
-      assert.equal(projects[0].name, 'the project');
-      done();
-    });
+  test('_getProjectSuggestions non-empty with two letter project', async () => {
+    const projects = await element._getProjectSuggestions('th');
+    assert.equal(projects.length, 1);
+    assert.equal(projects[0].name, 'the project');
   });
 
   test('_canAddProject', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index a3650b1..31c62b1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -19,12 +19,12 @@
 import '../gr-icons/gr-icons';
 import {AccountInfo, ChangeInfo} from '../../../types/common';
 import {appContext} from '../../../services/app-context';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
-import {classMap} from 'lit-html/directives/class-map';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {classMap} from 'lit/directives/class-map';
 
 @customElement('gr-account-chip')
-export class GrAccountChip extends GrLitElement {
+export class GrAccountChip extends LitElement {
   /**
    * Fired to indicate a key was pressed while this chip was focused.
    *
@@ -128,6 +128,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         .container {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
index dc36c78..9897a9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.ts
@@ -26,13 +26,14 @@
 import {fireEvent} from '../../../utils/event-util';
 import {isInvolved} from '../../../utils/change-util';
 import {ShowAlertEventDetail} from '../../../types/events';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, state} from 'lit-element';
-import {classMap} from 'lit-html/directives/class-map';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {classMap} from 'lit/directives/class-map';
 import {modifierPressed} from '../../../utils/dom-util';
+import {getRemovedByIconClickReason} from '../../../utils/attention-set-util';
 
 @customElement('gr-account-label')
-export class GrAccountLabel extends GrLitElement {
+export class GrAccountLabel extends LitElement {
   @property({type: Object})
   account?: AccountInfo;
 
@@ -205,18 +206,7 @@
             ></gr-hovercard-account>`
           : ''}
         ${hasAttention
-          ? html`<gr-button
-              id="attentionButton"
-              link=""
-              aria-label="Remove user from attention set"
-              @click=${this._handleRemoveAttentionClick}
-              ?disabled=${!this._computeAttentionButtonEnabled(
-                highlightAttention,
-                account,
-                change,
-                this.selected,
-                this._selfAccount
-              )}
+          ? html` <gr-tooltip-content
               ?has-tooltip=${this._computeAttentionButtonEnabled(
                 highlightAttention,
                 account,
@@ -232,11 +222,25 @@
                 this.selected,
                 this._selfAccount
               )}"
-              ><iron-icon
-                class="attention"
-                icon="gr-icons:attention"
-              ></iron-icon>
-            </gr-button>`
+            >
+              <gr-button
+                id="attentionButton"
+                link=""
+                aria-label="Remove user from attention set"
+                @click=${this._handleRemoveAttentionClick}
+                ?disabled=${!this._computeAttentionButtonEnabled(
+                  highlightAttention,
+                  account,
+                  change,
+                  this.selected,
+                  this._selfAccount
+                )}
+                ><iron-icon
+                  class="attention"
+                  icon="gr-icons:attention"
+                ></iron-icon>
+              </gr-button>
+            </gr-tooltip-content>`
           : ''}
       </span>
       <span
@@ -338,8 +342,7 @@
 
     // We are deliberately updating the UI before making the API call. It is a
     // risk that we are taking to achieve a better UX for 99.9% of the cases.
-    const selfName = getDisplayName(this._config, this._selfAccount);
-    const reason = `Removed by ${selfName} by clicking the attention icon`;
+    const reason = getRemovedByIconClickReason(this._selfAccount, this._config);
     if (this.change.attention_set)
       delete this.change.attention_set[this.account._account_id];
     // For re-evaluation of everything that depends on 'change'.
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
index 97b3b34..fbf29c63 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
@@ -18,11 +18,11 @@
 import '../gr-account-label/gr-account-label';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {AccountInfo, ChangeInfo} from '../../../types/common';
-import {css, customElement, html, property} from 'lit-element';
-import {GrLitElement} from '../../lit/gr-lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 @customElement('gr-account-link')
-export class GrAccountLink extends GrLitElement {
+export class GrAccountLink extends LitElement {
   @property({type: String})
   voteableText?: string;
 
@@ -95,7 +95,7 @@
           ?hideStatus=${this.hideStatus}
           ?firstName=${this.firstName}
           .voteableText=${this.voteableText}
-          part="gr-account-link-text => gr-account-label-text"
+          exportparts="gr-account-label-text: gr-account-link-text"
         >
         </gr-account-label>
       </a>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index 220f5fd..ae3853e 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -18,8 +18,8 @@
 import '../../../styles/shared-styles';
 import {getRootElement} from '../../../scripts/rootElement';
 import {ErrorType} from '../../../types/types';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {customElement, property, css, html} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {sharedStyles} from '../../../styles/shared-styles';
 
 declare global {
@@ -29,8 +29,8 @@
 }
 
 @customElement('gr-alert')
-export class GrAlert extends GrLitElement {
-  static get styles() {
+export class GrAlert extends LitElement {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -93,9 +93,10 @@
     >`;
   }
 
-  render() {
+  override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const style = html`<style>
       .action {
         --gr-button: {
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 ac6c99e..a629d0e 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
@@ -52,11 +52,14 @@
   selected: HTMLElement | null;
 }
 
-@customElement('gr-autocomplete-dropdown')
-export class GrAutocompleteDropdown extends IronFitMixin(
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = IronFitMixin(
   KeyboardShortcutMixin(PolymerElement),
   IronFitBehavior as IronFitBehavior
-) {
+);
+
+@customElement('gr-autocomplete-dropdown')
+export class GrAutocompleteDropdown extends base {
   static get template() {
     return htmlTemplate;
   }
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 7595a17..9918f39 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -29,6 +29,7 @@
 import {CustomKeyboardEvent} from '../../../types/events';
 import {fireEvent} from '../../../utils/event-util';
 import {debounce, DelayedTask} from '../../../utils/async-util';
+import {PropertyType} from '../../../types/common';
 
 const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
 const DEBOUNCE_WAIT_MS = 200;
@@ -40,9 +41,9 @@
   };
 }
 
-export type AutocompleteQuery = (
+export type AutocompleteQuery<T = string> = (
   text: string
-) => Promise<AutocompleteSuggestion[]>;
+) => Promise<Array<AutocompleteSuggestion<T>>>;
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -50,11 +51,11 @@
   }
 }
 
-export interface AutocompleteSuggestion {
+export interface AutocompleteSuggestion<T = string> {
   name?: string;
   label?: string;
-  value?: string;
-  text?: string;
+  value?: T;
+  text?: T;
 }
 
 export interface AutocompleteCommitEventDetail {
@@ -64,8 +65,11 @@
 export type AutocompleteCommitEvent =
   CustomEvent<AutocompleteCommitEventDetail>;
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-autocomplete')
-export class GrAutocomplete extends KeyboardShortcutMixin(PolymerElement) {
+export class GrAutocomplete extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -99,7 +103,7 @@
    *
    */
   @property({type: Object})
-  query: AutocompleteQuery = () => Promise.resolve([]);
+  query?: AutocompleteQuery = () => Promise.resolve([]);
 
   /**
    * The number of characters that must be typed before suggestions are
@@ -295,6 +299,12 @@
     if (this._disableSuggestions) {
       return;
     }
+
+    const query = this.query;
+    if (!query) {
+      return;
+    }
+
     if (text.length < threshold) {
       this.value = '';
       return;
@@ -305,7 +315,7 @@
     }
 
     const update = () => {
-      this.query(text).then(suggestions => {
+      query(text).then(suggestions => {
         if (text !== this.text) {
           // Late response.
           return;
@@ -502,3 +512,24 @@
     return showSearchIcon ? 'showSearchIcon' : '';
   }
 }
+
+/**
+ * Often gr-autocomplete is used for BranchName, RepoName, etc...
+ * GrTypedAutocomplete allows to define more precise typing in templates.
+ * For example, instead of
+ * $: {
+ *   branchSelect: GrAutocomplete
+ * }
+ * you can write
+ * $: {
+ *   branchSelect: GrTypedAutocomplete<BranchName>
+ * }
+ * And later user $.branchSelect.text without type conversion to BranchName.
+ */
+export interface GrTypedAutocomplete<
+  T extends PropertyType<GrAutocomplete, 'text'>
+> extends GrAutocomplete {
+  text: T;
+  value: T;
+  query?: AutocompleteQuery<T>;
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
index 6dd3a00..33bf6c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -18,11 +18,11 @@
 import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
 import {AccountInfo} from '../../../types/common';
 import {appContext} from '../../../services/app-context';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 @customElement('gr-avatar')
-export class GrAvatar extends GrLitElement {
+export class GrAvatar extends LitElement {
   @property({type: Object})
   account?: AccountInfo;
 
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
index bb70855..b3c485a 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
@@ -24,6 +24,7 @@
 import {
   createAccountWithEmail,
   createAccountWithId,
+  createServerInfo,
 } from '../../../test/test-data-generators';
 
 const basicFixture = fixtureFromElement('gr-avatar');
@@ -116,9 +117,11 @@
 
   suite('config set', () => {
     setup(() => {
-      stub('gr-avatar', '_getConfig').callsFake(() =>
-        Promise.resolve({plugin: {has_avatars: true}})
-      );
+      const config = {
+        ...createServerInfo(),
+        plugin: {has_avatars: true, js_resource_paths: []},
+      };
+      stub('gr-avatar', '_getConfig').returns(Promise.resolve(config));
       element = basicFixture.instantiate();
     });
 
@@ -154,9 +157,11 @@
     let element: GrAvatar;
 
     setup(() => {
-      stub('gr-avatar', '_getConfig').callsFake(() =>
-        Promise.resolve({plugin: {has_avatars: true}})
-      );
+      const config = {
+        ...createServerInfo(),
+        plugin: {has_avatars: true, js_resource_paths: []},
+      };
+      stub('gr-avatar', '_getConfig').returns(Promise.resolve(config));
 
       element = basicFixture.instantiate();
     });
@@ -182,7 +187,7 @@
     let element: GrAvatar;
 
     setup(() => {
-      stub('gr-avatar', '_getConfig').callsFake(() => Promise.resolve({}));
+      stub('gr-avatar', '_getConfig').returns(Promise.resolve(undefined));
 
       element = basicFixture.instantiate();
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index aef55fa..4467ba6 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -15,13 +15,11 @@
  * limitations under the License.
  */
 import '@polymer/paper-button/paper-button';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property, computed, observe} from '@polymer/decorators';
-import {htmlTemplate} from './gr-button_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
+import {votingStyles} from '../../../styles/gr-voting-styles';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {
-  PolymerEvent,
   getEventPath,
   getKeyboardEvent,
   isModifierPressed,
@@ -37,72 +35,242 @@
 }
 
 @customElement('gr-button')
-export class GrButton extends TooltipMixin(PolymerElement) {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrButton extends LitElement {
+  private readonly reporting: ReportingService = appContext.reportingService;
 
-  @property({type: Boolean, reflectToAttribute: true})
-  downArrow = false;
-
-  @property({type: Boolean, reflectToAttribute: true})
-  link = false;
-
-  @property({type: Boolean})
-  noUppercase = false;
-
-  @property({type: Boolean, reflectToAttribute: true})
-  loading = false;
-
-  @property({type: Boolean, reflectToAttribute: true})
-  disabled: boolean | null = null;
-
-  @property({type: String})
-  tooltip = '';
+  /**
+   * Should this button be rendered as a vote chip? Then we are applying
+   * the .voteChip class (see gr-voting-styles) to the paper-button.
+   */
+  @property({type: Boolean, reflect: true})
+  voteChip = false;
 
   // Note: don't assign a value to this, since constructor is called
   // after created, the initial value maybe overridden by this
-  @property({type: String})
-  _initialTabindex?: string;
+  private initialTabindex?: string;
 
-  @computed('disabled', 'loading')
-  get _disabled() {
-    return this.disabled || this.loading;
+  @property({type: Boolean, reflect: true, attribute: 'down-arrow'})
+  downArrow = false;
+
+  @property({type: Boolean, reflect: true})
+  link = false;
+
+  @property({type: Boolean, reflect: true})
+  loading = false;
+
+  @property({type: Boolean, reflect: true})
+  disabled: boolean | null = null;
+
+  static override get styles() {
+    return [
+      votingStyles,
+      spinnerStyles,
+      css`
+        /* general styles for all buttons */
+        :host {
+          --background-color: var(
+            --button-background-color,
+            var(--default-button-background-color)
+          );
+          --text-color: var(--default-button-text-color);
+          display: inline-block;
+          position: relative;
+        }
+        :host([hidden]) {
+          display: none;
+        }
+        :host([no-uppercase]) paper-button {
+          text-transform: none;
+        }
+        paper-button {
+          /* The next lines contains a copy of paper-button style.
+            Without a copy, the @apply works incorrectly with Polymer 2.
+            @apply is deprecated and is not recommended to use. It is expected
+            that @apply will be replaced with the ::part CSS pseudo-element.
+            After replacement copied lines can be removed.
+          */
+          @apply --layout-inline;
+          @apply --layout-center-center;
+          position: relative;
+          box-sizing: border-box;
+          min-width: 5.14em;
+          margin: 0 0.29em;
+          background: transparent;
+          -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+          -webkit-tap-highlight-color: transparent;
+          font: inherit;
+          text-transform: uppercase;
+          outline-width: 0;
+          border-top-left-radius: var(--border-radius);
+          border-top-right-radius: var(--border-radius);
+          border-bottom-right-radius: var(--border-radius);
+          border-bottom-left-radius: var(--border-radius);
+          -moz-user-select: none;
+          -ms-user-select: none;
+          -webkit-user-select: none;
+          user-select: none;
+          cursor: pointer;
+          z-index: 0;
+          padding: var(--spacing-m);
+
+          @apply --paper-font-common-base;
+          @apply --paper-button;
+          /* End of copy*/
+
+          /* paper-button sets this to anti-aliased, which appears different than
+            bold font elsewhere on macOS. */
+          -webkit-font-smoothing: initial;
+          align-items: center;
+          background-color: var(--background-color);
+          color: var(--text-color);
+          display: flex;
+          font-family: inherit;
+          justify-content: center;
+          margin: var(--margin, 0);
+          min-width: var(--border, 0);
+          padding: var(--padding, 4px 8px);
+          @apply --gr-button;
+        }
+        /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
+        /* BEGIN: Copy from paper-button */
+        paper-button[elevation='1'] {
+          @apply --paper-material-elevation-1;
+        }
+        paper-button[elevation='2'] {
+          @apply --paper-material-elevation-2;
+        }
+        paper-button[elevation='3'] {
+          @apply --paper-material-elevation-3;
+        }
+        paper-button[elevation='4'] {
+          @apply --paper-material-elevation-4;
+        }
+        paper-button[elevation='5'] {
+          @apply --paper-material-elevation-5;
+        }
+        /* END: Copy from paper-button */
+        paper-button:hover {
+          background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
+            var(--background-color);
+        }
+
+        /* Some mobile browsers treat focused element as hovered element.
+        As a result, element remains hovered after click (has grey background in default theme).
+        Use @media (hover:none) to remove background if
+        user's primary input mechanism can't hover over elements.
+        See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
+
+        Note 1: not all browsers support this media query
+        (see https://caniuse.com/#feat=css-media-interaction).
+        If browser doesn't support it, then the whole content of @media .. is ignored.
+        This is why the default behavior is placed outside of @media.
+        */
+        @media (hover: none) {
+          paper-button:hover {
+            background: transparent;
+          }
+        }
+
+        :host([primary]) {
+          --background-color: var(--primary-button-background-color);
+          --text-color: var(--primary-button-text-color);
+        }
+        :host([link][primary]) {
+          --text-color: var(--primary-button-background-color);
+        }
+
+        /* Keep below color definition for primary so that this takes precedence
+          when disabled. */
+        :host([disabled]),
+        :host([loading]) {
+          --background-color: var(--disabled-button-background-color);
+          --text-color: var(--deemphasized-text-color);
+          cursor: default;
+        }
+
+        /* Styles for link buttons specifically */
+        :host([link]) {
+          --background-color: transparent;
+          --margin: 0;
+          --padding: var(--spacing-s);
+        }
+        :host([disabled][link]),
+        :host([loading][link]) {
+          --background-color: transparent;
+          --text-color: var(--deemphasized-text-color);
+          cursor: default;
+        }
+
+        /* Styles for the optional down arrow */
+        :host(:not([down-arrow])) .downArrow {
+          display: none;
+        }
+        :host([down-arrow]) .downArrow {
+          border-top: 0.36em solid #ccc;
+          border-left: 0.36em solid transparent;
+          border-right: 0.36em solid transparent;
+          margin-bottom: var(--spacing-xxs);
+          margin-left: var(--spacing-m);
+          transition: border-top-color 200ms;
+        }
+        :host([down-arrow]) paper-button:hover .downArrow {
+          border-top-color: var(--deemphasized-text-color);
+        }
+      `,
+    ];
   }
 
-  @property({
-    computed: 'computeAriaDisabled(disabled, loading)',
-    reflectToAttribute: true,
-    type: Boolean,
-  })
-  ariaDisabled!: boolean;
-
-  computeAriaDisabled() {
-    return this._disabled;
+  override render() {
+    return html`<paper-button
+      ?raised="${!this.link}"
+      ?disabled="${this.disabled || this.loading}"
+      role="button"
+      tabindex="-1"
+      part="paper-button"
+      class="${this.voteChip ? 'voteChip' : ''}"
+    >
+      ${this.loading ? html`<span class="loadingSpin"></span>` : ''}
+      <slot></slot>
+      <i class="downArrow"></i>
+    </paper-button>`;
   }
 
-  private readonly reporting: ReportingService = appContext.reportingService;
-
   constructor() {
     super();
-    this._initialTabindex = this.getAttribute('tabindex') || '0';
-    // TODO(TS): try avoid using unknown
-    this.addEventListener('click', e =>
-      this._handleAction(e as unknown as PolymerEvent)
-    );
+    this.initialTabindex = this.getAttribute('tabindex') || '0';
+    this.addEventListener('click', e => this._handleAction(e));
     this.addEventListener('keydown', e =>
       this._handleKeydown(e as unknown as CustomKeyboardEvent)
     );
   }
 
-  override ready() {
-    super.ready();
-    this._ensureAttribute('role', 'button');
-    this._ensureAttribute('tabindex', '0');
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('disabled')) {
+      this.setAttribute(
+        'tabindex',
+        this.disabled ? '-1' : this.initialTabindex || '0'
+      );
+    }
+    if (changedProperties.has('loading') || changedProperties.has('disabled')) {
+      this.setAttribute(
+        'aria-disabled',
+        this.disabled || this.loading ? 'true' : 'false'
+      );
+    }
   }
 
-  _handleAction(e: PolymerEvent) {
-    if (this._disabled) {
+  override connectedCallback() {
+    super.connectedCallback();
+    if (!this.getAttribute('role')) {
+      this.setAttribute('role', 'button');
+    }
+    if (!this.getAttribute('tabindex')) {
+      this.setAttribute('tabindex', '0');
+    }
+  }
+
+  _handleAction(e: MouseEvent) {
+    if (this.disabled || this.loading) {
       e.preventDefault();
       e.stopPropagation();
       e.stopImmediatePropagation();
@@ -112,15 +280,6 @@
     this.reporting.reportInteraction('button-click', {path: getEventPath(e)});
   }
 
-  @observe('disabled')
-  _disabledChanged(disabled: boolean) {
-    this.setAttribute(
-      'tabindex',
-      disabled ? '-1' : this._initialTabindex || '0'
-    );
-    this.updateStyles();
-  }
-
   _handleKeydown(e: CustomKeyboardEvent) {
     if (isModifierPressed(e)) {
       return;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
deleted file mode 100644
index a15b560..0000000
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="gr-spinner-styles">
-    /* general styles for all buttons */
-    :host {
-      --background-color: var(
-        --button-background-color,
-        var(--default-button-background-color)
-      );
-      --text-color: var(--default-button-text-color);
-      display: inline-block;
-      position: relative;
-    }
-    :host([hidden]) {
-      display: none;
-    }
-    :host([no-uppercase]) paper-button {
-      text-transform: none;
-    }
-    paper-button {
-      /* The next lines contains a copy of paper-button style.
-          Without a copy, the @apply works incorrectly with Polymer 2.
-          @apply is deprecated and is not recommended to use. It is expected
-          that @apply will be replaced with the ::part CSS pseudo-element.
-          After replacement copied lines can be removed.
-        */
-      @apply --layout-inline;
-      @apply --layout-center-center;
-      position: relative;
-      box-sizing: border-box;
-      min-width: 5.14em;
-      margin: 0 0.29em;
-      background: transparent;
-      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-      -webkit-tap-highlight-color: transparent;
-      font: inherit;
-      text-transform: uppercase;
-      outline-width: 0;
-      border-top-left-radius: var(--border-radius);
-      border-top-right-radius: var(--border-radius);
-      border-bottom-right-radius: var(--border-radius);
-      border-bottom-left-radius: var(--border-radius);
-      -moz-user-select: none;
-      -ms-user-select: none;
-      -webkit-user-select: none;
-      user-select: none;
-      cursor: pointer;
-      z-index: 0;
-      padding: var(--spacing-m);
-
-      @apply --paper-font-common-base;
-      @apply --paper-button;
-      /* End of copy*/
-
-      /* paper-button sets this to anti-aliased, which appears different than
-          bold font elsewhere on macOS. */
-      -webkit-font-smoothing: initial;
-      align-items: center;
-      background-color: var(--background-color);
-      color: var(--text-color);
-      display: flex;
-      font-family: inherit;
-      justify-content: center;
-      margin: var(--margin, 0);
-      min-width: var(--border, 0);
-      padding: var(--padding, 4px 8px);
-      @apply --gr-button;
-    }
-    /* https://github.com/PolymerElements/paper-button/blob/2.x/paper-button.html */
-    /* BEGIN: Copy from paper-button */
-    paper-button[elevation='1'] {
-      @apply --paper-material-elevation-1;
-    }
-    paper-button[elevation='2'] {
-      @apply --paper-material-elevation-2;
-    }
-    paper-button[elevation='3'] {
-      @apply --paper-material-elevation-3;
-    }
-    paper-button[elevation='4'] {
-      @apply --paper-material-elevation-4;
-    }
-    paper-button[elevation='5'] {
-      @apply --paper-material-elevation-5;
-    }
-    /* END: Copy from paper-button */
-    paper-button:hover {
-      background: linear-gradient(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.12)),
-        var(--background-color);
-    }
-
-    /* Some mobile browsers treat focused element as hovered element.
-      As a result, element remains hovered after click (has grey background in default theme).
-      Use @media (hover:none) to remove background if
-      user's primary input mechanism can't hover over elements.
-      See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover
-
-      Note 1: not all browsers support this media query
-      (see https://caniuse.com/#feat=css-media-interaction).
-      If browser doesn't support it, then the whole content of @media .. is ignored.
-      This is why the default behavior is placed outside of @media.
-      */
-    @media (hover: none) {
-      paper-button:hover {
-        background: transparent;
-      }
-    }
-
-    :host([primary]) {
-      --background-color: var(--primary-button-background-color);
-      --text-color: var(--primary-button-text-color);
-    }
-    :host([link][primary]) {
-      --text-color: var(--primary-button-background-color);
-    }
-
-    /* Keep below color definition for primary so that this takes precedence
-        when disabled. */
-    :host([disabled]),
-    :host([loading]) {
-      --background-color: var(--disabled-button-background-color);
-      --text-color: var(--deemphasized-text-color);
-      cursor: default;
-    }
-
-    /* Styles for link buttons specifically */
-    :host([link]) {
-      --background-color: transparent;
-      --margin: 0;
-      --padding: var(--spacing-s);
-    }
-    :host([disabled][link]),
-    :host([loading][link]) {
-      --background-color: transparent;
-      --text-color: var(--deemphasized-text-color);
-      cursor: default;
-    }
-
-    /* Styles for the optional down arrow */
-    :host(:not([down-arrow])) .downArrow {
-      display: none;
-    }
-    :host([down-arrow]) .downArrow {
-      border-top: 0.36em solid #ccc;
-      border-left: 0.36em solid transparent;
-      border-right: 0.36em solid transparent;
-      margin-bottom: var(--spacing-xxs);
-      margin-left: var(--spacing-m);
-      transition: border-top-color 200ms;
-    }
-    :host([down-arrow]) paper-button:hover .downArrow {
-      border-top-color: var(--deemphasized-text-color);
-    }
-  </style>
-  <paper-button
-    raised="[[!link]]"
-    disabled="[[_disabled]]"
-    tabindex="-1"
-    part="paper-button"
-  >
-    <template is="dom-if" if="[[loading]]">
-      <span class="loadingSpin"></span>
-    </template>
-    <slot></slot>
-    <i class="downArrow"></i>
-  </paper-button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
index f0f122a..0149bd5 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
@@ -17,6 +17,7 @@
 
 import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 import '../../../test/common-test-setup-karma';
+import './gr-button';
 import {addListener} from '@polymer/polymer/lib/utils/gestures';
 import {appContext} from '../../../services/app-context';
 import {html} from '@polymer/polymer/lib/utils/html-tag';
@@ -49,23 +50,26 @@
     return spy;
   };
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
-  test('disabled is set by disabled', () => {
+  test('disabled is set by disabled', async () => {
     const paperBtn = queryAndAssert<PaperButtonElement>(
       element,
       'paper-button'
     );
     assert.isFalse(paperBtn.disabled);
     element.disabled = true;
+    await element.updateComplete;
     assert.isTrue(paperBtn.disabled);
     element.disabled = false;
+    await element.updateComplete;
     assert.isFalse(paperBtn.disabled);
   });
 
-  test('loading set from listener', () => {
+  test('loading set from listener', async () => {
     let resolve: Function;
     element.addEventListener('click', e => {
       const target = e.target as HTMLElement;
@@ -78,36 +82,44 @@
     );
     assert.isFalse(paperBtn.disabled);
     MockInteractions.tap(element);
+    await element.updateComplete;
     assert.isTrue(paperBtn.disabled);
     assert.isTrue(element.hasAttribute('loading'));
     resolve!();
-    flush();
+    await element.updateComplete;
     assert.isFalse(paperBtn.disabled);
     assert.isFalse(element.hasAttribute('loading'));
   });
 
-  test('tabindex should be -1 if disabled', () => {
+  test('tabindex should be -1 if disabled', async () => {
     element.disabled = true;
-    assert.isTrue(element.getAttribute('tabindex') === '-1');
+    await element.updateComplete;
+    assert.equal(element.getAttribute('tabindex'), '-1');
   });
 
   // Regression tests for Issue: 11969
-  test('tabindex should be reset to 0 if enabled', () => {
+  test('tabindex should be reset to 0 if enabled', async () => {
     element.disabled = false;
+    await element.updateComplete;
     assert.equal(element.getAttribute('tabindex'), '0');
     element.disabled = true;
+    await element.updateComplete;
     assert.equal(element.getAttribute('tabindex'), '-1');
     element.disabled = false;
+    await element.updateComplete;
     assert.equal(element.getAttribute('tabindex'), '0');
   });
 
-  test('tabindex should be preserved', () => {
+  test('tabindex should be preserved', async () => {
     const tabIndexElement = tabindexFixture.instantiate() as GrButton;
     tabIndexElement.disabled = false;
+    await element.updateComplete;
     assert.equal(tabIndexElement.getAttribute('tabindex'), '3');
     tabIndexElement.disabled = true;
+    await element.updateComplete;
     assert.equal(tabIndexElement.getAttribute('tabindex'), '-1');
     tabIndexElement.disabled = false;
+    await element.updateComplete;
     assert.equal(tabIndexElement.getAttribute('tabindex'), '3');
   });
 
@@ -152,8 +164,9 @@
   }
 
   suite('disabled', () => {
-    setup(() => {
+    setup(async () => {
       element.disabled = true;
+      await element.updateComplete;
     });
 
     for (const eventName of ['tap', 'click']) {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 2ca2744b..455bd4e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
@@ -80,8 +80,8 @@
     }
   </style>
   <gr-tooltip-content
-    has-tooltip=""
-    position-below=""
+    has-tooltip
+    position-below
     title="[[tooltipText]]"
     max-width="40em"
   >
@@ -101,9 +101,8 @@
       </a>
     </template>
     <template is="dom-if" if="[[!hasStatusLink(revertedChange, resolveWeblinks, status)]]">
-      <div class="chip" aria-label$="Label: [[status]]">
-        [[_computeStatusString(status)]]
-      </div>
+      <div class="chip" aria-label$="Label: [[status]]"
+      >[[_computeStatusString(status)]]</div>
     </template>
   </gr-tooltip-content>
 </span>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
index a56f6f1..39fc7c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import sinon from 'sinon/pkg/sinon-esm';
 import '../../../test/common-test-setup-karma';
 import {createChange} from '../../../test/test-data-generators';
 import './gr-change-status';
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 6745f58..fe4a66a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-a11y-styles';
 import '../../../styles/shared-styles';
 import '../gr-comment/gr-comment';
 import '../../diff/gr-diff/gr-diff';
@@ -56,8 +57,12 @@
 import {GrButton} from '../gr-button/gr-button';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {RenderPreferences} from '../../../api/diff';
-import {check, assertIsDefined} from '../../../utils/common-util';
+import {DiffLayer, RenderPreferences} from '../../../api/diff';
+import {
+  check,
+  assertIsDefined,
+  queryAndAssert,
+} from '../../../utils/common-util';
 import {fireAlert, waitForEventOnce} from '../../../utils/event-util';
 import {GrSyntaxLayer} from '../../diff/gr-syntax-layer/gr-syntax-layer';
 import {StorageLocation} from '../../../services/storage/gr-storage';
@@ -76,8 +81,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-comment-thread')
-export class GrCommentThread extends KeyboardShortcutMixin(PolymerElement) {
+export class GrCommentThread extends base {
   // KeyboardShortcutMixin Not used in this element rather other elements tests
 
   static get template() {
@@ -197,6 +205,9 @@
   @property({type: Object})
   _selfAccount?: AccountDetailInfo;
 
+  @property({type: Array})
+  layers: DiffLayer[] = [];
+
   get keyBindings() {
     return {
       'e shift+e': '_handleEKey',
@@ -220,6 +231,9 @@
     this.addEventListener('comment-update', e =>
       this._handleCommentUpdate(e as CustomEvent)
     );
+    appContext.restApiService.getPreferences().then(prefs => {
+      this._initLayers(!!prefs?.disable_token_highlighting);
+    });
   }
 
   override connectedCallback() {
@@ -267,6 +281,7 @@
       const resizeObserver = new ResizeObserver(
         (_entries: ResizeObserverEntry[], observer: ResizeObserver) => {
           if (this.offsetHeight > 0) {
+            queryAndAssert<HTMLDivElement>(this, '.comment-box').focus();
             this.scrollIntoView();
           }
           observer.unobserve(this);
@@ -312,7 +327,6 @@
     draft.__editing = true;
     draft.unresolved = unresolved === false ? unresolved : true;
     this.commentsService.addDraft(draft);
-    this.push('comments', draft);
   }
 
   _getDiffUrlForPath(
@@ -350,14 +364,14 @@
     return undefined;
   }
 
-  _getLayers(diff?: DiffInfo) {
-    if (!diff) return [];
-    const layers = [];
-    if (this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING)) {
-      layers.push(new TokenHighlightLayer());
+  _initLayers(disableTokenHighlighting: boolean) {
+    if (
+      this.flagsService.isEnabled(KnownExperimentId.TOKEN_HIGHLIGHTING) &&
+      !disableTokenHighlighting
+    ) {
+      this.layers.push(new TokenHighlightLayer(this));
     }
-    layers.push(this.syntaxLayer);
-    return layers;
+    this.layers.push(this.syntaxLayer);
   }
 
   _getUrlForViewDiff(
@@ -548,14 +562,11 @@
     }
 
     this.commentsService.addDraft(reply);
-    this.push('comments', reply);
 
     if (!isEditing) {
-      // Allow the reply to render in the dom-repeat.
-      setTimeout(() => {
-        const commentEl = this._commentElWithDraftID(reply.__draftID);
-        if (commentEl) commentEl.save();
-      }, 1);
+      assertIsDefined(this.changeNum, 'changeNum');
+      assertIsDefined(this.patchNum, 'patchNum');
+      this.restApiService.saveDiffDraft(this.changeNum, this.patchNum, reply);
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index 5c01467..245f71c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-a11y-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       font-family: var(--font-family);
@@ -143,7 +146,10 @@
     <h3 class="assistive-tech-only">
       [[_computeAriaHeading(_orderedComments)]]
     </h3>
-    <div class$="[[_computeHostClass(unresolved, isRobotComment)]] comment-box">
+    <div
+      class$="[[_computeHostClass(unresolved, isRobotComment)]] comment-box"
+      tabindex="0"
+    >
       <template
         id="commentList"
         is="dom-repeat"
@@ -226,7 +232,7 @@
           id="diff"
           change-num="[[changeNum]]"
           diff="[[_diff]]"
-          layers="[[_getLayers(_diff)]]"
+          layers="[[layers]]"
           path="[[path]]"
           prefs="[[_prefs]]"
           render-prefs="[[_renderPrefs]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 5f860f4..ecd9731 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -23,7 +23,6 @@
   sortComments,
   UIComment,
   UIRobot,
-  isDraft,
   UIDraft,
 } from '../../../utils/comment-util';
 import {GrCommentThread} from './gr-comment-thread';
@@ -46,11 +45,13 @@
 } from '@polymer/iron-test-helpers/mock-interactions';
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 import {
+  mockPromise,
   stubComments,
   stubReporting,
   stubRestApi,
 } from '../../../test/test-utils';
 import {_testOnly_resetState} from '../../../services/comments/comments-model';
+import {SinonStub} from 'sinon';
 
 const basicFixture = fixtureFromElement('gr-comment-thread');
 
@@ -235,16 +236,14 @@
       assert.equal(element._hideActions(showActions, robotComment), true);
     });
 
-    test('setting project name loads the project config', done => {
+    test('setting project name loads the project config', async () => {
       const projectName = 'foo/bar/baz' as RepoName;
       const getProjectStub = stubRestApi('getProjectConfig').returns(
         Promise.resolve({} as ConfigInfo)
       );
       element.projectName = projectName;
-      flush(() => {
-        assert.isTrue(getProjectStub.calledWithExactly(projectName as never));
-        done();
-      });
+      await flush();
+      assert.isTrue(getProjectStub.calledWithExactly(projectName as never));
     });
 
     test('optionally show file path', () => {
@@ -314,10 +313,12 @@
 
 suite('comment action tests with unresolved thread', () => {
   let element: GrCommentThread;
-
+  let addDraftServiceStub: SinonStub;
+  let saveDiffDraftStub: SinonStub;
   setup(() => {
+    addDraftServiceStub = stubComments('addDraft');
     stubRestApi('getLoggedIn').returns(Promise.resolve(false));
-    stubRestApi('saveDiffDraft').returns(
+    saveDiffDraftStub = stubRestApi('saveDiffDraft').returns(
       Promise.resolve({
         headers: {} as Headers,
         redirected: false,
@@ -373,12 +374,11 @@
     const replyBtn = element.$.replyBtn;
     tap(replyBtn);
     flush();
-
-    const drafts = element._orderedComments.filter(c => isDraft(c));
-    assert.equal(drafts.length, 1);
-    assert.notOk(drafts[0].message, 'message should be empty');
+    const draft = addDraftServiceStub.firstCall.args[0];
+    assert.isOk(draft);
+    assert.notOk(draft.message, 'message should be empty');
     assert.equal(
-      drafts[0].in_reply_to,
+      draft.in_reply_to,
       'baf0414d_60047215' as UrlEncodedCommentId as UrlEncodedCommentId
     );
     assert.isTrue(reportStub.calledOnce);
@@ -393,11 +393,10 @@
     tap(quoteBtn);
     flush();
 
-    const drafts = element._orderedComments.filter(c => isDraft(c));
-    assert.equal(drafts.length, 1);
-    assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
+    const draft = addDraftServiceStub.firstCall.args[0];
+    assert.equal(draft.message, '> is this a crossover episode!?\n\n');
     assert.equal(
-      drafts[0].in_reply_to,
+      draft.in_reply_to,
       'baf0414d_60047215' as UrlEncodedCommentId as UrlEncodedCommentId
     );
     assert.isTrue(reportStub.calledOnce);
@@ -427,20 +426,16 @@
     tap(quoteBtn);
     flush();
 
-    const drafts = element._orderedComments.filter(c => isDraft(c));
-    assert.equal(drafts.length, 1);
+    const draft = addDraftServiceStub.firstCall.args[0];
     assert.equal(
-      drafts[0].message,
+      draft.message,
       '> is this a crossover episode!?\n> It might be!\n\n'
     );
-    assert.equal(
-      drafts[0].in_reply_to,
-      'baf0414d_60047215' as UrlEncodedCommentId
-    );
+    assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
     assert.isTrue(reportStub.calledOnce);
   });
 
-  test('ack', done => {
+  test('ack', async () => {
     const reportStub = stubReporting('recordDraftInteraction');
     element.changeNum = 42 as NumericChangeId;
     element.patchNum = 1 as PatchSetNum;
@@ -451,22 +446,17 @@
     const ackBtn = element.shadowRoot?.querySelector('#ackBtn');
     assert.isOk(ackBtn);
     tap(ackBtn!);
-    flush(() => {
-      const drafts = element.comments.filter(c => isDraft(c));
-      assert.equal(drafts.length, 1);
-      assert.equal(drafts[0].message, 'Ack');
-      assert.equal(
-        drafts[0].in_reply_to,
-        'baf0414d_60047215' as UrlEncodedCommentId
-      );
-      assert.equal(drafts[0].unresolved, false);
-      assert.isTrue(reportStub.calledOnce);
-      done();
-    });
+    await flush();
+    const draft = addDraftServiceStub.firstCall.args[0];
+    assert.equal(draft.message, 'Ack');
+    assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
+    assert.equal(draft.unresolved, false);
+    assert.isTrue(reportStub.calledOnce);
   });
 
-  test('done', done => {
+  test('done', async () => {
     const reportStub = stubReporting('recordDraftInteraction');
+    assert.isFalse(saveDiffDraftStub.called);
     element.changeNum = 42 as NumericChangeId;
     element.patchNum = 1 as PatchSetNum;
     const commentEl = element.shadowRoot?.querySelector('gr-comment');
@@ -475,21 +465,16 @@
     const doneBtn = element.shadowRoot?.querySelector('#doneBtn');
     assert.isOk(doneBtn);
     tap(doneBtn!);
-    flush(() => {
-      const drafts = element.comments.filter(c => isDraft(c));
-      assert.equal(drafts.length, 1);
-      assert.equal(drafts[0].message, 'Done');
-      assert.equal(
-        drafts[0].in_reply_to,
-        'baf0414d_60047215' as UrlEncodedCommentId
-      );
-      assert.isFalse(drafts[0].unresolved);
-      assert.isTrue(reportStub.calledOnce);
-      done();
-    });
+    await flush();
+    const draft = addDraftServiceStub.firstCall.args[0];
+    assert.equal(draft.message, 'Done');
+    assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
+    assert.isFalse(draft.unresolved);
+    assert.isTrue(reportStub.calledOnce);
+    assert.isTrue(saveDiffDraftStub.called);
   });
 
-  test('save', done => {
+  test('save', async () => {
     element.changeNum = 42 as NumericChangeId;
     element.patchNum = 1 as PatchSetNum;
     element.path = '/path/to/file.txt';
@@ -498,30 +483,28 @@
 
     element.shadowRoot?.querySelector('gr-comment')?._fireSave();
 
-    flush(() => {
-      assert.equal(element.rootId, 'baf0414d_60047215' as UrlEncodedCommentId);
-      done();
-    });
+    await flush();
+    assert.equal(element.rootId, 'baf0414d_60047215' as UrlEncodedCommentId);
   });
 
-  test('please fix', done => {
+  test('please fix', async () => {
     element.changeNum = 42 as NumericChangeId;
     element.patchNum = 1 as PatchSetNum;
     const commentEl = element.shadowRoot?.querySelector('gr-comment');
     assert.ok(commentEl);
+    const promise = mockPromise();
     commentEl!.addEventListener('create-fix-comment', () => {
-      const drafts = element._orderedComments.filter(c => isDraft(c));
-      assert.equal(drafts.length, 1);
+      const draft = addDraftServiceStub.firstCall.args[0];
       assert.equal(
-        drafts[0].message,
+        draft.message,
         '> is this a crossover episode!?\n\nPlease fix.'
       );
       assert.equal(
-        drafts[0].in_reply_to,
+        draft.in_reply_to,
         'baf0414d_60047215' as UrlEncodedCommentId
       );
-      assert.isTrue(drafts[0].unresolved);
-      done();
+      assert.isTrue(draft.unresolved);
+      promise.resolve();
     });
     commentEl!.dispatchEvent(
       new CustomEvent('create-fix-comment', {
@@ -530,9 +513,10 @@
         bubbles: false,
       })
     );
+    await promise;
   });
 
-  test('discard', done => {
+  test('discard', async () => {
     element.changeNum = 42 as NumericChangeId;
     element.patchNum = 1 as PatchSetNum;
     element.path = '/path/to/file.txt';
@@ -545,37 +529,43 @@
         'it’s pronouced jiff, not giff'
       )
     );
-    flush();
+    await flush();
 
     const draftEl = element.root?.querySelectorAll('gr-comment')[1];
     assert.ok(draftEl);
     draftEl?._fireSave(); // tell the model about the draft
+    const promise = mockPromise();
     draftEl!.addEventListener('comment-discard', () => {
-      flush();
       assert.isTrue(deleteDraftStub.called);
-      done();
+      promise.resolve();
     });
     draftEl!._fireDiscard();
+    await promise;
   });
 
-  test('discard with a single comment still fires event with previous rootId', done => {
+  test('discard with a single comment still fires event with previous rootId', async () => {
     element.changeNum = 42 as NumericChangeId;
     element.patchNum = 1 as PatchSetNum;
     element.path = '/path/to/file.txt';
     element.comments = [];
     element.addOrEditDraft(1 as LineNumber);
+    const draft = addDraftServiceStub.firstCall.args[0];
+    element.comments = [draft];
     flush();
     const rootId = element.rootId;
     assert.isOk(rootId);
-
+    flush();
     const draftEl = element.root?.querySelectorAll('gr-comment')[0];
     assert.ok(draftEl);
     const deleteDraftStub = stubComments('deleteDraft');
+    const promise = mockPromise();
     draftEl!.addEventListener('comment-discard', () => {
       assert.isTrue(deleteDraftStub.called);
-      done();
+      promise.resolve();
     });
     draftEl!._fireDiscard();
+    await promise;
+    assert.isTrue(deleteDraftStub.called);
   });
 
   test('comment-update', () => {
@@ -644,6 +634,8 @@
 
     test('comment in_reply_to is either null or most recent comment', () => {
       element._createReplyComment('dummy', true);
+      const draft = addDraftServiceStub.firstCall.args[0];
+      element.comments = [...element.comments, draft];
       flush();
       assert.equal(element._orderedComments.length, 5);
       assert.equal(
@@ -655,6 +647,8 @@
     test('resolvable comments', () => {
       assert.isFalse(element.unresolved);
       element._createReplyComment('dummy', true, true);
+      const draft = addDraftServiceStub.firstCall.args[0];
+      element.comments = [...element.comments, draft];
       flush();
       assert.isTrue(element.unresolved);
     });
@@ -703,18 +697,23 @@
 
   test('addDraft sets unresolved state correctly', () => {
     let unresolved = true;
+    let draft;
     element.comments = [];
     element.path = 'abcd';
     element.addDraft(undefined, undefined, unresolved);
-    assert.equal(element.comments[0].unresolved, true);
+    draft = addDraftServiceStub.lastCall.args[0];
+    assert.equal(draft.unresolved, true);
 
     unresolved = false; // comment should get added as actually resolved.
     element.comments = [];
     element.addDraft(undefined, undefined, unresolved);
-    assert.equal(element.comments[0].unresolved, false);
+    draft = addDraftServiceStub.lastCall.args[0];
+    assert.equal(draft.unresolved, false);
+
     element.comments = [];
     element.addDraft();
-    assert.equal(element.comments[0].unresolved, true);
+    draft = addDraftServiceStub.lastCall.args[0];
+    assert.equal(draft.unresolved, true);
   });
 
   test('_newDraft with root', () => {
@@ -734,13 +733,16 @@
     element.comments = [];
     element.path = 'abcd';
     element.addOrEditDraft(1);
+    const draft = addDraftServiceStub.firstCall.args[0];
+    element.comments = [draft];
+    flush();
     assert.equal(element.comments.length, 1);
     // Mock a submitted comment.
     element.comments[0].id = (element.comments[0] as UIDraft)
       .__draftID as UrlEncodedCommentId;
     delete (element.comments[0] as UIDraft).__draft;
     element.addOrEditDraft(1);
-    assert.equal(element.comments.length, 2);
+    assert.equal(addDraftServiceStub.callCount, 2);
   });
 
   test('unresolved label', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 2c2d454..fa04860 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -20,7 +20,6 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
 import '../gr-button/gr-button';
 import '../gr-dialog/gr-dialog';
-import '../gr-date-formatter/gr-date-formatter';
 import '../gr-formatted-text/gr-formatted-text';
 import '../gr-icons/gr-icons';
 import '../gr-overlay/gr-overlay';
@@ -100,8 +99,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-comment')
-export class GrComment extends KeyboardShortcutMixin(PolymerElement) {
+export class GrComment extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -125,6 +127,12 @@
    */
 
   /**
+   * Fired when this comment is edited.
+   *
+   * @event comment-edit
+   */
+
+  /**
    * Fired when this comment is saved.
    *
    * @event comment-save
@@ -548,6 +556,17 @@
     return {comment: this.comment, patchNum: this.patchNum};
   }
 
+  _fireEdit() {
+    if (this.comment) this.commentsService.editDraft(this.comment);
+    this.dispatchEvent(
+      new CustomEvent('comment-edit', {
+        detail: this._getEventPayload(),
+        composed: true,
+        bubbles: true,
+      })
+    );
+  }
+
   _fireSave() {
     if (this.comment) this.commentsService.addDraft(this.comment);
     this.dispatchEvent(
@@ -655,11 +674,21 @@
 
   @observe('comment.message')
   _commentMessageChanged(message: string) {
-    this._messageText = message || '';
+    /*
+     * Only overwrite the message text user has typed if there is no existing
+     * text typed by the user. This prevents the bug where creating another
+     * comment triggered a recomputation of comments and the text written by
+     * the user was lost.
+     */
+    if (!this._messageText) this._messageText = message || '';
   }
 
   _messageTextChanged(_: string, oldValue: string) {
-    if (!this.comment || (this.comment && this.comment.id)) {
+    // Only store comments that are being edited in local storage.
+    if (
+      !this.comment ||
+      (this.comment.id && (!isDraft(this.comment) || !this.comment.__editing))
+    ) {
       return;
     }
 
@@ -667,33 +696,32 @@
       ? this.comment.patch_set
       : this._getPatchNum();
     const {path, line, range} = this.comment;
-    if (path) {
-      this.storeTask = debounce(
-        this.storeTask,
-        () => {
-          const message = this._messageText;
-          if (this.changeNum === undefined) {
-            throw new Error('undefined changeNum');
-          }
-          const commentLocation: StorageLocation = {
-            changeNum: this.changeNum,
-            patchNum,
-            path,
-            line,
-            range,
-          };
+    if (!path) return;
+    this.storeTask = debounce(
+      this.storeTask,
+      () => {
+        const message = this._messageText;
+        if (this.changeNum === undefined) {
+          throw new Error('undefined changeNum');
+        }
+        const commentLocation: StorageLocation = {
+          changeNum: this.changeNum,
+          patchNum,
+          path,
+          line,
+          range,
+        };
 
-          if ((!message || !message.length) && oldValue) {
-            // If the draft has been modified to be empty, then erase the storage
-            // entry.
-            this.storage.eraseDraftComment(commentLocation);
-          } else {
-            this.storage.setDraftComment(commentLocation, message);
-          }
-        },
-        STORAGE_DEBOUNCE_INTERVAL
-      );
-    }
+        if ((!message || !message.length) && oldValue) {
+          // If the draft has been modified to be empty, then erase the storage
+          // entry.
+          this.storage.eraseDraftComment(commentLocation);
+        } else {
+          this.storage.setDraftComment(commentLocation, message);
+        }
+      },
+      STORAGE_DEBOUNCE_INTERVAL
+    );
   }
 
   _handleAnchorClick(e: Event) {
@@ -715,6 +743,7 @@
     e.preventDefault();
     if (this.comment?.message) this._messageText = this.comment.message;
     this.editing = true;
+    this._fireEdit();
     this.reporting.recordDraftInteraction();
   }
 
@@ -722,9 +751,7 @@
     e.preventDefault();
 
     // Ignore saves started while already saving.
-    if (this.disabled) {
-      return;
-    }
+    if (this.disabled) return;
     const timingLabel = this.comment?.id
       ? REPORT_UPDATE_DRAFT
       : REPORT_CREATE_DRAFT;
@@ -737,17 +764,16 @@
 
   _handleCancel(e: Event) {
     e.preventDefault();
-
-    if (
-      !this.comment?.message ||
-      this.comment.message.trim().length === 0 ||
-      !this.comment.id
-    ) {
+    if (!this.comment) return;
+    if (!this.comment.id) {
+      // Ensures we update the discarded draft message before deleting the draft
+      this.set('comment.message', this._messageText);
       this._fireDiscard();
-      return;
+    } else {
+      this.set('comment.__editing', false);
+      this.commentsService.cancelDraft(this.comment);
+      this.editing = false;
     }
-    this._messageText = this.comment.message;
-    this.editing = false;
   }
 
   _fireDiscard() {
@@ -947,9 +973,15 @@
       return;
     }
 
-    // Only apply local drafts to comments that haven't been saved
-    // remotely, and haven't been given a default message already.
-    if (!comment || comment.id || comment.message || !comment.path) {
+    // Only apply local drafts to comments that are drafts and are currently
+    // being edited.
+    if (
+      !comment ||
+      !comment.path ||
+      comment.message ||
+      !isDraft(comment) ||
+      !comment.__editing
+    ) {
       return;
     }
 
@@ -962,7 +994,7 @@
     });
 
     if (draft) {
-      this.set('comment.message', draft.message);
+      this._messageText = draft.message || '';
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index b00bf8b..d1496fb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -275,10 +275,10 @@
         </template>
         <gr-tooltip-content
           class="draftTooltip"
-          has-tooltip=""
+          has-tooltip
           title="[[_computeDraftTooltip(_unableToSave)]]"
           max-width="20em"
-          show-icon=""
+          show-icon
         >
           <span class="draftLabel">[[_computeDraftText(_unableToSave)]]</span>
         </gr-tooltip-content>
@@ -313,7 +313,7 @@
       <template is="dom-if" if="[[comment.updated]]">
         <span class="date" tabindex="0" on-click="_handleAnchorClick">
           <gr-date-formatter
-            has-tooltip=""
+            withTooltip
             date-str="[[comment.updated]]"
           ></gr-date-formatter>
         </span>
@@ -357,7 +357,7 @@
           <div class="respectfulReviewTip">
             <div>
               <gr-tooltip-content
-                has-tooltip=""
+                has-tooltip
                 title="Tips for respectful code reviews."
               >
                 <iron-icon
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index cb09125..b963d4b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -28,6 +28,7 @@
   query,
   isVisible,
   stubReporting,
+  mockPromise,
 } from '../../../test/test-utils';
 import {
   AccountId,
@@ -51,7 +52,7 @@
   createFixSuggestionInfo,
 } from '../../../test/test-data-generators';
 import {Timer} from '../../../services/gr-reporting/gr-reporting';
-import {SinonFakeTimers, SinonStubbedMember} from 'sinon/pkg/sinon-esm';
+import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
 import {CreateFixCommentEvent} from '../../../types/events';
 import {DraftInfo, UIRobot} from '../../../utils/comment-util';
 import {MockTimer} from '../../../services/gr-reporting/gr-reporting_mock';
@@ -155,7 +156,7 @@
       });
     });
 
-    test('message is not retrieved from storage when other edits', done => {
+    test('message is not retrieved from storage when missing path', async () => {
       const storageStub = stubStorage('getDraftComment');
       const loadSpy = sinon.spy(element, '_loadLocalDraft');
 
@@ -168,14 +169,34 @@
         },
         line: 5,
       };
-      flush(() => {
-        assert.isTrue(loadSpy.called);
-        assert.isFalse(storageStub.called);
-        done();
-      });
+      await flush();
+      assert.isTrue(loadSpy.called);
+      assert.isFalse(storageStub.called);
     });
 
-    test('message is retrieved from storage when no other edits', done => {
+    test('message is not retrieved from storage when message present', async () => {
+      const storageStub = stubStorage('getDraftComment');
+      const loadSpy = sinon.spy(element, '_loadLocalDraft');
+
+      element.changeNum = 1 as NumericChangeId;
+      element.patchNum = 1 as PatchSetNum;
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com' as EmailAddress,
+        },
+        message: 'This is a message',
+        line: 5,
+        path: 'test',
+        __editing: true,
+        __draft: true,
+      };
+      await flush();
+      assert.isTrue(loadSpy.called);
+      assert.isFalse(storageStub.called);
+    });
+
+    test('message is retrieved from storage for drafts in edit', async () => {
       const storageStub = stubStorage('getDraftComment');
       const loadSpy = sinon.spy(element, '_loadLocalDraft');
 
@@ -188,12 +209,35 @@
         },
         line: 5,
         path: 'test',
+        __editing: true,
+        __draft: true,
       };
-      flush(() => {
-        assert.isTrue(loadSpy.called);
-        assert.isTrue(storageStub.called);
-        done();
-      });
+      await flush();
+      assert.isTrue(loadSpy.called);
+      assert.isTrue(storageStub.called);
+    });
+
+    test('comment message sets messageText only when empty', () => {
+      element.changeNum = 1 as NumericChangeId;
+      element.patchNum = 1 as PatchSetNum;
+      element._messageText = '';
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com' as EmailAddress,
+        },
+        line: 5,
+        path: 'test',
+        __editing: true,
+        __draft: true,
+        message: 'hello world',
+      };
+      // messageText was empty so overwrite the message now
+      assert.equal(element._messageText, 'hello world');
+
+      element.comment!.message = 'new message';
+      // messageText was already set so do not overwrite it
+      assert.equal(element._messageText, 'hello world');
     });
 
     test('_getPatchNum', () => {
@@ -315,7 +359,7 @@
       );
     });
 
-    test('delete comment', done => {
+    test('delete comment', async () => {
       const stub = stubRestApi('deleteComment').returns(
         Promise.resolve({
           id: '1' as UrlEncodedCommentId,
@@ -333,24 +377,21 @@
         )
       );
       tap(queryAndAssert(element, '.action.delete'));
-      flush(() => {
-        openSpy.lastCall.returnValue.then(() => {
-          const dialog = element.confirmDeleteOverlay?.querySelector(
-            '#confirmDeleteComment'
-          ) as GrConfirmDeleteCommentDialog;
-          dialog.message = 'removal reason';
-          element._handleConfirmDeleteComment();
-          assert.isTrue(
-            stub.calledWith(
-              42 as NumericChangeId,
-              1 as PatchSetNum,
-              'baf0414d_60047215' as UrlEncodedCommentId,
-              'removal reason'
-            )
-          );
-          done();
-        });
-      });
+      await flush();
+      await openSpy.lastCall.returnValue;
+      const dialog = element.confirmDeleteOverlay?.querySelector(
+        '#confirmDeleteComment'
+      ) as GrConfirmDeleteCommentDialog;
+      dialog.message = 'removal reason';
+      element._handleConfirmDeleteComment();
+      assert.isTrue(
+        stub.calledWith(
+          42 as NumericChangeId,
+          1 as PatchSetNum,
+          'baf0414d_60047215' as UrlEncodedCommentId,
+          'removal reason'
+        )
+      );
     });
 
     suite('draft update reporting', () => {
@@ -417,6 +458,7 @@
 
     test('edit reports interaction', () => {
       const reportStub = stubReporting('recordDraftInteraction');
+      sinon.stub(element, '_fireEdit');
       element.draft = true;
       flush();
       tap(queryAndAssert(element, '.edit'));
@@ -437,7 +479,7 @@
       assert.isTrue(reportStub.calledOnce);
     });
 
-    test('failed save draft request', done => {
+    test('failed save draft request', async () => {
       element.draft = true;
       element.changeNum = 1 as NumericChangeId;
       element.patchNum = 1 as PatchSetNum;
@@ -449,46 +491,42 @@
         ...createComment(),
         id: 'abc_123' as UrlEncodedCommentId,
       });
-      flush(() => {
-        let args = updateRequestStub.lastCall.args;
-        assert.deepEqual(args, [0, true]);
-        assert.equal(
-          element._getSavingMessage(...args),
-          __testOnly_UNSAVED_MESSAGE
-        );
-        assert.equal(
-          (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
-          'DRAFT(Failed to save)'
-        );
-        assert.isTrue(
-          isVisible(queryAndAssert(element, '.save')),
-          'save is visible'
-        );
-        diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
-        element._saveDraft({
-          ...createComment(),
-          id: 'abc_123' as UrlEncodedCommentId,
-        });
-        flush(() => {
-          args = updateRequestStub.lastCall.args;
-          assert.deepEqual(args, [0]);
-          assert.equal(element._getSavingMessage(...args), 'All changes saved');
-          assert.equal(
-            (queryAndAssert(element, '.draftLabel') as HTMLSpanElement)
-              .innerText,
-            'DRAFT'
-          );
-          assert.isFalse(
-            isVisible(queryAndAssert(element, '.save')),
-            'save is not visible'
-          );
-          assert.isFalse(element._unableToSave);
-          done();
-        });
+      await flush();
+      let args = updateRequestStub.lastCall.args;
+      assert.deepEqual(args, [0, true]);
+      assert.equal(
+        element._getSavingMessage(...args),
+        __testOnly_UNSAVED_MESSAGE
+      );
+      assert.equal(
+        (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
+        'DRAFT(Failed to save)'
+      );
+      assert.isTrue(
+        isVisible(queryAndAssert(element, '.save')),
+        'save is visible'
+      );
+      diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
+      element._saveDraft({
+        ...createComment(),
+        id: 'abc_123' as UrlEncodedCommentId,
       });
+      await flush();
+      args = updateRequestStub.lastCall.args;
+      assert.deepEqual(args, [0]);
+      assert.equal(element._getSavingMessage(...args), 'All changes saved');
+      assert.equal(
+        (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
+        'DRAFT'
+      );
+      assert.isFalse(
+        isVisible(queryAndAssert(element, '.save')),
+        'save is not visible'
+      );
+      assert.isFalse(element._unableToSave);
     });
 
-    test('failed save draft request with promise failure', done => {
+    test('failed save draft request with promise failure', async () => {
       element.draft = true;
       element.changeNum = 1 as NumericChangeId;
       element.patchNum = 1 as PatchSetNum;
@@ -500,43 +538,39 @@
         ...createComment(),
         id: 'abc_123' as UrlEncodedCommentId,
       });
-      flush(() => {
-        let args = updateRequestStub.lastCall.args;
-        assert.deepEqual(args, [0, true]);
-        assert.equal(
-          element._getSavingMessage(...args),
-          __testOnly_UNSAVED_MESSAGE
-        );
-        assert.equal(
-          (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
-          'DRAFT(Failed to save)'
-        );
-        assert.isTrue(
-          isVisible(queryAndAssert(element, '.save')),
-          'save is visible'
-        );
-        diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
-        element._saveDraft({
-          ...createComment(),
-          id: 'abc_123' as UrlEncodedCommentId,
-        });
-        flush(() => {
-          args = updateRequestStub.lastCall.args;
-          assert.deepEqual(args, [0]);
-          assert.equal(element._getSavingMessage(...args), 'All changes saved');
-          assert.equal(
-            (queryAndAssert(element, '.draftLabel') as HTMLSpanElement)
-              .innerText,
-            'DRAFT'
-          );
-          assert.isFalse(
-            isVisible(queryAndAssert(element, '.save')),
-            'save is not visible'
-          );
-          assert.isFalse(element._unableToSave);
-          done();
-        });
+      await flush();
+      let args = updateRequestStub.lastCall.args;
+      assert.deepEqual(args, [0, true]);
+      assert.equal(
+        element._getSavingMessage(...args),
+        __testOnly_UNSAVED_MESSAGE
+      );
+      assert.equal(
+        (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
+        'DRAFT(Failed to save)'
+      );
+      assert.isTrue(
+        isVisible(queryAndAssert(element, '.save')),
+        'save is visible'
+      );
+      diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
+      element._saveDraft({
+        ...createComment(),
+        id: 'abc_123' as UrlEncodedCommentId,
       });
+      await flush();
+      args = updateRequestStub.lastCall.args;
+      assert.deepEqual(args, [0]);
+      assert.equal(element._getSavingMessage(...args), 'All changes saved');
+      assert.equal(
+        (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
+        'DRAFT'
+      );
+      assert.isFalse(
+        isVisible(queryAndAssert(element, '.save')),
+        'save is not visible'
+      );
+      assert.isFalse(element._unableToSave);
     });
   });
 
@@ -579,10 +613,11 @@
         __draftID: 'temp_draft_id',
         path: '/path/to/file',
         line: 5,
+        id: undefined,
       };
     });
 
-    test('button visibility states', () => {
+    test('button visibility states', async () => {
       element.showActions = false;
       assert.isTrue(
         queryAndAssert(element, '.humanActions').hasAttribute('hidden')
@@ -600,7 +635,7 @@
       );
 
       element.draft = true;
-      flush();
+      await flush();
       assert.isTrue(
         isVisible(queryAndAssert(element, '.edit')),
         'edit is visible'
@@ -629,7 +664,7 @@
       );
 
       element.editing = true;
-      flush();
+      await flush();
       assert.isFalse(
         isVisible(queryAndAssert(element, '.edit')),
         'edit is not visible'
@@ -659,7 +694,7 @@
 
       element.draft = false;
       element.editing = false;
-      flush();
+      await flush();
       assert.isFalse(
         isVisible(queryAndAssert(element, '.edit')),
         'edit is not visible'
@@ -686,7 +721,7 @@
       element.comment!.id = 'foo' as UrlEncodedCommentId;
       element.draft = true;
       element.editing = true;
-      flush();
+      await flush();
       assert.isTrue(
         isVisible(queryAndAssert(element, '.cancel')),
         'cancel is visible'
@@ -726,14 +761,14 @@
       element.set(['comment', 'robot_run_id'], 'text');
       element.editing = false;
       element.collapsed = false;
-      flush();
+      await flush();
       assert.isTrue(
         queryAndAssert(element, '.robotRun.link').textContent === 'Run Details'
       );
 
       // A robot comment with run ID and url should display a link.
       element.set(['comment', 'url'], '/path/to/run');
-      flush();
+      await flush();
       assert.notEqual(
         getComputedStyle(queryAndAssert(element, '.robotRun.link')).display,
         'none'
@@ -745,7 +780,8 @@
       );
     });
 
-    test('collapsible drafts', () => {
+    test('collapsible drafts', async () => {
+      const fireEditStub = sinon.stub(element, '_fireEdit');
       assert.isTrue(element.collapsed);
       assert.isFalse(
         isVisible(queryAndAssert(element, 'gr-formatted-text')),
@@ -780,9 +816,10 @@
       // When the edit button is pressed, should still see the actions
       // and also textarea
       element.draft = true;
-      flush();
+      await flush();
       tap(queryAndAssert(element, '.edit'));
-      flush();
+      await flush();
+      assert.isTrue(fireEditStub.called);
       assert.isFalse(element.collapsed);
       assert.isFalse(
         isVisible(queryAndAssert(element, 'gr-formatted-text')),
@@ -837,7 +874,7 @@
       );
     });
 
-    test('robot comment layout', done => {
+    test('robot comment layout', async () => {
       const comment = {
         robot_id: 'happy_robot_id' as RobotId,
         url: '/robot/comment',
@@ -849,36 +886,32 @@
       };
       element.comment = comment;
       element.collapsed = false;
-      flush(() => {
-        let runIdMessage;
-        runIdMessage = queryAndAssert(element, '.runIdMessage') as HTMLElement;
-        assert.isFalse((runIdMessage as HTMLElement).hidden);
+      await flush;
+      let runIdMessage;
+      runIdMessage = queryAndAssert(element, '.runIdMessage') as HTMLElement;
+      assert.isFalse((runIdMessage as HTMLElement).hidden);
 
-        const runDetailsLink = queryAndAssert(
-          element,
-          '.robotRunLink'
-        ) as HTMLAnchorElement;
-        assert.isTrue(
-          runDetailsLink.href.indexOf((element.comment as UIRobot).url!) !== -1
-        );
+      const runDetailsLink = queryAndAssert(
+        element,
+        '.robotRunLink'
+      ) as HTMLAnchorElement;
+      assert.isTrue(
+        runDetailsLink.href.indexOf((element.comment as UIRobot).url!) !== -1
+      );
 
-        const robotServiceName = queryAndAssert(element, '.robotName');
-        assert.equal(robotServiceName.textContent?.trim(), 'happy_robot_id');
+      const robotServiceName = queryAndAssert(element, '.robotName');
+      assert.equal(robotServiceName.textContent?.trim(), 'happy_robot_id');
 
-        const authorName = queryAndAssert(element, '.robotId');
-        assert.isTrue(
-          (authorName as HTMLDivElement).innerText === 'Happy Robot'
-        );
+      const authorName = queryAndAssert(element, '.robotId');
+      assert.isTrue((authorName as HTMLDivElement).innerText === 'Happy Robot');
 
-        element.collapsed = true;
-        flush();
-        runIdMessage = queryAndAssert(element, '.runIdMessage');
-        assert.isTrue((runIdMessage as HTMLDivElement).hidden);
-        done();
-      });
+      element.collapsed = true;
+      await flush();
+      runIdMessage = queryAndAssert(element, '.runIdMessage');
+      assert.isTrue((runIdMessage as HTMLDivElement).hidden);
     });
 
-    test('author name fallback to email', done => {
+    test('author name fallback to email', async () => {
       const comment = {
         url: '/robot/comment',
         author: {
@@ -888,17 +921,16 @@
       };
       element.comment = comment;
       element.collapsed = false;
-      flush(() => {
-        const authorName = queryAndAssert(
-          queryAndAssert(element, 'gr-account-label'),
-          'span.name'
-        ) as HTMLSpanElement;
-        assert.equal(authorName.innerText.trim(), 'test@test.com');
-        done();
-      });
+      await flush();
+      const authorName = queryAndAssert(
+        queryAndAssert(element, 'gr-account-label'),
+        'span.name'
+      ) as HTMLSpanElement;
+      assert.equal(authorName.innerText.trim(), 'test@test.com');
     });
 
-    test('patchset level comment', done => {
+    test('patchset level comment', async () => {
+      const fireEditStub = sinon.stub(element, '_fireEdit');
       const comment = {
         ...element.comment,
         path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
@@ -906,25 +938,26 @@
         range: undefined,
       };
       element.comment = comment;
-      flush();
+      await flush();
       tap(queryAndAssert(element, '.edit'));
+      assert.isTrue(fireEditStub.called);
       assert.isTrue(element.editing);
 
       element._messageText = 'hello world';
       const eraseMessageDraftSpy = spyStorage('eraseDraftComment');
       const mockEvent = {...new Event('click'), preventDefault: sinon.stub()};
       element._handleSave(mockEvent);
-      flush(() => {
-        assert.isTrue(eraseMessageDraftSpy.called);
-        done();
-      });
+      await flush();
+      assert.isTrue(eraseMessageDraftSpy.called);
     });
 
-    test('draft creation/cancellation', done => {
+    test('draft creation/cancellation', async () => {
+      const fireEditStub = sinon.stub(element, '_fireEdit');
       assert.isFalse(element.editing);
       element.draft = true;
-      flush();
+      await flush();
       tap(queryAndAssert(element, '.edit'));
+      assert.isTrue(fireEditStub.called);
       assert.isTrue(element.editing);
 
       element.comment!.message = '';
@@ -945,37 +978,41 @@
       element.addEventListener('comment-update', updateStub);
 
       let numDiscardEvents = 0;
+      const promise = mockPromise();
       element.addEventListener('comment-discard', () => {
         numDiscardEvents++;
         assert.isFalse(eraseMessageDraftSpy.called);
         if (numDiscardEvents === 2) {
           assert.isFalse(updateStub.called);
-          done();
+          promise.resolve();
         }
       });
       tap(queryAndAssert(element, '.cancel'));
-      flush();
+      await flush();
       element._messageText = '';
       element.editing = true;
-      flush();
+      await flush();
       pressAndReleaseKeyOn(element.textarea!, 27); // esc
+      await promise;
     });
 
-    test('draft discard removes message from storage', done => {
+    test('draft discard removes message from storage', async () => {
       element._messageText = '';
       const eraseMessageDraftSpy = sinon.spy(
         element,
         '_eraseDraftCommentFromStorage'
       );
 
+      const promise = mockPromise();
       element.addEventListener('comment-discard', () => {
         assert.isTrue(eraseMessageDraftSpy.called);
-        done();
+        promise.resolve();
       });
       element._handleDiscard({
         ...new Event('click'),
         preventDefault: sinon.stub(),
       });
+      await promise;
     });
 
     test('storage is cleared only after save success', () => {
@@ -1022,22 +1059,24 @@
       assert.equal(element._computeSaveDisabled('', comment, false), true);
     });
 
-    test('ctrl+s saves comment', done => {
+    test('ctrl+s saves comment', async () => {
+      const promise = mockPromise();
       const stub = sinon.stub(element, 'save').callsFake(() => {
         assert.isTrue(stub.called);
         stub.restore();
-        done();
+        promise.resolve();
         return Promise.resolve();
       });
       element._messageText = 'is that the horse from horsing around??';
       element.editing = true;
-      flush();
+      await flush();
       pressAndReleaseKeyOn(element.textarea!.$.textarea.textarea, 83, 'ctrl'); // 'ctrl + s'
+      await promise;
     });
 
-    test('draft saving/editing', done => {
+    test('draft saving/editing', async () => {
       const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
-
+      const fireEditStub = sinon.stub(element, '_fireEdit');
       const clock: SinonFakeTimers = sinon.useFakeTimers();
       const tickAndFlush = async (repetitions: number) => {
         for (let i = 1; i <= repetitions; i++) {
@@ -1047,8 +1086,9 @@
       };
 
       element.draft = true;
-      flush();
+      await flush();
       tap(queryAndAssert(element, '.edit'));
+      assert.isTrue(fireEditStub.called);
       tickAndFlush(1);
       element._messageText = 'good news, everyone!';
       tickAndFlush(1);
@@ -1056,7 +1096,7 @@
       assert.isTrue(dispatchEventStub.calledTwice);
 
       element._messageText = 'good news, everyone!';
-      flush();
+      await flush();
       assert.isTrue(dispatchEventStub.calledTwice);
 
       tap(queryAndAssert(element, '.save'));
@@ -1066,68 +1106,62 @@
         'Element should be disabled when creating draft.'
       );
 
-      element
-        ._xhrPromise!.then(draft => {
-          const evt = dispatchEventStub.lastCall.args[0] as CustomEvent<{
-            comment: DraftInfo;
-          }>;
-          assert.equal(evt.type, 'comment-save');
+      let draft = await element._xhrPromise!;
+      const evt = dispatchEventStub.lastCall.args[0] as CustomEvent<{
+        comment: DraftInfo;
+      }>;
+      assert.equal(evt.type, 'comment-save');
 
-          const expectedDetail = {
-            comment: {
-              ...createComment(),
-              __draft: true,
-              __draftID: 'temp_draft_id',
-              id: 'baf0414d_40572e03' as UrlEncodedCommentId,
-              line: 5,
-              message: 'saved!',
-              path: '/path/to/file',
-              updated: '2015-12-08 21:52:36.177000000' as Timestamp,
-            },
-            patchNum: 1 as PatchSetNum,
-          };
+      const expectedDetail = {
+        comment: {
+          ...createComment(),
+          __draft: true,
+          __draftID: 'temp_draft_id',
+          id: 'baf0414d_40572e03' as UrlEncodedCommentId,
+          line: 5,
+          message: 'saved!',
+          path: '/path/to/file',
+          updated: '2015-12-08 21:52:36.177000000' as Timestamp,
+        },
+        patchNum: 1 as PatchSetNum,
+      };
 
-          assert.deepEqual(evt.detail, expectedDetail);
-          assert.isFalse(
-            element.disabled,
-            'Element should be enabled when done creating draft.'
-          );
-          assert.equal(draft.message, 'saved!');
-          assert.isFalse(element.editing);
-        })
-        .then(() => {
-          tap(queryAndAssert(element, '.edit'));
-          element._messageText =
-            'You’ll be delivering a package to Chapek 9, ' +
-            'a world where humans are killed on sight.';
-          tap(queryAndAssert(element, '.save'));
-          assert.isTrue(
-            element.disabled,
-            'Element should be disabled when updating draft.'
-          );
-
-          element._xhrPromise!.then(draft => {
-            assert.isFalse(
-              element.disabled,
-              'Element should be enabled when done updating draft.'
-            );
-            assert.equal(draft.message, 'saved!');
-            assert.isFalse(element.editing);
-            dispatchEventStub.restore();
-            done();
-          });
-        });
+      assert.deepEqual(evt.detail, expectedDetail);
+      assert.isFalse(
+        element.disabled,
+        'Element should be enabled when done creating draft.'
+      );
+      assert.equal(draft.message, 'saved!');
+      assert.isFalse(element.editing);
+      tap(queryAndAssert(element, '.edit'));
+      assert.isTrue(fireEditStub.calledTwice);
+      element._messageText =
+        'You’ll be delivering a package to Chapek 9, ' +
+        'a world where humans are killed on sight.';
+      tap(queryAndAssert(element, '.save'));
+      assert.isTrue(
+        element.disabled,
+        'Element should be disabled when updating draft.'
+      );
+      draft = await element._xhrPromise!;
+      assert.isFalse(
+        element.disabled,
+        'Element should be enabled when done updating draft.'
+      );
+      assert.equal(draft.message, 'saved!');
+      assert.isFalse(element.editing);
+      dispatchEventStub.restore();
     });
 
-    test('draft prevent save when disabled', () => {
+    test('draft prevent save when disabled', async () => {
       const saveStub = sinon.stub(element, 'save').returns(Promise.resolve());
       element.showActions = true;
       element.draft = true;
-      flush();
+      await flush();
       tap(element.$.header);
       tap(queryAndAssert(element, '.edit'));
       element._messageText = 'good news, everyone!';
-      flush();
+      await flush();
 
       element.disabled = true;
       tap(queryAndAssert(element, '.save'));
@@ -1138,14 +1172,16 @@
       assert.isTrue(saveStub.calledOnce);
     });
 
-    test('proper event fires on resolve, comment is not saved', done => {
+    test('proper event fires on resolve, comment is not saved', async () => {
       const save = sinon.stub(element, 'save');
+      const promise = mockPromise();
       element.addEventListener('comment-update', e => {
         assert.isTrue(e.detail.comment.unresolved);
         assert.isFalse(save.called);
-        done();
+        promise.resolve();
       });
       tap(queryAndAssert(element, '.resolve input'));
+      await promise;
     });
 
     test('resolved comment state indicated by checkbox', () => {
@@ -1209,7 +1245,7 @@
       });
     });
 
-    test('cancelling an unsaved draft discards, persists in storage', () => {
+    test('cancelling an unsaved draft discards, persists in storage', async () => {
       const clock: SinonFakeTimers = sinon.useFakeTimers();
       const tickAndFlush = async (repetitions: number) => {
         for (let i = 1; i <= repetitions; i++) {
@@ -1230,7 +1266,7 @@
         ...new Event('click'),
         preventDefault: sinon.stub(),
       });
-      flush();
+      await flush();
       assert.isTrue(discardSpy.called);
       assert.isFalse(eraseStub.called);
     });
@@ -1261,19 +1297,21 @@
       assert.isTrue(discardStub.called);
     });
 
-    test('_handleFix fires create-fix event', done => {
+    test('_handleFix fires create-fix event', async () => {
+      const promise = mockPromise();
       element.addEventListener(
         'create-fix-comment',
         (e: CreateFixCommentEvent) => {
           assert.deepEqual(e.detail, element._getEventPayload());
-          done();
+          promise.resolve();
         }
       );
       element.isRobotComment = true;
       element.comments = [element.comment!];
-      flush();
+      await flush();
 
       tap(queryAndAssert(element, '.fix'));
+      await promise;
     });
 
     test('do not show Please Fix button if human reply exists', () => {
@@ -1419,19 +1457,21 @@
       queryAndAssert(element, '.robotActions gr-button');
     });
 
-    test('_handleShowFix fires open-fix-preview event', done => {
+    test('_handleShowFix fires open-fix-preview event', async () => {
+      const promise = mockPromise();
       element.addEventListener('open-fix-preview', e => {
         assert.deepEqual(e.detail, element._getEventPayload());
-        done();
+        promise.resolve();
       });
       element.comment = {
         ...createComment(),
         fix_suggestions: [{...createFixSuggestionInfo()}],
       };
       element.isRobotComment = true;
-      flush();
+      await flush();
 
       tap(queryAndAssert(element, '.show-fix'));
+      await promise;
     });
   });
 
@@ -1449,7 +1489,7 @@
       sinon.restore();
     });
 
-    test('show tip when no cached record', done => {
+    test('show tip when no cached record', async () => {
       element = draftFixture.instantiate() as GrComment;
       const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
       const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
@@ -1457,15 +1497,13 @@
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true, __draft: true};
-      flush(() => {
-        assert.isTrue(respectfulGetStub.called);
-        assert.isTrue(respectfulSetStub.called);
-        assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
-        done();
-      });
+      await flush();
+      assert.isTrue(respectfulGetStub.called);
+      assert.isTrue(respectfulSetStub.called);
+      assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
     });
 
-    test('add 14-day delays once dismissed', done => {
+    test('add 14-day delays once dismissed', async () => {
       element = draftFixture.instantiate() as GrComment;
       const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
       const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
@@ -1473,20 +1511,18 @@
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true, __draft: true};
-      flush(() => {
-        assert.isTrue(respectfulGetStub.called);
-        assert.isTrue(respectfulSetStub.called);
-        assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
-        assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
+      await flush();
+      assert.isTrue(respectfulGetStub.called);
+      assert.isTrue(respectfulSetStub.called);
+      assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
+      assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
 
-        tap(queryAndAssert(element, '.respectfulReviewTip .close'));
-        flush();
-        assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
-        done();
-      });
+      tap(queryAndAssert(element, '.respectfulReviewTip .close'));
+      flush();
+      assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
     });
 
-    test('do not show tip when fall out of probability', done => {
+    test('do not show tip when fall out of probability', async () => {
       element = draftFixture.instantiate() as GrComment;
       const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
       const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
@@ -1494,15 +1530,13 @@
       // fake random
       element.getRandomNum = () => 3;
       element.comment = {__editing: true, __draft: true};
-      flush(() => {
-        assert.isTrue(respectfulGetStub.called);
-        assert.isFalse(respectfulSetStub.called);
-        assert.isNotOk(query(element, '.respectfulReviewTip'));
-        done();
-      });
+      await flush();
+      assert.isTrue(respectfulGetStub.called);
+      assert.isFalse(respectfulSetStub.called);
+      assert.isNotOk(query(element, '.respectfulReviewTip'));
     });
 
-    test('show tip when editing changed to true', done => {
+    test('show tip when editing changed to true', async () => {
       element = draftFixture.instantiate() as GrComment;
       const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
       const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
@@ -1510,22 +1544,19 @@
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: false};
-      flush(() => {
-        assert.isFalse(respectfulGetStub.called);
-        assert.isFalse(respectfulSetStub.called);
-        assert.isNotOk(query(element, '.respectfulReviewTip'));
+      await flush();
+      assert.isFalse(respectfulGetStub.called);
+      assert.isFalse(respectfulSetStub.called);
+      assert.isNotOk(query(element, '.respectfulReviewTip'));
 
-        element.editing = true;
-        flush(() => {
-          assert.isTrue(respectfulGetStub.called);
-          assert.isTrue(respectfulSetStub.called);
-          assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
-          done();
-        });
-      });
+      element.editing = true;
+      await flush();
+      assert.isTrue(respectfulGetStub.called);
+      assert.isTrue(respectfulSetStub.called);
+      assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
     });
 
-    test('no tip when cached record', done => {
+    test('no tip when cached record', async () => {
       element = draftFixture.instantiate() as GrComment;
       const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
       const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
@@ -1533,12 +1564,10 @@
       // fake random
       element.getRandomNum = () => 0;
       element.comment = {__editing: true, __draft: true};
-      flush(() => {
-        assert.isTrue(respectfulGetStub.called);
-        assert.isFalse(respectfulSetStub.called);
-        assert.isNotOk(query(element, '.respectfulReviewTip'));
-        done();
-      });
+      await flush();
+      assert.isTrue(respectfulGetStub.called);
+      assert.isFalse(respectfulSetStub.called);
+      assert.isNotOk(query(element, '.respectfulReviewTip'));
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index b85703a..0cd522a 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -19,10 +19,10 @@
 import '../gr-icons/gr-icons';
 import {IronIconElement} from '@polymer/iron-icon';
 import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
-import {classMap} from 'lit-html/directives/class-map';
-import {ifDefined} from 'lit-html/directives/if-defined';
-import {css, customElement, html, property} from 'lit-element';
-import {GrLitElement} from '../../lit/gr-lit-element';
+import {classMap} from 'lit/directives/class-map';
+import {ifDefined} from 'lit/directives/if-defined';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {GrButton} from '../gr-button/gr-button';
 
 const COPY_TIMEOUT_MS = 1000;
@@ -33,7 +33,7 @@
   }
 }
 @customElement('gr-copy-clipboard')
-export class GrCopyClipboard extends GrLitElement {
+export class GrCopyClipboard extends LitElement {
   @property({type: String})
   text: string | undefined;
 
@@ -89,6 +89,7 @@
   override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         iron-icon {
@@ -109,7 +110,7 @@
           type="text"
           @click="${this._handleInputClick}"
           readonly=""
-          bind-value=${this.text}
+          bind-value=${this.text || ''}
         >
           <input
             id="input"
@@ -118,21 +119,24 @@
             type="text"
             @click="${this._handleInputClick}"
             readonly=""
-            .value=${this.text}
+            .value=${this.text || ''}
             part="text-container-style"
           />
         </iron-input>
-        <gr-button
-          id="copy-clipboard-button"
-          link=""
+        <gr-tooltip-content
           ?has-tooltip=${this.hasTooltip}
-          class="copyToClipboard"
           title="${ifDefined(this.buttonTitle)}"
-          @click="${this._copyToClipboard}"
-          aria-label="Click to copy to clipboard"
         >
-          <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
-        </gr-button>
+          <gr-button
+            id="copy-clipboard-button"
+            link=""
+            class="copyToClipboard"
+            @click="${this._copyToClipboard}"
+            aria-label="Click to copy to clipboard"
+          >
+            <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
+          </gr-button>
+        </gr-tooltip-content>
       </div> `;
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index 39ec9e4..99f9265 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -14,11 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-date-formatter_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {property, customElement} from '@polymer/decorators';
+import '../gr-tooltip-content/gr-tooltip-content';
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {
   parseDate,
   fromNow,
@@ -76,13 +74,9 @@
 }
 
 @customElement('gr-date-formatter')
-export class GrDateFormatter extends TooltipMixin(PolymerElement) {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  @property({type: String, notify: true})
-  dateStr: string | null = null;
+export class GrDateFormatter extends LitElement {
+  @property({type: String})
+  dateStr: string | undefined = undefined;
 
   @property({type: Boolean})
   showDateAndTime = false;
@@ -92,30 +86,20 @@
    * native browser tooltip.
    */
   @property({type: Boolean})
-  override hasTooltip = false;
+  withTooltip = false;
 
   @property({type: Boolean})
   showYesterday = false;
 
-  /**
-   * The title to be used as the native tooltip or by the tooltip behavior.
-   */
-  @property({
-    type: String,
-    reflectToAttribute: true,
-    computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)',
-  })
-  override title = '';
-
   /** @type {?{short: string, full: string}} */
   @property({type: Object})
-  _dateFormat?: DateFormatPair;
+  private dateFormat?: DateFormatPair;
 
   @property({type: String})
-  _timeFormat?: string;
+  private timeFormat?: string;
 
   @property({type: Boolean})
-  _relative = false;
+  private relative = false;
 
   @property({type: Boolean})
   forceRelative = false;
@@ -129,76 +113,110 @@
     super();
   }
 
+  static override get styles() {
+    return [
+      css`
+        host {
+          color: inherit;
+          display: inline;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    if (!this.withTooltip) {
+      return this.renderDateString();
+    }
+
+    const fullDateStr = this.computeFullDateStr();
+    if (!fullDateStr) {
+      return this.renderDateString();
+    }
+    return html`
+      <gr-tooltip-content has-tooltip title=${fullDateStr}>
+        ${this.renderDateString()}
+      </gr-tooltip-content>
+    `;
+  }
+
+  private renderDateString() {
+    return html` <span>${this._computeDateStr()}</span>`;
+  }
+
   override connectedCallback() {
     super.connectedCallback();
     this._loadPreferences();
   }
 
+  // private but used by tests
   _getUtcOffsetString() {
     return utcOffsetString();
   }
 
+  // private but used by tests
   _loadPreferences() {
     return this._getLoggedIn().then(loggedIn => {
       if (!loggedIn) {
-        this._timeFormat = TimeFormats.TIME_24;
-        this._dateFormat = DateFormats.STD;
-        this._relative = this.forceRelative;
+        this.timeFormat = TimeFormats.TIME_24;
+        this.dateFormat = DateFormats.STD;
+        this.relative = this.forceRelative;
         return;
       }
-      return Promise.all([this._loadTimeFormat(), this._loadRelative()]);
+      return Promise.all([this._loadTimeFormat(), this.loadRelative()]);
     });
   }
 
+  // private but used in gr/file-list_test.js
   _loadTimeFormat() {
-    return this._getPreferences().then(preferences => {
+    return this.getPreferences().then(preferences => {
       if (!preferences) {
         throw Error('Preferences is not set');
       }
-      this._decideTimeFormat(preferences.time_format);
-      this._decideDateFormat(preferences.date_format);
+      this.decideTimeFormat(preferences.time_format);
+      this.decideDateFormat(preferences.date_format);
     });
   }
 
-  _decideTimeFormat(timeFormat: TimeFormat) {
+  private decideTimeFormat(timeFormat: TimeFormat) {
     switch (timeFormat) {
       case TimeFormat.HHMM_12:
-        this._timeFormat = TimeFormats.TIME_12;
+        this.timeFormat = TimeFormats.TIME_12;
         break;
       case TimeFormat.HHMM_24:
-        this._timeFormat = TimeFormats.TIME_24;
+        this.timeFormat = TimeFormats.TIME_24;
         break;
       default:
         assertNever(timeFormat, `Invalid time format: ${timeFormat}`);
     }
   }
 
-  _decideDateFormat(dateFormat: DateFormat) {
+  private decideDateFormat(dateFormat: DateFormat) {
     switch (dateFormat) {
       case DateFormat.STD:
-        this._dateFormat = DateFormats.STD;
+        this.dateFormat = DateFormats.STD;
         break;
       case DateFormat.US:
-        this._dateFormat = DateFormats.US;
+        this.dateFormat = DateFormats.US;
         break;
       case DateFormat.ISO:
-        this._dateFormat = DateFormats.ISO;
+        this.dateFormat = DateFormats.ISO;
         break;
       case DateFormat.EURO:
-        this._dateFormat = DateFormats.EURO;
+        this.dateFormat = DateFormats.EURO;
         break;
       case DateFormat.UK:
-        this._dateFormat = DateFormats.UK;
+        this.dateFormat = DateFormats.UK;
         break;
       default:
         assertNever(dateFormat, `Invalid date format: ${dateFormat}`);
     }
   }
 
-  _loadRelative() {
-    return this._getPreferences().then(prefs => {
+  private loadRelative() {
+    return this.getPreferences().then(prefs => {
       // prefs.relative_date_in_change_table is not set when false.
-      this._relative =
+      this.relative =
         this.forceRelative || !!(prefs && prefs.relative_date_in_change_table);
     });
   }
@@ -207,70 +225,60 @@
     return this.restApiService.getLoggedIn();
   }
 
-  _getPreferences() {
+  private getPreferences() {
     return this.restApiService.getPreferences();
   }
 
-  _computeDateStr(
-    dateStr?: Timestamp,
-    timeFormat?: string,
-    dateFormat?: DateFormatPair,
-    relative?: boolean,
-    showDateAndTime?: boolean,
-    showYesterday?: boolean
-  ) {
-    if (!dateStr || !timeFormat || !dateFormat) {
+  // private but used by tests
+  _computeDateStr() {
+    if (!this.dateStr || !this.timeFormat || !this.dateFormat) {
       return '';
     }
-    const date = parseDate(dateStr);
+    const date = parseDate(this.dateStr as Timestamp);
     if (!isValidDate(date)) {
       return '';
     }
-    if (relative) {
+    if (this.relative) {
       return fromNow(date, this.relativeOptionNoAgo);
     }
     const now = new Date();
-    let format = dateFormat.full;
+    let format = this.dateFormat.full;
     if (isWithinDay(now, date)) {
-      format = timeFormat;
-    } else if (showYesterday && wasYesterday(now, date)) {
-      return `Yesterday at ${formatDate(date, timeFormat)}`;
+      format = this.timeFormat;
+    } else if (this.showYesterday && wasYesterday(now, date)) {
+      return `Yesterday at ${formatDate(date, this.timeFormat)}`;
     } else {
       if (isWithinHalfYear(now, date)) {
-        format = dateFormat.short;
+        format = this.dateFormat.short;
       }
-      if (this.showDateAndTime || showDateAndTime) {
-        format = `${format} ${timeFormat}`;
+      if (this.showDateAndTime || this.showDateAndTime) {
+        format = `${format} ${this.timeFormat}`;
       }
     }
     return formatDate(date, format);
   }
 
-  _timeToSecondsFormat(timeFormat: string | undefined) {
-    return timeFormat === TimeFormats.TIME_12
-      ? TimeFormats.TIME_12_WITH_SEC
-      : TimeFormats.TIME_24_WITH_SEC;
-  }
-
-  _computeFullDateStr(
-    dateStr?: Timestamp,
-    timeFormat?: string,
-    dateFormat?: DateFormatPair
-  ) {
+  private computeFullDateStr() {
     // Polymer 2: check for undefined
-    if ([dateStr, timeFormat].includes(undefined) || !dateFormat) {
+    if (
+      [this.dateStr, this.timeFormat].includes(undefined) ||
+      !this.dateFormat
+    ) {
       return undefined;
     }
 
-    if (!dateStr) {
+    if (!this.dateStr) {
       return '';
     }
-    const date = parseDate(dateStr);
+    const date = parseDate(this.dateStr as Timestamp);
     if (!isValidDate(date)) {
       return '';
     }
-    let format = dateFormat.full + ', ';
-    format += this._timeToSecondsFormat(timeFormat);
+    let format = this.dateFormat.full + ', ';
+    format +=
+      this.timeFormat === TimeFormats.TIME_12
+        ? TimeFormats.TIME_12_WITH_SEC
+        : TimeFormats.TIME_24_WITH_SEC;
     return formatDate(date, format) + this._getUtcOffsetString();
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
index 9a96c2d..860a7e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
@@ -22,14 +22,18 @@
 import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`
-<gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+<gr-date-formatter withTooltip dateStr="2015-09-24 23:30:17.033000000">
+</gr-date-formatter>
+`);
+
+const lightFixture = fixtureFromTemplate(html`
+<gr-date-formatter dateStr="2015-09-24 23:30:17.033000000"></gr-date-formatter>
 `);
 
 suite('gr-date-formatter tests', () => {
   let element;
 
   setup(() => {
-
   });
 
   /**
@@ -41,7 +45,7 @@
     return d;
   }
 
-  function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
+  async function testDates(nowStr, dateStr, expected, expectedWithDateAndTime,
       expectedTooltip) {
     // Normalize and convert the date to mimic server response.
     dateStr = normalizedDate(dateStr)
@@ -50,13 +54,13 @@
         .slice(0, -1);
     sinon.useFakeTimers(normalizedDate(nowStr).getTime());
     element.dateStr = dateStr;
-    flush();
-    const span = element.shadowRoot
-        .querySelector('span');
+    await element.updateComplete;
+    const span = element.shadowRoot.querySelector('span');
+    const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
     assert.equal(span.textContent.trim(), expected);
-    assert.equal(element.title, expectedTooltip);
+    assert.equal(tooltip.title, expectedTooltip);
     element.showDateAndTime = true;
-    flush();
+    await element.updateComplete;
     assert.equal(span.textContent.trim(), expectedWithDateAndTime);
   }
 
@@ -81,35 +85,37 @@
 
     test('invalid dates are quietly rejected', () => {
       assert.notOk((new Date('foo')).valueOf());
-      assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
+      element.dateStr = 'foo';
+      element.timeFormat = 'h:mm A';
+      assert.equal(element._computeDateStr(), '');
     });
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           'Jul 29, 2015, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           'Jul 28',
           'Jul 28 20:25',
           'Jul 28, 2015, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           'Jun 15',
           'Jun 15 03:25',
           'Jun 15, 2015, 03:25:14');
     });
 
-    test('More than six months', () => {
-      testDates('2015-09-15 20:34:00.000000000',
+    test('More than six months', async () => {
+      await testDates('2015-09-15 20:34:00.000000000',
           '2015-01-15 03:25:00.000000000',
           'Jan 15, 2015',
           'Jan 15, 2015 03:25',
@@ -128,24 +134,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '07/29/15, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '07/28',
           '07/28 20:25',
           '07/28/15, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '06/15',
           '06/15 03:25',
@@ -164,24 +170,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '2015-07-29, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '07-28',
           '07-28 20:25',
           '2015-07-28, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '06-15',
           '06-15 03:25',
@@ -200,24 +206,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '29.07.2015, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '28. Jul',
           '28. Jul 20:25',
           '28.07.2015, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '15. Jun',
           '15. Jun 03:25',
@@ -236,24 +242,24 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '15:34',
           '15:34',
           '29/07/2015, 15:34:14');
     });
 
-    test('Within 24 hours on different days', () => {
-      testDates('2015-07-29 03:34:14.985000000',
+    test('Within 24 hours on different days', async () => {
+      await testDates('2015-07-29 03:34:14.985000000',
           '2015-07-28 20:25:14.985000000',
           '28/07',
           '28/07 20:25',
           '28/07/2015, 20:25:14');
     });
 
-    test('More than 24 hours but less than six months', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('More than 24 hours but less than six months', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-06-15 03:25:14.985000000',
           '15/06',
           '15/06 03:25',
@@ -273,8 +279,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -294,8 +300,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -315,8 +321,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -336,8 +342,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -357,8 +363,8 @@
       })
     );
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '3:34 PM',
           '3:34 PM',
@@ -377,16 +383,16 @@
       return element._loadPreferences();
     }));
 
-    test('Within 24 hours on same day', () => {
-      testDates('2015-07-29 20:34:14.985000000',
+    test('Within 24 hours on same day', async () => {
+      await testDates('2015-07-29 20:34:14.985000000',
           '2015-07-29 15:34:14.985000000',
           '5 hours ago',
           '5 hours ago',
           'Jul 29, 2015, 3:34:14 PM');
     });
 
-    test('More than six months', () => {
-      testDates('2015-09-15 20:34:00.000000000',
+    test('More than six months', async () => {
+      await testDates('2015-09-15 20:34:00.000000000',
           '2015-01-15 03:25:00.000000000',
           '8 months ago',
           '8 months ago',
@@ -405,10 +411,10 @@
     }));
 
     test('Preferences are respected', () => {
-      assert.equal(element._timeFormat, 'h:mm A');
-      assert.equal(element._dateFormat.short, 'MM/DD');
-      assert.equal(element._dateFormat.full, 'MM/DD/YY');
-      assert.isTrue(element._relative);
+      assert.equal(element.timeFormat, 'h:mm A');
+      assert.equal(element.dateFormat.short, 'MM/DD');
+      assert.equal(element.dateFormat.full, 'MM/DD/YY');
+      assert.isTrue(element.relative);
     });
   });
 
@@ -419,10 +425,38 @@
     }));
 
     test('Default preferences are respected', () => {
-      assert.equal(element._timeFormat, 'HH:mm');
-      assert.equal(element._dateFormat.short, 'MMM DD');
-      assert.equal(element._dateFormat.full, 'MMM DD, YYYY');
-      assert.isFalse(element._relative);
+      assert.equal(element.timeFormat, 'HH:mm');
+      assert.equal(element.dateFormat.short, 'MMM DD');
+      assert.equal(element.dateFormat.full, 'MMM DD, YYYY');
+      assert.isFalse(element.relative);
+    });
+  });
+
+  suite('with tooltip', () => {
+    setup(async () => {
+      await stubRestAPI(null);
+      element = basicFixture.instantiate();
+      await element._loadPreferences();
+      await element.updateComplete;
+    });
+
+    test('Tooltip is present', () => {
+      const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
+      assert.isOk(tooltip);
+    });
+  });
+
+  suite('without tooltip', () => {
+    setup(async () => {
+      await stubRestAPI(null);
+      element = lightFixture.instantiate();
+      await element._loadPreferences();
+      await element.updateComplete;
+    });
+
+    test('Tooltip is absent', () => {
+      const tooltip = element.shadowRoot.querySelector('gr-tooltip-content');
+      assert.isNotOk(tooltip);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index f39e5b8..27c6341 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 import '../gr-button/gr-button';
+import '../../../styles/gr-font-styles';
 import '../../../styles/shared-styles';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-dialog_html';
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
index f8ddcfd..a5cf8f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="shared-styles">
     :host {
       color: var(--primary-text-color);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
index 18a46a0..9ec1d39 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
@@ -123,6 +123,7 @@
     class="dropdown-trigger"
     on-click="_showDropdownTapHandler"
     slot="dropdown-trigger"
+    no-uppercase
   >
     <span id="triggerText">[[text]]</span>
     <gr-copy-clipboard
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
index 078e2ac..f4179f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -67,8 +67,11 @@
   bold?: boolean;
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-dropdown')
-export class GrDropdown extends KeyboardShortcutMixin(PolymerElement) {
+export class GrDropdown extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index b2bd925..bd1046f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -48,8 +48,11 @@
   };
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-editable-label')
-export class GrEditableLabel extends KeyboardShortcutMixin(PolymerElement) {
+export class GrEditableLabel extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
index ac14960..3f759b5 100644
--- a/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-file-status-chip/gr-file-status-chip.ts
@@ -18,8 +18,8 @@
 import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
 import {hasOwnProperty} from '../../../utils/common-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 const FileStatus = {
   A: 'Added',
@@ -32,11 +32,11 @@
 };
 
 @customElement('gr-file-status-chip')
-export class GrFileStatusChip extends GrLitElement {
+export class GrFileStatusChip extends LitElement {
   @property({type: Object})
   file?: NormalizedFileInfo;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -65,7 +65,7 @@
     ];
   }
 
-  render() {
+  override render() {
     return html` <span
       class="${this._computeStatusClass(this.file)}"
       tabindex="0"
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 487f288..17621c3 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,17 +16,23 @@
  */
 import '../gr-linked-text/gr-linked-text';
 import {CommentLinks} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html, TemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
 
-export interface Block {
-  type: string;
-  text?: string;
-  blocks?: Block[];
-  items?: string[];
+export type Block = ListBlock | QuoteBlock | TextBlock;
+export interface ListBlock {
+  type: 'list';
+  items: string[];
+}
+export interface QuoteBlock {
+  type: 'quote';
+  blocks: Block[];
+}
+export interface TextBlock {
+  type: 'paragraph' | 'code' | 'pre';
+  text: string;
 }
 
 declare global {
@@ -35,7 +41,7 @@
   }
 }
 @customElement('gr-formatted-text')
-export class GrFormattedText extends GrLitElement {
+export class GrFormattedText extends LitElement {
   @property({type: String})
   content?: string;
 
@@ -45,8 +51,6 @@
   @property({type: Boolean, reflect: true})
   noTrailingMargin = false;
 
-  private readonly reporting = appContext.reportingService;
-
   static override get styles() {
     return [
       css`
@@ -99,8 +103,9 @@
   }
 
   override render() {
-    const nodes = this._computeNodes(this._computeBlocks(this.content));
-    return html`<div id="container">${nodes}</div>`;
+    if (!this.content) return;
+    const blocks = this._computeBlocks(this.content);
+    return html`${blocks.map(block => this.renderBlock(block))}`;
   }
 
   /**
@@ -123,9 +128,7 @@
    *
    * NOTE: Strings appearing in all block objects are NOT escaped.
    */
-  _computeBlocks(content?: string): Block[] {
-    if (!content) return [];
-
+  _computeBlocks(content: string): Block[] {
     const result: Block[] = [];
     const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
 
@@ -134,82 +137,87 @@
         continue;
       }
 
-      if (this._isCodeMarkLine(lines[i])) {
-        // handle multi-line code
-        let nextI = i + 1;
-        while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
-          nextI++;
-        }
-
-        if (this._isCodeMarkLine(lines[nextI])) {
+      if (this.isCodeMarkLine(lines[i])) {
+        const startOfCode = i + 1;
+        const endOfCode = this.getEndOfSection(
+          lines,
+          startOfCode,
+          line => !this.isCodeMarkLine(line)
+        );
+        // If the code extends to the end then there is no closing``` and the
+        // opening``` should not be counted as a multiline code block.
+        const lineAfterCode = lines[endOfCode];
+        if (lineAfterCode && this.isCodeMarkLine(lineAfterCode)) {
           result.push({
             type: 'code',
-            text: lines.slice(i + 1, nextI).join('\n'),
+            // Does not include either of the ``` lines
+            text: lines.slice(startOfCode, endOfCode).join('\n'),
           });
-          i = nextI;
+          i = endOfCode; // advances past the closing```
           continue;
         }
-
-        // otherwise treat it as regular line and continue
-        // check for other cases
       }
-
-      if (this._isSingleLineCode(lines[i])) {
+      if (this.isSingleLineCode(lines[i])) {
         // no guard check as _isSingleLineCode tested on the pattern
         const codeContent = lines[i].match(CODE_MARKER_PATTERN)![2];
         result.push({type: 'code', text: codeContent});
-      } else if (this._isList(lines[i])) {
-        let nextI = i + 1;
-        while (this._isList(lines[nextI])) {
-          nextI++;
-        }
-        result.push(this._makeList(lines.slice(i, nextI)));
-        i = nextI - 1;
-      } else if (this._isQuote(lines[i])) {
-        let nextI = i + 1;
-        while (this._isQuote(lines[nextI])) {
-          nextI++;
-        }
+      } else if (this.isList(lines[i])) {
+        const endOfList = this.getEndOfSection(lines, i + 1, line =>
+          this.isList(line)
+        );
+        result.push(this.makeList(lines.slice(i, endOfList)));
+        i = endOfList - 1;
+      } else if (this.isQuote(lines[i])) {
+        const endOfQuote = this.getEndOfSection(lines, i + 1, line =>
+          this.isQuote(line)
+        );
         const blockLines = lines
-          .slice(i, nextI)
+          .slice(i, endOfQuote)
           .map(l => l.replace(/^[ ]?>[ ]?/, ''));
         result.push({
           type: 'quote',
           blocks: this._computeBlocks(blockLines.join('\n')),
         });
-        i = nextI - 1;
-      } else if (this._isPreFormat(lines[i])) {
-        let nextI = i + 1;
+        i = endOfQuote - 1;
+      } else if (this.isPreFormat(lines[i])) {
         // include pre or all regular lines but stop at next new line
-        while (
-          this._isPreFormat(lines[nextI]) ||
-          (this._isRegularLine(lines[nextI]) &&
-            !this._isWhitespaceLine(lines[nextI]) &&
-            lines[nextI].length)
-        ) {
-          nextI++;
-        }
+        const predicate = (line: string) =>
+          this.isPreFormat(line) ||
+          (this.isRegularLine(line) &&
+            !this.isWhitespaceLine(line) &&
+            line.length > 0);
+        const endOfPre = this.getEndOfSection(lines, i + 1, predicate);
         result.push({
           type: 'pre',
-          text: lines.slice(i, nextI).join('\n'),
+          text: lines.slice(i, endOfPre).join('\n'),
         });
-        i = nextI - 1;
+        i = endOfPre - 1;
       } else {
-        let nextI = i + 1;
-        while (this._isRegularLine(lines[nextI])) {
-          nextI++;
-        }
+        const endOfRegularLines = this.getEndOfSection(lines, i + 1, line =>
+          this.isRegularLine(line)
+        );
         result.push({
           type: 'paragraph',
-          text: lines.slice(i, nextI).join('\n'),
+          text: lines.slice(i, endOfRegularLines).join('\n'),
         });
-        i = nextI - 1;
+        i = endOfRegularLines - 1;
       }
     }
 
     return result;
   }
 
+  private getEndOfSection(
+    lines: string[],
+    startIndex: number,
+    sectionPredicate: (line: string) => boolean
+  ) {
+    const index = lines
+      .slice(startIndex)
+      .findIndex(line => !sectionPredicate(line));
+    return index === -1 ? lines.length : index + startIndex;
+  }
+
   /**
    * Take a block of comment text that contains a list, generate appropriate
    * block objects and append them to the output list.
@@ -222,105 +230,78 @@
    *
    * @param lines The block containing the list.
    */
-  _makeList(lines: string[]) {
-    const items = [];
-    for (let i = 0; i < lines.length; i++) {
-      let line = lines[i];
-      line = line.substring(1).trim();
-      items.push(line);
-    }
+  private makeList(lines: string[]): Block {
+    const items = lines.map(line => line.substring(1).trim());
     return {type: 'list', items};
   }
 
-  _isRegularLine(line: string) {
-    // line can not be recognized by existing patterns
-    if (line === undefined) return false;
+  private isRegularLine(line: string): boolean {
     return (
-      !this._isQuote(line) &&
-      !this._isCodeMarkLine(line) &&
-      !this._isSingleLineCode(line) &&
-      !this._isList(line) &&
-      !this._isPreFormat(line)
+      !this.isQuote(line) &&
+      !this.isCodeMarkLine(line) &&
+      !this.isSingleLineCode(line) &&
+      !this.isList(line) &&
+      !this.isPreFormat(line)
     );
   }
 
-  _isQuote(line: string) {
-    return line && (line.startsWith('> ') || line.startsWith(' > '));
+  private isQuote(line: string): boolean {
+    return line.startsWith('> ') || line.startsWith(' > ');
   }
 
-  _isCodeMarkLine(line: string) {
-    return line && line.trim() === '```';
+  private isCodeMarkLine(line: string): boolean {
+    return line.trim() === '```';
   }
 
-  _isSingleLineCode(line: string) {
-    return line && CODE_MARKER_PATTERN.test(line);
+  private isSingleLineCode(line: string): boolean {
+    return CODE_MARKER_PATTERN.test(line);
   }
 
-  _isPreFormat(line: string) {
-    return line && /^[ \t]/.test(line) && !this._isWhitespaceLine(line);
+  private isPreFormat(line: string): boolean {
+    return /^[ \t]/.test(line) && !this.isWhitespaceLine(line);
   }
 
-  _isList(line: string) {
-    return line && /^[-*] /.test(line);
+  private isList(line: string): boolean {
+    return /^[-*] /.test(line);
   }
 
-  _isWhitespaceLine(line: string) {
-    return line && /^\s+$/.test(line);
+  private isWhitespaceLine(line: string): boolean {
+    return /^\s+$/.test(line);
   }
 
-  _makeLinkedText(content = '', isPre?: boolean) {
-    const text = document.createElement('gr-linked-text');
-    text.config = this.config;
-    text.content = content;
-    text.pre = true;
-    if (isPre) {
-      text.classList.add('pre');
+  private renderLinkedText(content: string, isPre?: boolean): TemplateResult {
+    return html`
+      <gr-linked-text
+        class="${isPre ? 'pre' : ''}"
+        .config=${this.config}
+        content=${content}
+        pre
+      ></gr-linked-text>
+    `;
+  }
+
+  private renderBlock(block: Block): TemplateResult {
+    switch (block.type) {
+      case 'paragraph':
+        return html`<p>${this.renderLinkedText(block.text)}</p>`;
+      case 'quote':
+        return html`
+          <blockquote>
+            ${block.blocks.map(subBlock => this.renderBlock(subBlock))}
+          </blockquote>
+        `;
+      case 'code':
+        return html`<code>${block.text}</code>`;
+      case 'pre':
+        return this.renderLinkedText(block.text, true);
+      case 'list':
+        return html`
+          <ul>
+            ${block.items.map(
+              item => html`<li>${this.renderLinkedText(item)}</li>`
+            )}
+          </ul>
+        `;
     }
-    return text;
-  }
-
-  /**
-   * Map an array of block objects to an array of DOM nodes.
-   */
-  _computeNodes(blocks: Block[]): HTMLElement[] {
-    return blocks.map(block => {
-      if (block.type === 'paragraph') {
-        const p = document.createElement('p');
-        p.appendChild(this._makeLinkedText(block.text));
-        return p;
-      }
-
-      if (block.type === 'quote') {
-        const bq = document.createElement('blockquote');
-        for (const node of this._computeNodes(block.blocks || [])) {
-          if (node) bq.appendChild(node);
-        }
-        return bq;
-      }
-
-      if (block.type === 'code') {
-        const code = document.createElement('code');
-        code.textContent = block.text || '';
-        return code;
-      }
-
-      if (block.type === 'pre') {
-        return this._makeLinkedText(block.text, true);
-      }
-
-      if (block.type === 'list') {
-        const ul = document.createElement('ul');
-        const items = block.items || [];
-        for (const item of items) {
-          const li = document.createElement('li');
-          li.appendChild(this._makeLinkedText(item));
-          ul.appendChild(li);
-        }
-        return ul;
-      }
-
-      this.reporting.error(new Error(`Unrecognized block type: ${block.type}`));
-      return document.createElement('span');
-    });
   }
 }
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 cf12032..8cecf71 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
@@ -17,40 +17,41 @@
 
 import '../../../test/common-test-setup-karma';
 import './gr-formatted-text';
-import {GrFormattedText, Block} from './gr-formatted-text';
+import {
+  GrFormattedText,
+  Block,
+  ListBlock,
+  TextBlock,
+  QuoteBlock,
+} from './gr-formatted-text';
 
 const basicFixture = fixtureFromElement('gr-formatted-text');
 
 suite('gr-formatted-text tests', () => {
   let element: GrFormattedText;
 
-  function assertBlock(
-    result: Block[],
-    index: number,
-    type: string,
-    text?: string
-  ) {
-    assert.equal(result[index].type, type);
-    assert.equal(result[index].text, text);
+  function assertTextBlock(block: Block, type: string, text: string) {
+    assert.equal(block.type, type);
+    const textBlock = block as TextBlock;
+    assert.equal(textBlock.text, text);
   }
 
-  function assertListBlock(
-    result: Block[],
-    resultIndex: number,
-    itemIndex: number,
-    text: string
-  ) {
-    assert.equal(result[resultIndex].type, 'list');
-    const item = result[resultIndex].items?.[itemIndex];
-    assert.equal(item, text);
+  function assertListBlock(block: Block, items: string[]) {
+    assert.equal(block.type, 'list');
+    const listBlock = block as ListBlock;
+    assert.deepEqual(listBlock.items, items);
+  }
+
+  function assertQuoteBlock(block: Block): QuoteBlock {
+    assert.equal(block.type, 'quote');
+    return block as QuoteBlock;
   }
 
   setup(() => {
     element = basicFixture.instantiate();
   });
 
-  test('parse null undefined and empty', () => {
-    assert.lengthOf(element._computeBlocks(undefined), 0);
+  test('parse empty', () => {
     assert.lengthOf(element._computeBlocks(''), 0);
   });
 
@@ -58,53 +59,49 @@
     const comment = 'Para1';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertBlock(result, 0, 'paragraph', comment);
+    assertTextBlock(result[0], 'paragraph', comment);
   });
 
   test('parse multiline para', () => {
     const comment = 'Para 1\nStill para 1';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertBlock(result, 0, 'paragraph', comment);
+    assertTextBlock(result[0], 'paragraph', comment);
   });
 
   test('parse para break without special blocks', () => {
     const comment = 'Para 1\n\nPara 2\n\nPara 3';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertBlock(result, 0, 'paragraph', comment);
+    assertTextBlock(result[0], 'paragraph', comment);
   });
 
   test('parse quote', () => {
     const comment = '> Quote text';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assert.equal(result[0].type, 'quote');
-    const blocks = result[0].blocks!;
-    assert.lengthOf(blocks, 1);
-    assertBlock(blocks, 0, 'paragraph', 'Quote text');
+    const quoteBlock = assertQuoteBlock(result[0]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(quoteBlock.blocks[0], 'paragraph', 'Quote text');
   });
 
   test('parse quote lead space', () => {
     const comment = ' > Quote text';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assert.equal(result[0].type, 'quote');
-    const blocks = result[0].blocks!;
-    assert.lengthOf(blocks, 1);
-    assertBlock(blocks, 0, 'paragraph', 'Quote text');
+    const quoteBlock = assertQuoteBlock(result[0]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(quoteBlock.blocks[0], 'paragraph', 'Quote text');
   });
 
   test('parse multiline quote', () => {
     const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assert.equal(result[0].type, 'quote');
-    const blocks = result[0].blocks!;
-    assert.lengthOf(blocks, 1);
-    assertBlock(
-      blocks,
-      0,
+    const quoteBlock = assertQuoteBlock(result[0]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(
+      quoteBlock.blocks[0],
       'paragraph',
       'Quote line 1\nQuote line 2\nQuote line 3'
     );
@@ -114,49 +111,42 @@
     const comment = '    Four space indent.';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertBlock(result, 0, 'pre', comment);
+    assertTextBlock(result[0], 'pre', comment);
   });
 
   test('parse one space pre', () => {
     const comment = ' One space indent.\n Another line.';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertBlock(result, 0, 'pre', comment);
+    assertTextBlock(result[0], 'pre', comment);
   });
 
   test('parse tab pre', () => {
     const comment = '\tOne tab indent.\n\tAnother line.\n  Yet another!';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertBlock(result, 0, 'pre', comment);
+    assertTextBlock(result[0], 'pre', comment);
   });
 
   test('parse star list', () => {
     const comment = '* Item 1\n* Item 2\n* Item 3';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertListBlock(result, 0, 0, 'Item 1');
-    assertListBlock(result, 0, 1, 'Item 2');
-    assertListBlock(result, 0, 2, 'Item 3');
+    assertListBlock(result[0], ['Item 1', 'Item 2', 'Item 3']);
   });
 
   test('parse dash list', () => {
     const comment = '- Item 1\n- Item 2\n- Item 3';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertListBlock(result, 0, 0, 'Item 1');
-    assertListBlock(result, 0, 1, 'Item 2');
-    assertListBlock(result, 0, 2, 'Item 3');
+    assertListBlock(result[0], ['Item 1', 'Item 2', 'Item 3']);
   });
 
   test('parse mixed list', () => {
     const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assertListBlock(result, 0, 0, 'Item 1');
-    assertListBlock(result, 0, 1, 'Item 2');
-    assertListBlock(result, 0, 2, 'Item 3');
-    assertListBlock(result, 0, 3, 'Item 4');
+    assertListBlock(result[0], ['Item 1', 'Item 2', 'Item 3', 'Item 4']);
   });
 
   test('parse mixed block types', () => {
@@ -176,64 +166,75 @@
       'Parting words.';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 7);
-    assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n');
+    assertTextBlock(
+      result[0],
+      'paragraph',
+      'Paragraph\nacross\na\nfew\nlines.\n'
+    );
 
-    assert.equal(result[1].type, 'quote');
-    const secondBlocks = result[1].blocks!;
-    assert.lengthOf(secondBlocks, 1);
-    assertBlock(secondBlocks, 0, 'paragraph', 'Quote\nacross\nnot many lines.');
+    const quoteBlock = assertQuoteBlock(result[1]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(
+      quoteBlock.blocks[0],
+      'paragraph',
+      'Quote\nacross\nnot many lines.'
+    );
 
-    assertBlock(result, 2, 'paragraph', 'Another paragraph\n');
-    assertListBlock(result, 3, 0, 'Series');
-    assertListBlock(result, 3, 1, 'of');
-    assertListBlock(result, 3, 2, 'list');
-    assertListBlock(result, 3, 3, 'items');
-    assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n');
-    assertBlock(result, 5, 'pre', '\tPreformatted text.');
-    assertBlock(result, 6, 'paragraph', 'Parting words.');
+    assertTextBlock(result[2], 'paragraph', 'Another paragraph\n');
+    assertListBlock(result[3], ['Series', 'of', 'list', 'items']);
+    assertTextBlock(result[4], 'paragraph', 'Yet another paragraph\n');
+    assertTextBlock(result[5], 'pre', '\tPreformatted text.');
+    assertTextBlock(result[6], 'paragraph', 'Parting words.');
   });
 
   test('bullet list 1', () => {
-    const comment = 'A\n\n* line 1\n* 2nd line';
+    const comment = 'A\n\n* line 1';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'paragraph', 'A\n');
-    assertListBlock(result, 1, 0, 'line 1');
-    assertListBlock(result, 1, 1, '2nd line');
+    assertTextBlock(result[0], 'paragraph', 'A\n');
+    assertListBlock(result[1], ['line 1']);
   });
 
   test('bullet list 2', () => {
-    const comment = 'A\n* line 1\n* 2nd line\n\nB';
+    const comment = 'A\n\n* line 1\n* 2nd line';
     const result = element._computeBlocks(comment);
-    assert.lengthOf(result, 3);
-    assertBlock(result, 0, 'paragraph', 'A');
-    assertListBlock(result, 1, 0, 'line 1');
-    assertListBlock(result, 1, 1, '2nd line');
-    assertBlock(result, 2, 'paragraph', 'B');
+    assert.lengthOf(result, 2);
+    assertTextBlock(result[0], 'paragraph', 'A\n');
+    assertListBlock(result[1], ['line 1', '2nd line']);
   });
 
   test('bullet list 3', () => {
-    const comment = '* line 1\n* 2nd line\n\nB';
+    const comment = 'A\n* line 1\n* 2nd line\n\nB';
     const result = element._computeBlocks(comment);
-    assert.lengthOf(result, 2);
-    assertListBlock(result, 0, 0, 'line 1');
-    assertListBlock(result, 0, 1, '2nd line');
-    assertBlock(result, 1, 'paragraph', 'B');
+    assert.lengthOf(result, 3);
+    assertTextBlock(result[0], 'paragraph', 'A');
+    assertListBlock(result[1], ['line 1', '2nd line']);
+    assertTextBlock(result[2], 'paragraph', 'B');
   });
 
   test('bullet list 4', () => {
+    const comment = '* line 1\n* 2nd line\n\nB';
+    const result = element._computeBlocks(comment);
+    assert.lengthOf(result, 2);
+    assertListBlock(result[0], ['line 1', '2nd line']);
+    assertTextBlock(result[1], 'paragraph', 'B');
+  });
+
+  test('bullet list 5', () => {
     const comment =
       'To see this bug, you have to:\n' +
       '* Be on IMAP or EAS (not on POP)\n' +
       '* Be very unlucky\n';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:');
-    assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
-    assertListBlock(result, 1, 1, 'Be very unlucky');
+    assertTextBlock(result[0], 'paragraph', 'To see this bug, you have to:');
+    assertListBlock(result[1], [
+      'Be on IMAP or EAS (not on POP)',
+      'Be very unlucky',
+    ]);
   });
 
-  test('bullet list 5', () => {
+  test('bullet list 6', () => {
     const comment =
       'To see this bug,\n' +
       'you have to:\n' +
@@ -241,37 +242,36 @@
       '* Be very unlucky\n';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:');
-    assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)');
-    assertListBlock(result, 1, 1, 'Be very unlucky');
+    assertTextBlock(result[0], 'paragraph', 'To see this bug,\nyou have to:');
+    assertListBlock(result[1], [
+      'Be on IMAP or EAS (not on POP)',
+      'Be very unlucky',
+    ]);
   });
 
   test('dash list 1', () => {
     const comment = 'A\n- line 1\n- 2nd line';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'paragraph', 'A');
-    assertListBlock(result, 1, 0, 'line 1');
-    assertListBlock(result, 1, 1, '2nd line');
+    assertTextBlock(result[0], 'paragraph', 'A');
+    assertListBlock(result[1], ['line 1', '2nd line']);
   });
 
   test('dash list 2', () => {
     const comment = 'A\n- line 1\n- 2nd line\n\nB';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 3);
-    assertBlock(result, 0, 'paragraph', 'A');
-    assertListBlock(result, 1, 0, 'line 1');
-    assertListBlock(result, 1, 1, '2nd line');
-    assertBlock(result, 2, 'paragraph', 'B');
+    assertTextBlock(result[0], 'paragraph', 'A');
+    assertListBlock(result[1], ['line 1', '2nd line']);
+    assertTextBlock(result[2], 'paragraph', 'B');
   });
 
   test('dash list 3', () => {
     const comment = '- line 1\n- 2nd line\n\nB';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertListBlock(result, 0, 0, 'line 1');
-    assertListBlock(result, 0, 1, '2nd line');
-    assertBlock(result, 1, 'paragraph', 'B');
+    assertListBlock(result[0], ['line 1', '2nd line']);
+    assertTextBlock(result[1], 'paragraph', 'B');
   });
 
   test('nested list will NOT be recognized', () => {
@@ -279,134 +279,143 @@
     const comment = '- line 1\n  - line with indentation\n- line 2';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 3);
-    assertListBlock(result, 0, 0, 'line 1');
-    assert.equal(result[1].type, 'pre');
-    assertListBlock(result, 2, 0, 'line 2');
+    assertListBlock(result[0], ['line 1']);
+    assertTextBlock(result[1], 'pre', '  - line with indentation');
+    assertListBlock(result[2], ['line 2']);
   });
 
   test('pre format 1', () => {
     const comment = 'A\n  This is pre\n  formatted';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'paragraph', 'A');
-    assertBlock(result, 1, 'pre', '  This is pre\n  formatted');
+    assertTextBlock(result[0], 'paragraph', 'A');
+    assertTextBlock(result[1], 'pre', '  This is pre\n  formatted');
   });
 
   test('pre format 2', () => {
     const comment = 'A\n  This is pre\n  formatted\n\nbut this is not';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 3);
-    assertBlock(result, 0, 'paragraph', 'A');
-    assertBlock(result, 1, 'pre', '  This is pre\n  formatted');
-    assertBlock(result, 2, 'paragraph', 'but this is not');
+    assertTextBlock(result[0], 'paragraph', 'A');
+    assertTextBlock(result[1], 'pre', '  This is pre\n  formatted');
+    assertTextBlock(result[2], 'paragraph', 'but this is not');
   });
 
   test('pre format 3', () => {
     const comment = 'A\n  Q\n    <R>\n  S\n\nB';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 3);
-    assertBlock(result, 0, 'paragraph', 'A');
-    assertBlock(result, 1, 'pre', '  Q\n    <R>\n  S');
-    assertBlock(result, 2, 'paragraph', 'B');
+    assertTextBlock(result[0], 'paragraph', 'A');
+    assertTextBlock(result[1], 'pre', '  Q\n    <R>\n  S');
+    assertTextBlock(result[2], 'paragraph', 'B');
   });
 
   test('pre format 4', () => {
     const comment = '  Q\n    <R>\n  S\n\nB';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'pre', '  Q\n    <R>\n  S');
-    assertBlock(result, 1, 'paragraph', 'B');
+    assertTextBlock(result[0], 'pre', '  Q\n    <R>\n  S');
+    assertTextBlock(result[1], 'paragraph', 'B');
   });
 
   test('pre format 5', () => {
     const comment = '  Q\n    <R>\n  S\n \nB';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assertBlock(result, 0, 'pre', '  Q\n    <R>\n  S');
-    assertBlock(result, 1, 'paragraph', ' \nB');
+    assertTextBlock(result[0], 'pre', '  Q\n    <R>\n  S');
+    assertTextBlock(result[1], 'paragraph', ' \nB');
   });
 
   test('quote 1', () => {
-    const comment = "> I'm happy\n > with quotes!\n\nSee above.";
+    const comment = "> I'm happy with quotes!!";
     const result = element._computeBlocks(comment);
-    assert.lengthOf(result, 2);
-    assert.equal(result[0].type, 'quote');
-    const blocks = result[0].blocks!;
-    assert.lengthOf(blocks, 1);
-    assertBlock(blocks, 0, 'paragraph', "I'm happy\nwith quotes!");
-    assertBlock(result, 1, 'paragraph', 'See above.');
+    assert.lengthOf(result, 1);
+    const quoteBlock = assertQuoteBlock(result[0]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(
+      quoteBlock.blocks[0],
+      'paragraph',
+      "I'm happy with quotes!!"
+    );
   });
 
   test('quote 2', () => {
+    const comment = "> I'm happy\n > with quotes!\n\nSee above.";
+    const result = element._computeBlocks(comment);
+    assert.lengthOf(result, 2);
+    const quoteBlock = assertQuoteBlock(result[0]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(
+      quoteBlock.blocks[0],
+      'paragraph',
+      "I'm happy\nwith quotes!"
+    );
+    assertTextBlock(result[1], 'paragraph', 'See above.');
+  });
+
+  test('quote 3', () => {
     const comment = 'See this said:\n > a quoted\n > string block\n\nOK?';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 3);
-    assertBlock(result, 0, 'paragraph', 'See this said:');
-    assert.equal(result[1].type, 'quote');
-    const secondBlocks = result[1].blocks!;
-    assert.lengthOf(secondBlocks, 1);
-    assertBlock(secondBlocks, 0, 'paragraph', 'a quoted\nstring block');
-    assertBlock(result, 2, 'paragraph', 'OK?');
+    assertTextBlock(result[0], 'paragraph', 'See this said:');
+    const quoteBlock = assertQuoteBlock(result[1]);
+    assert.lengthOf(quoteBlock.blocks, 1);
+    assertTextBlock(
+      quoteBlock.blocks[0],
+      'paragraph',
+      'a quoted\nstring block'
+    );
+    assertTextBlock(result[2], 'paragraph', 'OK?');
   });
 
   test('nested quotes', () => {
     const comment = ' > > prior\n > \n > next\n';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assert.equal(result[0].type, 'quote');
-    const blocks = result[0].blocks!;
-    assert.lengthOf(blocks, 2);
-    assert.equal(blocks[0].type, 'quote');
-    const blocksBlocks = blocks[0].blocks!;
-    assert.lengthOf(blocksBlocks, 1);
-    assertBlock(blocksBlocks, 0, 'paragraph', 'prior');
-    assertBlock(blocks, 1, 'paragraph', 'next');
+    const outerQuoteBlock = assertQuoteBlock(result[0]);
+    assert.lengthOf(outerQuoteBlock.blocks, 2);
+    const nestedQuoteBlock = assertQuoteBlock(outerQuoteBlock.blocks[0]);
+    assert.lengthOf(nestedQuoteBlock.blocks, 1);
+    assertTextBlock(nestedQuoteBlock.blocks[0], 'paragraph', 'prior');
+    assertTextBlock(outerQuoteBlock.blocks[1], 'paragraph', 'next');
   });
 
   test('code 1', () => {
     const comment = '```\n// test code\n```';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assert.equal(result[0].type, 'code');
-    assert.equal(result[0].text, '// test code');
+    assertTextBlock(result[0], 'code', '// test code');
   });
 
   test('code 2', () => {
     const comment = 'test code\n```// test code```';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assert.equal(result[0].type, 'paragraph');
-    assert.equal(result[0].text, 'test code');
-    assert.equal(result[1].type, 'code');
-    assert.equal(result[1].text, '// test code');
+    assertTextBlock(result[0], 'paragraph', 'test code');
+    assertTextBlock(result[1], 'code', '// test code');
   });
 
-  test('code 3', () => {
-    const comment = 'test code\n```// test code```';
-    const result = element._computeBlocks(comment);
-    assert.lengthOf(result, 2);
-    assert.equal(result[0].type, 'paragraph');
-    assert.equal(result[0].text, 'test code');
-    assert.equal(result[1].type, 'code');
-    assert.equal(result[1].text, '// test code');
-  });
-
-  test('not a code', () => {
+  test('not a code block', () => {
     const comment = 'test code\n```// test code';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 1);
-    assert.equal(result[0].type, 'paragraph');
-    assert.equal(result[0].text, 'test code\n```// test code');
+    assertTextBlock(result[0], 'paragraph', 'test code\n```// test code');
   });
 
-  test('not a code 2', () => {
+  test('not a code block 2', () => {
     const comment = 'test code\n```\n// test code';
     const result = element._computeBlocks(comment);
     assert.lengthOf(result, 2);
-    assert.equal(result[0].type, 'paragraph');
-    assert.equal(result[0].text, 'test code');
-    assert.equal(result[1].type, 'paragraph');
-    assert.equal(result[1].text, '```\n// test code');
+    assertTextBlock(result[0], 'paragraph', 'test code');
+    assertTextBlock(result[1], 'paragraph', '```\n// test code');
+  });
+
+  test('not a code block 3', () => {
+    const comment = 'test code\n```';
+    const result = element._computeBlocks(comment);
+    assert.lengthOf(result, 2);
+    assertTextBlock(result[0], 'paragraph', 'test code');
+    assertTextBlock(result[1], 'paragraph', '```');
   });
 
   test('mix all 1', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
index a788f05..b789bee 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -16,10 +16,11 @@
  */
 
 import '@polymer/iron-icon/iron-icon';
+import '../../../styles/gr-font-styles';
 import '../../../styles/shared-styles';
 import '../gr-avatar/gr-avatar';
 import '../gr-button/gr-button';
-import {hovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior';
+import {HovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-hovercard-account_html';
 import {appContext} from '../../../services/app-context';
@@ -45,8 +46,11 @@
 import {isInvolved, isRemovableReviewer} from '../../../utils/change-util';
 import {assertIsDefined} from '../../../utils/common-util';
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardBehaviorMixin(PolymerElement);
+
 @customElement('gr-hovercard-account')
-export class GrHovercardAccount extends hovercardBehaviorMixin(PolymerElement) {
+export class GrHovercardAccount extends base {
   static get template() {
     return htmlTemplate;
   }
@@ -92,7 +96,7 @@
     this.reporting = appContext.reportingService;
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     this.restApiService.getConfig().then(config => {
       this._config = config;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
index dac5962..076553b 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
@@ -18,6 +18,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="gr-hovercard-shared-style">
     .top,
     .attention,
@@ -125,7 +128,7 @@
             <span class="value">[[_computeReason(change)]]</span>
             <template is="dom-if" if="[[_computeLastUpdate(change)]]">
               (<gr-date-formatter
-                has-tooltip
+                withTooltip
                 date-str="[[_computeLastUpdate(change)]]"
               ></gr-date-formatter
               >)
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
index e9a224c..82f64d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -109,7 +109,7 @@
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
-    flush();
+    await flush();
     const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
     assert.isOk(button);
     assert.equal(button.innerText, 'Remove Reviewer');
@@ -132,7 +132,7 @@
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
-    flush();
+    await flush();
     const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
 
     assert.isOk(button);
@@ -156,7 +156,7 @@
     stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
-    flush();
+    await flush();
 
     const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
     assert.isOk(button);
@@ -180,7 +180,7 @@
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
-    flush();
+    await flush();
     const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
 
     assert.equal(button.innerText, 'Remove CC');
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
index 76c13e3..bbd1708 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
@@ -19,7 +19,6 @@
 import {getRootElement} from '../../../scripts/rootElement';
 import {Constructor} from '../../../utils/common-util';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin';
 import {property, observe} from '@polymer/decorators';
 import {
   pushScrollLock,
@@ -82,401 +81,396 @@
  * @polymer
  * @mixinFunction
  */
-export const hovercardBehaviorMixin = dedupingMixin(
-  <T extends Constructor<PolymerElement>>(
-    superClass: T
-  ): T & Constructor<GrHovercardBehaviorInterface> => {
+export const HovercardBehaviorMixin = <T extends Constructor<PolymerElement>>(
+  superClass: T
+) => {
+  /**
+   * @polymer
+   * @mixinClass
+   */
+  class Mixin extends superClass {
+    @property({type: Object})
+    _target: HTMLElement | null = null;
+
+    // Determines whether or not the hovercard is visible.
+    @property({type: Boolean})
+    _isShowing = false;
+
+    // The `id` of the element that the hovercard is anchored to.
+    @property({type: String})
+    for?: string;
+
     /**
-     * @polymer
-     * @mixinClass
+     * The spacing between the top of the hovercard and the element it is
+     * anchored to.
      */
-    class Mixin extends superClass {
-      @property({type: Object})
-      _target: HTMLElement | null = null;
+    @property({type: Number})
+    offset = 14;
 
-      // Determines whether or not the hovercard is visible.
-      @property({type: Boolean})
-      _isShowing = false;
+    /**
+     * Positions the hovercard to the top, right, bottom, left, bottom-left,
+     * bottom-right, top-left, or top-right of its content.
+     */
+    @property({type: String})
+    position = 'right';
 
-      // The `id` of the element that the hovercard is anchored to.
-      @property({type: String})
-      for?: string;
+    @property({type: Object})
+    container: HTMLElement | null = null;
 
-      /**
-       * The spacing between the top of the hovercard and the element it is
-       * anchored to.
-       */
-      @property({type: Number})
-      offset = 14;
+    private hideTask?: DelayedTask;
 
-      /**
-       * Positions the hovercard to the top, right, bottom, left, bottom-left,
-       * bottom-right, top-left, or top-right of its content.
-       */
-      @property({type: String})
-      position = 'right';
+    private showTask?: DelayedTask;
 
-      @property({type: Object})
-      container: HTMLElement | null = null;
+    private isScheduledToShow?: boolean;
 
-      private hideTask?: DelayedTask;
+    private isScheduledToHide?: boolean;
 
-      private showTask?: DelayedTask;
-
-      private isScheduledToShow?: boolean;
-
-      private isScheduledToHide?: boolean;
-
-      override connectedCallback() {
-        super.connectedCallback();
-        if (!this._target) {
-          this._target = this.target;
-        }
-        this._target.addEventListener('mouseenter', this.debounceShow);
-        this._target.addEventListener('focus', this.debounceShow);
-        this._target.addEventListener('mouseleave', this.debounceHide);
-        this._target.addEventListener('blur', this.debounceHide);
-
-        // when click, dismiss immediately
-        this._target.addEventListener('click', this.hide);
-
-        // show the hovercard if mouse moves to hovercard
-        // this will cancel pending hide as well
-        this.addEventListener('mouseenter', this.show);
-        this.addEventListener('mouseenter', this.lock);
-        // when leave hovercard, hide it immediately
-        this.addEventListener('mouseleave', this.hide);
-        this.addEventListener('mouseleave', this.unlock);
-      }
-
-      override disconnectedCallback() {
-        this.cancelShowTask();
-        this.cancelHideTask();
-        this.unlock();
-        super.disconnectedCallback();
-      }
-
-      override ready() {
-        super.ready();
-        // First, check to see if the container has already been created.
-        this.container = getHovercardContainer({createIfNotExists: true});
-      }
-
-      removeListeners() {
-        this._target?.removeEventListener('mouseenter', this.debounceShow);
-        this._target?.removeEventListener('focus', this.debounceShow);
-        this._target?.removeEventListener('mouseleave', this.debounceHide);
-        this._target?.removeEventListener('blur', this.debounceHide);
-        this._target?.removeEventListener('click', this.hide);
-      }
-
-      readonly debounceHide = () => {
-        this.cancelShowTask();
-        if (!this._isShowing || this.isScheduledToHide) return;
-        this.isScheduledToHide = true;
-        this.hideTask = debounce(
-          this.hideTask,
-          () => {
-            // This happens when hide immediately through click or mouse leave
-            // on the hovercard
-            if (!this.isScheduledToHide) return;
-            this.hide();
-          },
-          HIDE_DELAY_MS
-        );
-      };
-
-      cancelHideTask() {
-        if (this.hideTask) {
-          this.hideTask.cancel();
-          this.isScheduledToHide = false;
-        }
-      }
-
-      /**
-       * Hovercard elements are created outside of <gr-app>, so if you want to fire
-       * events, then you probably want to do that through the target element.
-       */
-
-      dispatchEventThroughTarget(eventName: string): void;
-
-      dispatchEventThroughTarget(
-        eventName: 'show-alert',
-        detail: ShowAlertEventDetail
-      ): void;
-
-      dispatchEventThroughTarget(
-        eventName: 'reload',
-        detail: ReloadEventDetail
-      ): void;
-
-      dispatchEventThroughTarget(eventName: string, detail?: unknown) {
-        if (!detail) detail = {};
-        if (this._target)
-          this._target.dispatchEvent(
-            new CustomEvent(eventName, {
-              detail,
-              bubbles: true,
-              composed: true,
-            })
-          );
-      }
-
-      /**
-       * Returns the target element that the hovercard is anchored to (the `id` of
-       * the `for` property).
-       */
-      get target(): HTMLElement {
-        const parentNode = this.parentNode;
-        // If the parentNode is a document fragment, then we need to use the host.
-        const ownerRoot = this.getRootNode() as ShadowRoot;
-        let target;
-        if (this.for) {
-          target = ownerRoot.querySelector('#' + this.for);
-        } else {
-          target =
-            !parentNode || parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
-              ? ownerRoot.host
-              : parentNode;
-        }
-        return target as HTMLElement;
-      }
-
-      /**
-       * unlock scroll, this will resume the scroll outside of the hovercard.
-       */
-      readonly unlock = () => {
-        removeScrollLock(this);
-      };
-
-      /**
-       * Hides/closes the hovercard. This occurs when the user triggers the
-       * `mouseleave` event on the hovercard's `target` element (as long as the
-       * user is not hovering over the hovercard).
-       *
-       */
-      readonly hide = (e?: MouseEvent) => {
-        this.cancelHideTask();
-        this.cancelShowTask();
-        if (!this._isShowing) {
-          return;
-        }
-
-        // If the user is now hovering over the hovercard or the user is returning
-        // from the hovercard but now hovering over the target (to stop an annoying
-        // flicker effect), just return.
-        if (e) {
-          if (
-            e.relatedTarget === this ||
-            (e.target === this && e.relatedTarget === this._target)
-          ) {
-            return;
-          }
-        }
-
-        // Mark that the hovercard is not visible and do not allow focusing
-        this._isShowing = false;
-
-        // Clear styles in preparation for the next time we need to show the card
-        this.classList.remove(HOVER_CLASS);
-
-        // Reset and remove the hovercard from the DOM
-        this.style.cssText = '';
-        this.$['container'].setAttribute('tabindex', '-1');
-
-        // Remove the hovercard from the container, given that it is still a child
-        // of the container.
-        if (this.container?.contains(this)) {
-          this.container.removeChild(this);
-        }
-      };
-
-      /**
-       * Shows/opens the hovercard with a fixed delay.
-       */
-      readonly debounceShow = () => {
-        this.debounceShowBy(SHOW_DELAY_MS);
-      };
-
-      /**
-       * Shows/opens the hovercard with the given delay.
-       */
-      debounceShowBy(delayMs: number) {
-        this.cancelHideTask();
-        if (this._isShowing || this.isScheduledToShow) return;
-        this.isScheduledToShow = true;
-        this.showTask = debounce(
-          this.showTask,
-          () => {
-            // This happens when the mouse leaves the target before the delay is over.
-            if (!this.isScheduledToShow) return;
-            this.show();
-          },
-          delayMs
-        );
-      }
-
-      cancelShowTask() {
-        if (this.showTask) {
-          this.showTask.cancel();
-          this.isScheduledToShow = false;
-        }
-      }
-
-      /**
-       * Lock background scroll but enable scroll inside of current hovercard.
-       */
-      readonly lock = () => {
-        pushScrollLock(this);
-      };
-
-      /**
-       * Shows/opens the hovercard. This occurs when the user triggers the
-       * `mousenter` event on the hovercard's `target` element.
-       */
-      readonly show = () => {
-        this.cancelHideTask();
-        this.cancelShowTask();
-        if (this._isShowing || !this.container) {
-          return;
-        }
-
-        // Mark that the hovercard is now visible
-        this._isShowing = true;
-        this.setAttribute('tabindex', '0');
-
-        // Add it to the DOM and calculate its position
-        this.container.appendChild(this);
-        // We temporarily hide the hovercard until we have found the correct
-        // position for it.
-        this.classList.add(HIDE_CLASS);
-        this.classList.add(HOVER_CLASS);
-        // Make sure that the hovercard actually rendered and all dom-if
-        // statements processed, so that we can measure the (invisible)
-        // hovercard properly in updatePosition().
-        flush();
-        this.updatePosition();
-        this.classList.remove(HIDE_CLASS);
-      };
-
-      updatePosition() {
-        const positionsToTry = new Set([
-          this.position,
-          'right',
-          'bottom-right',
-          'top-right',
-          'bottom',
-          'top',
-          'bottom-left',
-          'top-left',
-          'left',
-        ]);
-        for (const position of positionsToTry) {
-          this.updatePositionTo(position);
-          if (this._isInsideViewport()) return;
-        }
-        console.warn('Could not find a visible position for the hovercard.');
-      }
-
-      _isInsideViewport() {
-        const thisRect = this.getBoundingClientRect();
-        if (thisRect.top < 0) return false;
-        if (thisRect.left < 0) return false;
-        const docuRect = document.documentElement.getBoundingClientRect();
-        if (thisRect.bottom > docuRect.height) return false;
-        if (thisRect.right > docuRect.width) return false;
-        return true;
-      }
-
-      /**
-       * Updates the hovercard's position based the current position of the `target`
-       * element.
-       *
-       * The hovercard is supposed to stay open if the user hovers over it.
-       * To keep it open when the user moves away from the target, the bounding
-       * rects of the target and hovercard must touch or overlap.
-       *
-       * NOTE: You do not need to directly call this method unless you need to
-       * update the position of the tooltip while it is already visible (the
-       * target element has moved and the tooltip is still open).
-       */
-      updatePositionTo(position: string) {
-        if (!this._target) {
-          return;
-        }
-
-        // Make sure that thisRect will not get any paddings and such included
-        // in the width and height of the bounding client rect.
-        this.style.cssText = '';
-
-        const docuRect = document.documentElement.getBoundingClientRect();
-        const targetRect = this._target.getBoundingClientRect();
-        const thisRect = this.getBoundingClientRect();
-
-        const targetLeft = targetRect.left - docuRect.left;
-        const targetTop = targetRect.top - docuRect.top;
-
-        let hovercardLeft;
-        let hovercardTop;
-
-        switch (position) {
-          case 'top':
-            hovercardLeft =
-              targetLeft + (targetRect.width - thisRect.width) / 2;
-            hovercardTop = targetTop - thisRect.height - this.offset;
-            break;
-          case 'bottom':
-            hovercardLeft =
-              targetLeft + (targetRect.width - thisRect.width) / 2;
-            hovercardTop = targetTop + targetRect.height + this.offset;
-            break;
-          case 'left':
-            hovercardLeft = targetLeft - thisRect.width - this.offset;
-            hovercardTop =
-              targetTop + (targetRect.height - thisRect.height) / 2;
-            break;
-          case 'right':
-            hovercardLeft = targetLeft + targetRect.width + this.offset;
-            hovercardTop =
-              targetTop + (targetRect.height - thisRect.height) / 2;
-            break;
-          case 'bottom-right':
-            hovercardLeft = targetLeft + targetRect.width + this.offset;
-            hovercardTop = targetTop;
-            break;
-          case 'bottom-left':
-            hovercardLeft = targetLeft - thisRect.width - this.offset;
-            hovercardTop = targetTop;
-            break;
-          case 'top-left':
-            hovercardLeft = targetLeft - thisRect.width - this.offset;
-            hovercardTop = targetTop + targetRect.height - thisRect.height;
-            break;
-          case 'top-right':
-            hovercardLeft = targetLeft + targetRect.width + this.offset;
-            hovercardTop = targetTop + targetRect.height - thisRect.height;
-            break;
-        }
-
-        this.style.left = `${hovercardLeft}px`;
-        this.style.top = `${hovercardTop}px`;
-      }
-
-      /**
-       * Responds to a change in the `for` value and gets the updated `target`
-       * element for the hovercard.
-       */
-      @observe('for')
-      _forChanged() {
+    override connectedCallback() {
+      super.connectedCallback();
+      if (!this._target) {
         this._target = this.target;
       }
+      this._target.addEventListener('mouseenter', this.debounceShow);
+      this._target.addEventListener('focus', this.debounceShow);
+      this._target.addEventListener('mouseleave', this.debounceHide);
+      this._target.addEventListener('blur', this.debounceHide);
+
+      // when click, dismiss immediately
+      this._target.addEventListener('click', this.hide);
+
+      // show the hovercard if mouse moves to hovercard
+      // this will cancel pending hide as well
+      this.addEventListener('mouseenter', this.show);
+      this.addEventListener('mouseenter', this.lock);
+      // when leave hovercard, hide it immediately
+      this.addEventListener('mouseleave', this.hide);
+      this.addEventListener('mouseleave', this.unlock);
+    }
+
+    override disconnectedCallback() {
+      this.cancelShowTask();
+      this.cancelHideTask();
+      this.unlock();
+      super.disconnectedCallback();
+    }
+
+    override ready() {
+      super.ready();
+      // First, check to see if the container has already been created.
+      this.container = getHovercardContainer({createIfNotExists: true});
+    }
+
+    removeListeners() {
+      this._target?.removeEventListener('mouseenter', this.debounceShow);
+      this._target?.removeEventListener('focus', this.debounceShow);
+      this._target?.removeEventListener('mouseleave', this.debounceHide);
+      this._target?.removeEventListener('blur', this.debounceHide);
+      this._target?.removeEventListener('click', this.hide);
+    }
+
+    readonly debounceHide = () => {
+      this.cancelShowTask();
+      if (!this._isShowing || this.isScheduledToHide) return;
+      this.isScheduledToHide = true;
+      this.hideTask = debounce(
+        this.hideTask,
+        () => {
+          // This happens when hide immediately through click or mouse leave
+          // on the hovercard
+          if (!this.isScheduledToHide) return;
+          this.hide();
+        },
+        HIDE_DELAY_MS
+      );
+    };
+
+    cancelHideTask() {
+      if (this.hideTask) {
+        this.hideTask.cancel();
+        this.isScheduledToHide = false;
+      }
     }
 
-    return Mixin;
+    /**
+     * Hovercard elements are created outside of <gr-app>, so if you want to fire
+     * events, then you probably want to do that through the target element.
+     */
+
+    dispatchEventThroughTarget(eventName: string): void;
+
+    dispatchEventThroughTarget(
+      eventName: 'show-alert',
+      detail: ShowAlertEventDetail
+    ): void;
+
+    dispatchEventThroughTarget(
+      eventName: 'reload',
+      detail: ReloadEventDetail
+    ): void;
+
+    dispatchEventThroughTarget(eventName: string, detail?: unknown) {
+      if (!detail) detail = {};
+      if (this._target)
+        this._target.dispatchEvent(
+          new CustomEvent(eventName, {
+            detail,
+            bubbles: true,
+            composed: true,
+          })
+        );
+    }
+
+    /**
+     * Returns the target element that the hovercard is anchored to (the `id` of
+     * the `for` property).
+     */
+    get target(): HTMLElement {
+      const parentNode = this.parentNode;
+      // If the parentNode is a document fragment, then we need to use the host.
+      const ownerRoot = this.getRootNode() as ShadowRoot;
+      let target;
+      if (this.for) {
+        target = ownerRoot.querySelector('#' + this.for);
+      } else {
+        target =
+          !parentNode || parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
+            ? ownerRoot.host
+            : parentNode;
+      }
+      return target as HTMLElement;
+    }
+
+    /**
+     * unlock scroll, this will resume the scroll outside of the hovercard.
+     */
+    readonly unlock = () => {
+      removeScrollLock(this);
+    };
+
+    /**
+     * Hides/closes the hovercard. This occurs when the user triggers the
+     * `mouseleave` event on the hovercard's `target` element (as long as the
+     * user is not hovering over the hovercard).
+     *
+     */
+    readonly hide = (e?: MouseEvent) => {
+      this.cancelHideTask();
+      this.cancelShowTask();
+      if (!this._isShowing) {
+        return;
+      }
+
+      // If the user is now hovering over the hovercard or the user is returning
+      // from the hovercard but now hovering over the target (to stop an annoying
+      // flicker effect), just return.
+      if (e) {
+        if (
+          e.relatedTarget === this ||
+          (e.target === this && e.relatedTarget === this._target)
+        ) {
+          return;
+        }
+      }
+
+      // Mark that the hovercard is not visible and do not allow focusing
+      this._isShowing = false;
+
+      // Clear styles in preparation for the next time we need to show the card
+      this.classList.remove(HOVER_CLASS);
+
+      // Reset and remove the hovercard from the DOM
+      this.style.cssText = '';
+      this.$['container'].setAttribute('tabindex', '-1');
+
+      // Remove the hovercard from the container, given that it is still a child
+      // of the container.
+      if (this.container?.contains(this)) {
+        this.container.removeChild(this);
+      }
+    };
+
+    /**
+     * Shows/opens the hovercard with a fixed delay.
+     */
+    readonly debounceShow = () => {
+      this.debounceShowBy(SHOW_DELAY_MS);
+    };
+
+    /**
+     * Shows/opens the hovercard with the given delay.
+     */
+    debounceShowBy(delayMs: number) {
+      this.cancelHideTask();
+      if (this._isShowing || this.isScheduledToShow) return;
+      this.isScheduledToShow = true;
+      this.showTask = debounce(
+        this.showTask,
+        () => {
+          // This happens when the mouse leaves the target before the delay is over.
+          if (!this.isScheduledToShow) return;
+          this.show();
+        },
+        delayMs
+      );
+    }
+
+    cancelShowTask() {
+      if (this.showTask) {
+        this.showTask.cancel();
+        this.isScheduledToShow = false;
+      }
+    }
+
+    /**
+     * Lock background scroll but enable scroll inside of current hovercard.
+     */
+    readonly lock = () => {
+      pushScrollLock(this);
+    };
+
+    /**
+     * Shows/opens the hovercard. This occurs when the user triggers the
+     * `mousenter` event on the hovercard's `target` element.
+     */
+    readonly show = () => {
+      this.cancelHideTask();
+      this.cancelShowTask();
+      if (this._isShowing || !this.container) {
+        return;
+      }
+
+      // Mark that the hovercard is now visible
+      this._isShowing = true;
+      this.setAttribute('tabindex', '0');
+
+      // Add it to the DOM and calculate its position
+      this.container.appendChild(this);
+      // We temporarily hide the hovercard until we have found the correct
+      // position for it.
+      this.classList.add(HIDE_CLASS);
+      this.classList.add(HOVER_CLASS);
+      // Make sure that the hovercard actually rendered and all dom-if
+      // statements processed, so that we can measure the (invisible)
+      // hovercard properly in updatePosition().
+      flush();
+      this.updatePosition();
+      this.classList.remove(HIDE_CLASS);
+    };
+
+    updatePosition() {
+      const positionsToTry = new Set([
+        this.position,
+        'right',
+        'bottom-right',
+        'top-right',
+        'bottom',
+        'top',
+        'bottom-left',
+        'top-left',
+        'left',
+      ]);
+      for (const position of positionsToTry) {
+        this.updatePositionTo(position);
+        if (this._isInsideViewport()) return;
+      }
+      console.warn('Could not find a visible position for the hovercard.');
+    }
+
+    _isInsideViewport() {
+      const thisRect = this.getBoundingClientRect();
+      if (thisRect.top < 0) return false;
+      if (thisRect.left < 0) return false;
+      const docuRect = document.documentElement.getBoundingClientRect();
+      if (thisRect.bottom > docuRect.height) return false;
+      if (thisRect.right > docuRect.width) return false;
+      return true;
+    }
+
+    /**
+     * Updates the hovercard's position based the current position of the `target`
+     * element.
+     *
+     * The hovercard is supposed to stay open if the user hovers over it.
+     * To keep it open when the user moves away from the target, the bounding
+     * rects of the target and hovercard must touch or overlap.
+     *
+     * NOTE: You do not need to directly call this method unless you need to
+     * update the position of the tooltip while it is already visible (the
+     * target element has moved and the tooltip is still open).
+     */
+    updatePositionTo(position: string) {
+      if (!this._target) {
+        return;
+      }
+
+      // Make sure that thisRect will not get any paddings and such included
+      // in the width and height of the bounding client rect.
+      this.style.cssText = '';
+
+      const docuRect = document.documentElement.getBoundingClientRect();
+      const targetRect = this._target.getBoundingClientRect();
+      const thisRect = this.getBoundingClientRect();
+
+      const targetLeft = targetRect.left - docuRect.left;
+      const targetTop = targetRect.top - docuRect.top;
+
+      let hovercardLeft;
+      let hovercardTop;
+
+      switch (position) {
+        case 'top':
+          hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+          hovercardTop = targetTop - thisRect.height - this.offset;
+          break;
+        case 'bottom':
+          hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
+          hovercardTop = targetTop + targetRect.height + this.offset;
+          break;
+        case 'left':
+          hovercardLeft = targetLeft - thisRect.width - this.offset;
+          hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+          break;
+        case 'right':
+          hovercardLeft = targetLeft + targetRect.width + this.offset;
+          hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
+          break;
+        case 'bottom-right':
+          hovercardLeft = targetLeft + targetRect.width + this.offset;
+          hovercardTop = targetTop;
+          break;
+        case 'bottom-left':
+          hovercardLeft = targetLeft - thisRect.width - this.offset;
+          hovercardTop = targetTop;
+          break;
+        case 'top-left':
+          hovercardLeft = targetLeft - thisRect.width - this.offset;
+          hovercardTop = targetTop + targetRect.height - thisRect.height;
+          break;
+        case 'top-right':
+          hovercardLeft = targetLeft + targetRect.width + this.offset;
+          hovercardTop = targetTop + targetRect.height - thisRect.height;
+          break;
+      }
+
+      this.style.left = `${hovercardLeft}px`;
+      this.style.top = `${hovercardTop}px`;
+    }
+
+    /**
+     * Responds to a change in the `for` value and gets the updated `target`
+     * element for the hovercard.
+     */
+    @observe('for')
+    _forChanged() {
+      this._target = this.target;
+    }
   }
-);
+
+  return Mixin as T & Constructor<GrHovercardBehaviorInterface>;
+};
 
 export interface GrHovercardBehaviorInterface {
+  _target: HTMLElement | null;
   ready(): void;
   removeListeners(): void;
   debounceHide(): void;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
index 8857d36..acc5e15 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.ts
@@ -18,12 +18,15 @@
 import '../../../styles/shared-styles';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-hovercard_html';
-import {hovercardBehaviorMixin} from './gr-hovercard-behavior';
+import {HovercardBehaviorMixin} from './gr-hovercard-behavior';
 import './gr-hovercard-shared-style';
 import {customElement} from '@polymer/decorators';
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardBehaviorMixin(PolymerElement);
+
 @customElement('gr-hovercard')
-export class GrHovercard extends hovercardBehaviorMixin(PolymerElement) {
+export class GrHovercard extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index a3d9e58..da1a782 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -44,6 +44,8 @@
       <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></g>
       <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
+      <g id="more-horiz"><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g>
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g>
       <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js -->
       <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g>
@@ -154,6 +156,10 @@
       <g id="file-present"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V7l-5-5zM6 20V4h8v4h4v12H6zm10-10v5c0 2.21-1.79 4-4 4s-4-1.79-4-4V8.5c0-1.47 1.26-2.64 2.76-2.49 1.3.13 2.24 1.32 2.24 2.63V15h-2V8.5c0-.28-.22-.5-.5-.5s-.5.22-.5.5V15c0 1.1.9 2 2 2s2-.9 2-2v-5h2z"/></g>
       <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material%20Icons%3Aarrow_forward-->
       <g id="arrow-forward"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></g>
+      <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons:feedback -->
+      <g id="feedback"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/></g>
+      <!-- This SVG is a copy from material.io https://fonts.google.com/icons?selected=Material+Icons&icon.query=description -->
+      <g id="description"><path xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0V0z" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></g>
     </defs>
   </svg>
 </iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.ts
similarity index 70%
rename from polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.js
rename to polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.ts
index e45cb0c..865aa20 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.ts
@@ -15,9 +15,9 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-js-api-interface.js';
-import {getPluginNameFromUrl} from './gr-api-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-js-api-interface';
+import {getPluginNameFromUrl} from './gr-api-utils';
 
 suite('gr-api-utils tests', () => {
   suite('test getPluginNameFromUrl', () => {
@@ -32,37 +32,36 @@
     test('with random invalid url', () => {
       assert.equal(getPluginNameFromUrl('http://example.com'), null);
       assert.equal(
-          getPluginNameFromUrl('http://example.com/static/a.js'),
-          null
+        getPluginNameFromUrl('http://example.com/static/a.js'),
+        null
       );
     });
 
     test('with valid urls', () => {
       assert.equal(
-          getPluginNameFromUrl('http://example.com/plugins/a.js'),
-          'a'
+        getPluginNameFromUrl('http://example.com/plugins/a.js'),
+        'a'
       );
       assert.equal(
-          getPluginNameFromUrl('http://example.com/plugins/a/static/t.js'),
-          'a'
+        getPluginNameFromUrl('http://example.com/plugins/a/static/t.js'),
+        'a'
       );
     });
 
     test('with gerrit-theme override', () => {
       assert.equal(
-          getPluginNameFromUrl('http://example.com/static/gerrit-theme.js'),
-          'gerrit-theme'
+        getPluginNameFromUrl('http://example.com/static/gerrit-theme.js'),
+        'gerrit-theme'
       );
     });
 
     test('with ASSETS_PATH', () => {
       window.ASSETS_PATH = 'http://cdn.com/2';
       assert.equal(
-          getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.js`),
-          'a'
+        getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.js`),
+        'a'
       );
       window.ASSETS_PATH = undefined;
     });
   });
 });
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
index 466e8e4..a33b145 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.ts
@@ -90,7 +90,7 @@
    * Ensure GrChangeActionsInterface instance has access to gr-change-actions
    * element and retrieve if the interface was created before element.
    */
-  private ensureEl(): GrChangeActionsElement {
+  ensureEl(): GrChangeActionsElement {
     if (!this.el) {
       const sharedApiElement = appContext.jsApiService;
       this.setEl(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 203784d..87f6052 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -125,7 +125,7 @@
           .querySelector('[data-action-key="' + key + '"]'));
     });
 
-    test('action button properties', () => {
+    test('action button properties', async () => {
       const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
       flush();
       const button = element.shadowRoot
@@ -137,17 +137,17 @@
       changeActions.setTitle(key, 'Yo hint');
       changeActions.setEnabled(key, false);
       changeActions.setIcon(key, 'pupper');
-      flush();
+      await flush();
       assert.equal(button.getAttribute('data-label'), 'Yo');
-      assert.equal(button.getAttribute('title'), 'Yo hint');
+      assert.equal(button.parentElement.getAttribute('title'), 'Yo hint');
       assert.isTrue(button.disabled);
       assert.equal(button.querySelector('iron-icon').icon,
           'gr-icons:pupper');
     });
 
-    test('hide action buttons', () => {
+    test('hide action buttons', async () => {
       const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
-      flush();
+      await flush();
       let button = element.shadowRoot
           .querySelector('[data-action-key="' + key + '"]');
       assert.isOk(button);
@@ -168,7 +168,7 @@
           .querySelector('[data-action-key="' + key + '"]'));
       changeActions.setActionOverflow(
           changeActions.ActionType.REVISION, key, true);
-      flush();
+      await flush();
       assert.isNotOk(element.shadowRoot
           .querySelector('[data-action-key="' + key + '"]'));
       assert.isFalse(element.$.moreActions.hidden);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index 3ee393c..1d4bd06 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -31,8 +31,10 @@
 } from '../../../services/gr-event-interface/gr-event-interface';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {Gerrit} from '../../../api/gerrit';
+import {fontStyles} from '../../../styles/gr-font-styles';
 import {formStyles} from '../../../styles/gr-form-styles';
 import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
+import {spinnerStyles} from '../../../styles/gr-spinner-styles';
 import {subpageStyles} from '../../../styles/gr-subpage-styles';
 import {tableStyles} from '../../../styles/gr-table-styles';
 
@@ -121,8 +123,10 @@
   public readonly Auth = appContext.authService;
 
   public readonly styles = {
+    font: fontStyles,
     form: formStyles,
     menuPage: menuPageStyles,
+    spinner: spinnerStyles,
     subPage: subpageStyles,
     table: tableStyles,
   };
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 8ec2607..29db685 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -23,7 +23,6 @@
 import {getPluginLoader} from './gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {stubBaseUrl} from '../../../test/test-utils.js';
-import sinon from 'sinon/pkg/sinon-esm';
 import {stubRestApi} from '../../../test/test-utils.js';
 import {appContext} from '../../../services/app-context.js';
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
index f7475bf..b039a7e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
@@ -104,7 +104,7 @@
    * which endpoints to dynamically add to the page.
    */
   registerModule(plugin: PluginApi, opts: Options) {
-    const endpoint = opts.endpoint!;
+    const endpoint = opts.endpoint;
     const dynamicEndpoint = opts.dynamicEndpoint;
     if (dynamicEndpoint) {
       if (!this._dynamicPlugins.has(dynamicEndpoint)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
deleted file mode 100644
index e3475ad..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import {resetPlugins} from '../../../test/test-utils.js';
-import './gr-js-api-interface.js';
-import {GrPluginEndpoints} from './gr-plugin-endpoints.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-plugin-endpoints tests', () => {
-  let instance;
-  let pluginFoo;
-  let pluginBar;
-  let domHook;
-
-  setup(() => {
-    domHook = {};
-    instance = new GrPluginEndpoints();
-    pluginApi.install(p => { pluginFoo = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/foo.js');
-    instance.registerModule(
-        pluginFoo,
-        {
-          endpoint: 'a-place',
-          type: 'decorate',
-          moduleName: 'foo-module',
-          domHook,
-        }
-    );
-    pluginApi.install(p => { pluginBar = p; }, '0.1',
-        'http://test.com/plugins/testplugin/static/bar.js');
-    instance.registerModule(
-        pluginBar,
-        {
-          endpoint: 'a-place',
-          type: 'style',
-          moduleName: 'bar-module',
-          domHook,
-        }
-    );
-  });
-
-  teardown(() => {
-    resetPlugins();
-  });
-
-  test('getDetails all', () => {
-    assert.deepEqual(instance.getDetails('a-place'), [
-      {
-        moduleName: 'foo-module',
-        plugin: pluginFoo,
-        pluginUrl: pluginFoo._url,
-        type: 'decorate',
-        domHook,
-        slot: undefined,
-      },
-      {
-        moduleName: 'bar-module',
-        plugin: pluginBar,
-        pluginUrl: pluginBar._url,
-        type: 'style',
-        domHook,
-        slot: undefined,
-      },
-    ]);
-  });
-
-  test('getDetails by type', () => {
-    assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [
-      {
-        moduleName: 'bar-module',
-        plugin: pluginBar,
-        pluginUrl: pluginBar._url,
-        type: 'style',
-        domHook,
-        slot: undefined,
-      },
-    ]);
-  });
-
-  test('getDetails by module', () => {
-    assert.deepEqual(
-        instance.getDetails('a-place', {moduleName: 'foo-module'}),
-        [
-          {
-            moduleName: 'foo-module',
-            plugin: pluginFoo,
-            pluginUrl: pluginFoo._url,
-            type: 'decorate',
-            domHook,
-            slot: undefined,
-          },
-        ]);
-  });
-
-  test('getModules', () => {
-    assert.deepEqual(
-        instance.getModules('a-place'), ['foo-module', 'bar-module']);
-  });
-
-  test('getPlugins', () => {
-    assert.deepEqual(
-        instance.getPlugins('a-place'), [pluginFoo._url]);
-  });
-
-  test('onNewEndpoint', () => {
-    const newModuleStub = sinon.stub();
-    instance.setPluginsReady();
-    instance.onNewEndpoint('a-place', newModuleStub);
-    instance.registerModule(
-        pluginFoo,
-        {
-          endpoint: 'a-place',
-          type: 'replace',
-          moduleName: 'zaz-module',
-          domHook,
-        });
-    assert.deepEqual(newModuleStub.lastCall.args[0], {
-      moduleName: 'zaz-module',
-      plugin: pluginFoo,
-      pluginUrl: pluginFoo._url,
-      type: 'replace',
-      domHook,
-      slot: undefined,
-    });
-  });
-
-  test('reuse dom hooks', () => {
-    instance.registerModule(
-        pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
-    assert.deepEqual(instance.getDetails('a-place'), [
-      {
-        moduleName: 'foo-module',
-        plugin: pluginFoo,
-        pluginUrl: pluginFoo._url,
-        type: 'decorate',
-        domHook,
-        slot: undefined,
-      },
-      {
-        moduleName: 'bar-module',
-        plugin: pluginBar,
-        pluginUrl: pluginBar._url,
-        type: 'style',
-        domHook,
-        slot: undefined,
-      },
-    ]);
-  });
-});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
new file mode 100644
index 0000000..c7bdfb4
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
@@ -0,0 +1,177 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../test/common-test-setup-karma';
+import {resetPlugins} from '../../../test/test-utils';
+import './gr-js-api-interface';
+import {GrPluginEndpoints} from './gr-plugin-endpoints';
+import {_testOnly_initGerritPluginApi} from './gr-gerrit';
+import {PluginApi} from '../../../api/plugin';
+import {HookApi, HookCallback, PluginElement} from '../../../api/hook';
+
+const pluginApi = _testOnly_initGerritPluginApi();
+
+export class MockHook<T extends PluginElement> implements HookApi<T> {
+  handleInstanceDetached(_: T) {}
+
+  handleInstanceAttached(_: T) {}
+
+  getLastAttached(): Promise<HTMLElement> {
+    throw new Error('unimplemented in mock');
+  }
+
+  getAllAttached() {
+    return [];
+  }
+
+  onAttached(_: HookCallback<T>) {
+    return this;
+  }
+
+  onDetached(_: HookCallback<T>) {
+    return this;
+  }
+
+  getModuleName() {
+    return 'MockHookApi-ModuleName';
+  }
+}
+
+suite('gr-plugin-endpoints tests', () => {
+  let instance: GrPluginEndpoints;
+  let decoratePlugin: PluginApi;
+  let stylePlugin: PluginApi;
+  let domHook: HookApi<PluginElement>;
+
+  setup(() => {
+    domHook = new MockHook<PluginElement>();
+    instance = new GrPluginEndpoints();
+    pluginApi.install(
+      plugin => (decoratePlugin = plugin),
+      '0.1',
+      'http://test.com/plugins/testplugin/static/decorate.js'
+    );
+    instance.registerModule(decoratePlugin, {
+      endpoint: 'my-endpoint',
+      type: 'decorate',
+      moduleName: 'decorate-module',
+      domHook,
+    });
+    pluginApi.install(
+      plugin => (stylePlugin = plugin),
+      '0.1',
+      'http://test.com/plugins/testplugin/static/style.js'
+    );
+    instance.registerModule(stylePlugin, {
+      endpoint: 'my-endpoint',
+      type: 'style',
+      moduleName: 'style-module',
+      domHook,
+    });
+  });
+
+  teardown(() => {
+    resetPlugins();
+  });
+
+  test('getDetails all', () => {
+    assert.deepEqual(instance.getDetails('my-endpoint'), [
+      {
+        moduleName: 'decorate-module',
+        plugin: decoratePlugin,
+        pluginUrl: decoratePlugin._url,
+        type: 'decorate',
+        domHook,
+        slot: undefined,
+      },
+      {
+        moduleName: 'style-module',
+        plugin: stylePlugin,
+        pluginUrl: stylePlugin._url,
+        type: 'style',
+        domHook,
+        slot: undefined,
+      },
+    ]);
+  });
+
+  test('getDetails by type', () => {
+    assert.deepEqual(
+      instance.getDetails('my-endpoint', {endpoint: 'a-place', type: 'style'}),
+      [
+        {
+          moduleName: 'style-module',
+          plugin: stylePlugin,
+          pluginUrl: stylePlugin._url,
+          type: 'style',
+          domHook,
+          slot: undefined,
+        },
+      ]
+    );
+  });
+
+  test('getDetails by module', () => {
+    assert.deepEqual(
+      instance.getDetails('my-endpoint', {
+        endpoint: 'my-endpoint',
+        moduleName: 'decorate-module',
+      }),
+      [
+        {
+          moduleName: 'decorate-module',
+          plugin: decoratePlugin,
+          pluginUrl: decoratePlugin._url,
+          type: 'decorate',
+          domHook,
+          slot: undefined,
+        },
+      ]
+    );
+  });
+
+  test('getModules', () => {
+    assert.deepEqual(instance.getModules('my-endpoint'), [
+      'decorate-module',
+      'style-module',
+    ]);
+  });
+
+  test('getPlugins URLs are unique', () => {
+    assert.equal(decoratePlugin._url, stylePlugin._url);
+    assert.deepEqual(instance.getPlugins('my-endpoint'), [decoratePlugin._url]);
+  });
+
+  test('onNewEndpoint', () => {
+    const newModuleStub = sinon.stub();
+    instance.setPluginsReady();
+    instance.onNewEndpoint('my-endpoint', newModuleStub);
+    instance.registerModule(decoratePlugin, {
+      endpoint: 'my-endpoint',
+      type: 'replace',
+      moduleName: 'replace-module',
+      domHook,
+    });
+    assert.deepEqual(newModuleStub.lastCall.args[0], {
+      moduleName: 'replace-module',
+      plugin: decoratePlugin,
+      pluginUrl: decoratePlugin._url,
+      type: 'replace',
+      domHook,
+      slot: undefined,
+    });
+  });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
index 6fc8f1b..7c99480 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.ts
@@ -123,7 +123,10 @@
     });
 
     this.awaitPluginsLoaded().then(() => {
-      this._getReporting().pluginsLoaded(this._getAllInstalledPluginNames());
+      const loaded = this.getPluginsByState(PluginState.LOADED);
+      const failed = this.getPluginsByState(PluginState.LOAD_FAILED);
+      this._getReporting().pluginsLoaded(loaded.map(p => p.name));
+      this._getReporting().pluginsFailed(failed.map(p => p.name));
     });
   }
 
@@ -140,14 +143,8 @@
     return url.pathname && url.pathname.endsWith(suffix);
   }
 
-  _getAllInstalledPluginNames() {
-    const installedPlugins = [];
-    for (const plugin of this._plugins.values()) {
-      if (plugin.state === PluginState.LOADED) {
-        installedPlugins.push(plugin.name);
-      }
-    }
-    return installedPlugins;
+  private getPluginsByState(state: PluginState) {
+    return [...this._plugins.values()].filter(p => p.state === state);
   }
 
   install(
@@ -190,16 +187,9 @@
     }
   }
 
-  // The polygerrit uses version of sinon where you can't stub getter,
-  // declare it as a function here
   arePluginsLoaded() {
-    // As the size of plugins is relatively small,
-    // so the performance of this check should be reasonable
     if (!this._pluginListLoaded) return false;
-    for (const plugin of this._plugins.values()) {
-      if (plugin.state === PluginState.PENDING) return false;
-    }
-    return true;
+    return this.getPluginsByState(PluginState.PENDING).length === 0;
   }
 
   _checkIfCompleted() {
@@ -214,15 +204,14 @@
   }
 
   _timeout() {
-    const pendingPlugins = [];
-    for (const plugin of this._plugins.values()) {
-      if (plugin.state === PluginState.PENDING) {
-        this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
-        this._checkIfCompleted();
-        pendingPlugins.push(plugin.url);
-      }
+    const pending = this.getPluginsByState(PluginState.PENDING);
+    for (const plugin of pending) {
+      this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
     }
-    return `Timeout when loading plugins: ${pendingPlugins.join(',')}`;
+    this._checkIfCompleted();
+    return `Timeout when loading plugins: ${pending
+      .map(p => p.name)
+      .join(',')}`;
   }
 
   _failToLoad(message: string, pluginUrl?: string) {
@@ -252,6 +241,7 @@
         plugin: null,
       });
     }
+    console.info(`Plugin ${key} ${state}`);
     return this._plugins.get(key)!;
   }
 
@@ -259,7 +249,6 @@
     const pluginObj = this._updatePluginState(url, PluginState.LOADED);
     pluginObj.plugin = plugin;
     this._getReporting().pluginLoaded(plugin.getPluginName() || url);
-    console.info(`Plugin ${plugin.getPluginName() || url} installed.`);
     this._checkIfCompleted();
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
index 903fb2c..2b6db21 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
@@ -68,11 +68,6 @@
     return this.restApi.getAccount();
   }
 
-  getAccountCapabilities(capabilities: string[]) {
-    this.reporting.trackApi(this.plugin, 'rest', 'getAccountCapabilities');
-    return this.restApi.getAccountCapabilities(capabilities);
-  }
-
   getRepos(filter: string, reposPerPage: number, offset?: number) {
     this.reporting.trackApi(this.plugin, 'rest', 'getRepos');
     return this.restApi.getRepos(filter, reposPerPage, offset);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index 31a709a..dba36a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/gr-font-styles';
 import '../../../styles/gr-voting-styles';
 import '../../../styles/shared-styles';
 import '../gr-account-label/gr-account-label';
@@ -21,6 +22,7 @@
 import '../gr-button/gr-button';
 import '../gr-icons/gr-icons';
 import '../gr-label/gr-label';
+import '../gr-tooltip-content/gr-tooltip-content';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-label-info_html';
@@ -38,6 +40,7 @@
 import {GrButton} from '../gr-button/gr-button';
 import {getVotingRangeOrDefault} from '../../../utils/label-util';
 import {appContext} from '../../../services/app-context';
+import {ParsedChangeInfo} from '../../../types/types';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -71,7 +74,7 @@
   label = '';
 
   @property({type: Object})
-  change?: ChangeInfo;
+  change?: ParsedChangeInfo;
 
   @property({type: Object})
   account?: AccountInfo;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index b6583d9..552bd08 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
@@ -17,6 +17,9 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
+  <style include="gr-font-styles">
+    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+  </style>
   <style include="gr-voting-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
@@ -27,13 +30,13 @@
     .hidden {
       display: none;
     }
+    /* Note that most of the .voteChip styles are coming from the
+       gr-voting-styles include. */
     .voteChip {
       display: flex;
       justify-content: center;
       margin-right: var(--spacing-s);
       padding: 1px;
-      @apply --vote-chip-styles;
-      border: 1px solid var(--border-color);
     }
     .max {
       background-color: var(--vote-color-approved);
@@ -98,13 +101,14 @@
     >
       <tr class="labelValueContainer">
         <td>
-          <gr-label
-            has-tooltip=""
+          <gr-tooltip-content
+            has-tooltip
             title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]"
-            class$="[[mappedLabel.className]] voteChip font-small"
           >
-            [[mappedLabel.value]]
-          </gr-label>
+            <gr-label class$="[[mappedLabel.className]] voteChip font-small">
+              [[mappedLabel.value]]
+            </gr-label>
+          </gr-tooltip-content>
         </td>
         <td>
           <gr-account-link
@@ -113,16 +117,17 @@
           ></gr-account-link>
         </td>
         <td>
-          <gr-button
-            link=""
-            aria-label="Remove vote"
-            on-click="_onDeleteVote"
-            tooltip="Remove vote"
-            data-account-id$="[[mappedLabel.account._account_id]]"
-            class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"
-          >
-            <iron-icon icon="gr-icons:delete"></iron-icon>
-          </gr-button>
+          <gr-tooltip-content has-tooltip title="Remove vote">
+            <gr-button
+              link=""
+              aria-label="Remove vote"
+              on-click="_onDeleteVote"
+              data-account-id$="[[mappedLabel.account._account_id]]"
+              class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"
+            >
+              <iron-icon icon="gr-icons:delete"></iron-icon>
+            </gr-button>
+          </gr-tooltip-content>
         </td>
       </tr>
     </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index b3235fa..b1bd6fa 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -31,7 +31,7 @@
 import {GrAccountLink} from '../gr-account-link/gr-account-link';
 import {
   createAccountWithIdNameAndEmail,
-  createChange,
+  createParsedChange,
 } from '../../../test/test-data-generators';
 import {LabelInfo} from '../../../types/common';
 
@@ -46,7 +46,7 @@
 
     // Needed to trigger computed bindings.
     element.account = {};
-    element.change = {...createChange(), labels: {}};
+    element.change = {...createParsedChange(), labels: {}};
   });
 
   suite('remove reviewer votes', () => {
@@ -60,7 +60,7 @@
       sinon.stub(element, '_computeValueTooltip').returns('');
       element.account = account;
       element.change = {
-        ...createChange(),
+        ...createParsedChange(),
         labels: {'Code-Review': label},
       };
       element.labelInfo = label;
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
index c13ca6e..842b35e 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
@@ -21,10 +21,8 @@
  * used in gr-label-info.
  */
 
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement} from '@polymer/decorators';
-import {htmlTemplate} from './gr-label_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -33,8 +31,12 @@
 }
 
 @customElement('gr-label')
-export class GrLabel extends TooltipMixin(PolymerElement) {
-  static get template() {
-    return htmlTemplate;
+export class GrLabel extends LitElement {
+  static override get styles() {
+    return [];
+  }
+
+  override render() {
+    return html` <slot></slot> `;
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
index 4a2ba86..7008db2 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
@@ -14,10 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-limited-text_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {customElement, observe, property} from '@polymer/decorators';
+import {customElement, property} from 'lit/decorators';
+import {html, LitElement} from 'lit';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -32,57 +30,56 @@
  * and a tooltip containing the full text is enabled.
  */
 @customElement('gr-limited-text')
-export class GrLimitedText extends TooltipMixin(PolymerElement) {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrLimitedText extends LitElement {
   /** The un-truncated text to display. */
   @property({type: String})
-  text?: string;
+  text = '';
 
   /** The maximum length for the text to display before truncating. */
   @property({type: Number})
-  limit?: number;
+  limit = 0;
 
   @property({type: String})
   tooltip?: string;
 
-  /** Boolean property used by TooltipMixin. */
-  @property({type: Boolean})
-  override hasTooltip = false;
+  static override get styles() {
+    return [];
+  }
 
-  /** Boolean property used by TooltipMixin. */
-  @property({type: Boolean})
-  disableTooltip = false;
-
-  /**
-   * The text or limit have changed. Recompute whether a tooltip needs to be
-   * enabled.
-   */
-  @observe('text', 'tooltip', 'limit')
-  _updateTitle(text?: string, tooltip?: string, limit?: number) {
-    text = text ?? '';
-    tooltip = tooltip ?? '';
-    limit = limit ?? 0;
-
-    this.hasTooltip = !!tooltip || (!!limit && text.length > limit);
-    if (this.hasTooltip && !this.disableTooltip) {
-      // Combine the text and title if over-length
-      if (limit && text.length > limit) {
-        this.title = `${text}${tooltip ? ` (${tooltip})` : ''}`;
-      } else {
-        this.title = tooltip;
-      }
+  override render() {
+    if (this.tooltip || this.tooLong()) {
+      return html` <gr-tooltip-content
+        has-tooltip
+        .title=${this.renderTooltip()}
+      >
+        ${this.renderText()}
+      </gr-tooltip-content>`;
     } else {
-      this.title = '';
+      return this.renderText();
     }
   }
 
-  _computeDisplayText(text?: string, limit?: number) {
-    if (!!limit && !!text && text.length > limit) {
-      return text.substr(0, limit - 1) + '…';
+  // Should be private but used in tests.
+  renderText() {
+    if (this.tooLong()) {
+      return this.text.substr(0, this.limit - 1) + '…';
     }
-    return text;
+    return this.text;
+  }
+
+  private renderTooltip() {
+    if (this.tooLong()) {
+      return `${this.text}${this.tooltip ? ` (${this.tooltip})` : ''}`;
+    } else if (this.tooltip) {
+      return this.tooltip;
+    } else {
+      return '';
+    }
+  }
+
+  private tooLong() {
+    if (!this.limit) return false;
+    if (!this.text) return false;
+    return this.text.length > this.limit;
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
deleted file mode 100644
index b942d07..0000000
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html` [[_computeDisplayText(text, limit)]] `;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
index 3b99d6d..e3e72d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
@@ -23,77 +23,65 @@
 suite('gr-limited-text tests', () => {
   let element;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
   });
 
-  test('tooltip without title input', () => {
-    const updateSpy = sinon.spy(element, '_updateTitle');
+  test('tooltip without title input', async () => {
     element.text = 'abc 123';
-    flush();
-    assert.isTrue(updateSpy.calledOnce);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
+    await element.updateComplete;
+    assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
 
     element.limit = 10;
-    flush();
-    assert.isTrue(updateSpy.calledTwice);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
+    await element.updateComplete;
+    assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
 
     element.limit = 3;
-    flush();
-    assert.equal(updateSpy.callCount, 3);
-    assert.equal(element.getAttribute('title'), 'abc 123');
-    assert.equal(element.title, 'abc 123');
-    assert.isTrue(element.hasTooltip);
+    await element.updateComplete;
+    assert.isOk(element.shadowRoot.querySelector('gr-tooltip-content'));
+    assert.equal(
+        element.shadowRoot.querySelector('gr-tooltip-content').title,
+        'abc 123');
 
     element.limit = 100;
-    flush();
-    assert.equal(updateSpy.callCount, 4);
-    assert.isFalse(element.hasTooltip);
+    await element.updateComplete;
+    assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
 
     element.limit = null;
-    flush();
-    assert.equal(updateSpy.callCount, 5);
-    assert.isNotOk(element.getAttribute('title'));
-    assert.isFalse(element.hasTooltip);
+    await element.updateComplete;
+    assert.isNotOk(element.shadowRoot.querySelector('gr-tooltip-content'));
   });
 
-  test('with tooltip input', () => {
-    const updateSpy = sinon.spy(element, '_updateTitle');
+  test('with tooltip input', async () => {
     element.tooltip = 'abc 123';
-    flush();
-    assert.isTrue(updateSpy.calledOnce);
-    assert.isTrue(element.hasTooltip);
-    assert.equal(element.getAttribute('title'), 'abc 123');
-    assert.equal(element.title, 'abc 123');
+    await element.updateComplete;
+    let tooltipContent = element.shadowRoot.querySelector('gr-tooltip-content');
+    assert.isOk(tooltipContent);
+    assert.equal(tooltipContent.title, 'abc 123');
 
     element.text = 'abc';
-    flush();
-    assert.equal(element.getAttribute('title'), 'abc 123');
-    assert.isTrue(element.hasTooltip);
+    await element.updateComplete;
+    tooltipContent = element.shadowRoot.querySelector('gr-tooltip-content');
+    assert.isOk(tooltipContent);
+    assert.equal(tooltipContent.title, 'abc 123');
 
     element.text = 'abcdef';
     element.limit = 3;
-    flush();
-    assert.equal(element.getAttribute('title'), 'abcdef (abc 123)');
-    assert.isTrue(element.hasTooltip);
+    await element.updateComplete;
+    tooltipContent = element.shadowRoot.querySelector('gr-tooltip-content');
+    assert.isOk(tooltipContent);
+    assert.equal(tooltipContent.title, 'abcdef (abc 123)');
   });
 
   test('_computeDisplayText', () => {
-    assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
-    assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
-    assert.equal(element._computeDisplayText('foo bar', null), 'foo bar');
-  });
-
-  test('when disable tooltip', () => {
-    sinon.spy(element, '_updateTitle');
-    element.text = 'abcdefghijklmn';
-    element.disableTooltip = true;
-    element.limit = 10;
-    flush();
-    assert.equal(element.getAttribute('title'), '');
+    element.text = 'foo bar';
+    element.limit = 100;
+    assert.equal(element.renderText(), 'foo bar');
+    element.limit = 4;
+    assert.equal(element.renderText(), 'foo…');
+    element.limit = 0;
+    assert.equal(element.renderText(), 'foo bar');
   });
 });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
index b74d643..801b8bf 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.ts
@@ -20,8 +20,8 @@
 import '../gr-limited-text/gr-limited-text';
 import {fireEvent} from '../../../utils/event-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -30,7 +30,7 @@
 }
 
 @customElement('gr-linked-chip')
-export class GrLinkedChip extends GrLitElement {
+export class GrLinkedChip extends LitElement {
   @property({type: String})
   href = '';
 
@@ -50,7 +50,7 @@
   @property({type: Number})
   limit?: number;
 
-  static get styles() {
+  static override get styles() {
     return [
       sharedStyles,
       css`
@@ -84,9 +84,10 @@
     ];
   }
 
-  render() {
+  override render() {
     // To pass CSS mixins for @apply to Polymer components, they need to appear
     // in <style> inside the template.
+    /* eslint-disable lit/prefer-static-styles */
     const customStyle = html`
       <style>
         gr-button::part(paper-button),
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
index 5d72925..2812b47 100644
--- 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
@@ -14,10 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import '../../../styles/shared-styles';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-linked-text_html';
 import {GrLinkTextParser, LinkTextParserConfig} from './link-text-parser';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property, query} from 'lit-element';
+import {customElement, property, observe} from '@polymer/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -25,10 +27,17 @@
   }
 }
 
+export interface GrLinkedText {
+  $: {
+    output: HTMLSpanElement;
+  };
+}
+
 @customElement('gr-linked-text')
-export class GrLinkedText extends GrLitElement {
-  @query('#output')
-  outputElement?: HTMLSpanElement;
+export class GrLinkedText extends PolymerElement {
+  static get template() {
+    return htmlTemplate;
+  }
 
   @property({type: Boolean})
   removeZeroWidthSpace?: boolean;
@@ -37,63 +46,61 @@
   @property({type: String})
   content: string | null = null;
 
-  @property({type: Boolean, reflect: true})
+  @property({type: Boolean, reflectToAttribute: true})
   pre = false;
 
-  @property({type: Boolean, reflect: true})
+  @property({type: Boolean, reflectToAttribute: true})
   disabled = false;
 
   @property({type: Object})
   config?: LinkTextParserConfig;
 
-  static override get styles() {
-    return [
-      css`
-        :host {
-          display: block;
-        }
-        :host([pre]) span {
-          white-space: var(--linked-text-white-space, pre-wrap);
-          word-wrap: var(--linked-text-word-wrap, break-word);
-        }
-        :host([disabled]) a {
-          color: inherit;
-          text-decoration: none;
-          pointer-events: none;
-        }
-        a {
-          color: var(--link-color);
-        }
-      `,
-    ];
-  }
-
-  override render() {
+  @observe('content')
+  _contentChanged(content: string | null) {
+    // In the case where the config may not be set (perhaps due to the
+    // request for it still being in flight), set the content anyway to
+    // prevent waiting on the config to display the text.
     if (!this.config) {
       return;
     }
-    return html`<span id="output">${this.content}</span>`;
+    this.$.output.textContent = content;
   }
 
-  override updated() {
-    if (!this.outputElement || !this.config) return;
-    this.outputElement.textContent = '';
+  /**
+   * Because either the source text or the linkification config has changed,
+   * the content should be re-parsed.
+   *
+   * @param content The raw, un-linkified source string to parse.
+   * @param config The server config specifying commentLink patterns
+   */
+  @observe('content', 'config')
+  _contentOrConfigChanged(
+    content: string | null,
+    config?: LinkTextParserConfig
+  ) {
+    if (!config) {
+      return;
+    }
+
     // TODO(TS): mapCommentlinks always has value, remove
     if (!GerritNav.mapCommentlinks) return;
-    const config = GerritNav.mapCommentlinks(this.config);
+    config = GerritNav.mapCommentlinks(config);
+    const output = this.$.output;
+    output.textContent = '';
     const parser = new GrLinkTextParser(
       config,
       (text: string | null, href: string | null, fragment?: DocumentFragment) =>
         this._handleParseResult(text, href, fragment),
       this.removeZeroWidthSpace
     );
-    parser.parse(this.content);
+    parser.parse(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 => {
+    output.querySelectorAll('a').forEach(anchor => {
       if (anchor.hostname === window.location.hostname) {
         anchor.removeAttribute('target');
       } else {
@@ -117,8 +124,7 @@
     href: string | null,
     fragment?: DocumentFragment
   ) {
-    const output = this.outputElement;
-    if (!output) return;
+    const output = this.$.output;
     if (href) {
       const a = document.createElement('a');
       a.setAttribute('href', href);
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
similarity index 69%
rename from polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
rename to polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
index 4808832..0d44bc8 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
@@ -19,12 +19,20 @@
 export const htmlTemplate = html`
   <style>
     :host {
+      display: block;
+    }
+    :host([pre]) span {
+      white-space: var(--linked-text-white-space, pre-wrap);
+      word-wrap: var(--linked-text-word-wrap, break-word);
+    }
+    :host([disabled]) a {
       color: inherit;
-      display: inline;
+      text-decoration: none;
+      pointer-events: none;
+    }
+    a {
+      color: var(--link-color);
     }
   </style>
-  <span>
-    [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative,
-    showDateAndTime, showYesterday)]]
-  </span>
+  <span id="output"></span>
 `;
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
index c97c168..b2cdba1 100644
--- 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
@@ -85,11 +85,10 @@
     window.CANONICAL_PATH = originalCanonicalPath;
   });
 
-  test('URL pattern was parsed and linked.', async () => {
+  test('URL pattern was parsed and linked.', () => {
     // Regular inline link.
     const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
     element.content = url;
-    await flush();
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     assert.equal(linkEl.target, '_blank');
@@ -98,10 +97,9 @@
     assert.equal(linkEl.textContent, url);
   });
 
-  test('Bug pattern was parsed and linked', async () => {
+  test('Bug pattern was parsed and linked', () => {
     // "Issue/Bug" pattern.
     element.content = 'Issue 3650';
-    await flush();
 
     let linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
@@ -111,7 +109,6 @@
     assert.equal(linkEl.textContent, 'Issue 3650');
 
     element.content = 'Bug 3650';
-    await flush();
     linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     assert.equal(linkEl.target, '_blank');
@@ -120,10 +117,10 @@
     assert.equal(linkEl.textContent, 'Bug 3650');
   });
 
-  test('Pattern with same prefix as link was correctly parsed', async () => {
+  test('Pattern with same prefix as link was correctly parsed', () => {
     // Pattern starts with the same prefix (`http`) as the url.
     element.content = 'httpexample 3650';
-    await flush();
+
     assert.equal(queryAndAssert(element, '#output').childNodes.length, 1);
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
@@ -133,12 +130,12 @@
     assert.equal(linkEl.textContent, 'httpexample 3650');
   });
 
-  test('Change-Id pattern was parsed and linked', async () => {
+  test('Change-Id pattern was parsed and linked', () => {
     // "Change-Id:" pattern.
     const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
     const prefix = 'Change-Id: ';
     element.content = prefix + changeID;
-    await flush();
+
     const textNode = queryAndAssert(element, '#output').childNodes[0];
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[1] as HTMLAnchorElement;
@@ -150,14 +147,14 @@
     assert.equal(linkEl.textContent, changeID);
   });
 
-  test('Change-Id pattern was parsed and linked with base url', async () => {
+  test('Change-Id pattern was parsed and linked with base url', () => {
     window.CANONICAL_PATH = '/r';
 
     // "Change-Id:" pattern.
     const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
     const prefix = 'Change-Id: ';
     element.content = prefix + changeID;
-    await flush();
+
     const textNode = queryAndAssert(element, '#output').childNodes[0];
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[1] as HTMLAnchorElement;
@@ -169,9 +166,8 @@
     assert.equal(linkEl.textContent, changeID);
   });
 
-  test('Multiple matches', async () => {
+  test('Multiple matches', () => {
     element.content = 'Issue 3650\nIssue 3450';
-    await flush();
     const linkEl1 = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     const linkEl2 = queryAndAssert(element, '#output')
@@ -192,7 +188,7 @@
     assert.equal(linkEl2.textContent, 'Issue 3450');
   });
 
-  test('Change-Id pattern parsed before bug pattern', async () => {
+  test('Change-Id pattern parsed before bug pattern', () => {
     // "Change-Id:" pattern.
     const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
     const prefix = 'Change-Id: ';
@@ -204,7 +200,7 @@
     const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650';
 
     element.content = prefix + changeID + bug;
-    await flush();
+
     const textNode = queryAndAssert(element, '#output').childNodes[0];
     const changeLinkEl = queryAndAssert(element, '#output')
       .childNodes[1] as HTMLAnchorElement;
@@ -222,9 +218,8 @@
     assert.equal(bugLinkEl.textContent, 'Issue 3650');
   });
 
-  test('html field in link config', async () => {
+  test('html field in link config', () => {
     element.content = 'google:do a barrel roll';
-    await flush();
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     assert.equal(
@@ -234,58 +229,52 @@
     assert.equal(linkEl.textContent, 'do a barrel roll');
   });
 
-  test('removing hash from links', async () => {
+  test('removing hash from links', () => {
     element.content = 'hash:foo';
-    await flush();
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
     assert.equal(linkEl.textContent, 'foo');
   });
 
-  test('html with base url', async () => {
+  test('html with base url', () => {
     window.CANONICAL_PATH = '/r';
 
     element.content = 'test foo';
-    await flush();
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
     assert.equal(linkEl.textContent, 'foo');
   });
 
-  test('a is not at start', async () => {
+  test('a is not at start', () => {
     window.CANONICAL_PATH = '/r';
 
     element.content = 'a test foo';
-    await flush();
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[1] as HTMLAnchorElement;
     assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
     assert.equal(linkEl.textContent, 'foo');
   });
 
-  test('hash html with base url', async () => {
+  test('hash html with base url', () => {
     window.CANONICAL_PATH = '/r';
 
     element.content = 'hash:foo';
-    await flush();
     const linkEl = queryAndAssert(element, '#output')
       .childNodes[0] as HTMLAnchorElement;
     assert.isTrue(linkEl.href.endsWith('/r/awesomesauce'));
     assert.equal(linkEl.textContent, 'foo');
   });
 
-  test('disabled config', async () => {
+  test('disabled config', () => {
     element.content = 'foo:baz';
-    await flush();
     assert.equal(queryAndAssert(element, '#output').innerHTML, 'foo:baz');
   });
 
-  test('R=email labels link correctly', async () => {
+  test('R=email labels link correctly', () => {
     element.removeZeroWidthSpace = true;
     element.content = 'R=\u200Btest@google.com';
-    await flush();
     assert.equal(
       queryAndAssert(element, '#output').textContent,
       'R=test@google.com'
@@ -296,10 +285,9 @@
     );
   });
 
-  test('CC=email labels link correctly', async () => {
+  test('CC=email labels link correctly', () => {
     element.removeZeroWidthSpace = true;
     element.content = 'CC=\u200Btest@google.com';
-    await flush();
     assert.equal(
       queryAndAssert(element, '#output').textContent,
       'CC=test@google.com'
@@ -310,42 +298,36 @@
     );
   });
 
-  test('only {http,https,mailto} protocols are linkified', async () => {
+  test('only {http,https,mailto} protocols are linkified', () => {
     element.content = 'xx mailto:test@google.com yy';
-    await flush();
     let links = queryAndAssert(element, '#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 flush();
     links = queryAndAssert(element, '#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 flush();
     links = queryAndAssert(element, '#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 flush();
     links = queryAndAssert(element, '#output').querySelectorAll('a');
     assert.equal(links.length, 0);
 
     element.content = 'xx ftp://google.com yy';
-    await flush();
     links = queryAndAssert(element, '#output').querySelectorAll('a');
     assert.equal(links.length, 0);
   });
 
-  test('links without leading whitespace are linkified', async () => {
+  test('links without leading whitespace are linkified', () => {
     element.content = 'xx abcmailto:test@google.com yy';
-    await flush();
     assert.equal(
       queryAndAssert(element, '#output').innerHTML.substr(0, 6),
       'xx abc'
@@ -356,7 +338,6 @@
     assert.equal(links[0].innerHTML, 'mailto:test@google.com');
 
     element.content = 'xx defhttp://google.com yy';
-    await flush();
     assert.equal(
       queryAndAssert(element, '#output').innerHTML.substr(0, 6),
       'xx def'
@@ -367,7 +348,6 @@
     assert.equal(links[0].innerHTML, 'http://google.com');
 
     element.content = 'xx qwehttps://google.com yy';
-    await flush();
     assert.equal(
       queryAndAssert(element, '#output').innerHTML.substr(0, 6),
       'xx qwe'
@@ -379,7 +359,6 @@
 
     // Non-latin character
     element.content = 'xx абвhttps://google.com yy';
-    await flush();
     assert.equal(
       queryAndAssert(element, '#output').innerHTML.substr(0, 6),
       'xx абв'
@@ -390,17 +369,15 @@
     assert.equal(links[0].innerHTML, 'https://google.com');
 
     element.content = 'xx ssh://google.com yy';
-    await flush();
     links = queryAndAssert(element, '#output').querySelectorAll('a');
     assert.equal(links.length, 0);
 
     element.content = 'xx ftp://google.com yy';
-    await flush();
     links = queryAndAssert(element, '#output').querySelectorAll('a');
     assert.equal(links.length, 0);
   });
 
-  test('overlapping links', async () => {
+  test('overlapping links', () => {
     element.config = {
       b1: {
         match: '(B:\\s*)(\\d+)',
@@ -412,8 +389,7 @@
       },
     };
     element.content = '- B: 123, 45';
-    await flush();
-    const links = element.shadowRoot!.querySelectorAll('a');
+    const links = element.root!.querySelectorAll('a');
 
     assert.equal(links.length, 2);
     assert.equal(
@@ -427,4 +403,12 @@
     assert.equal(links[1].href, 'ftp://foo/45');
     assert.equal(links[1].textContent, '45');
   });
+
+  test('_contentOrConfigChanged called with config', () => {
+    const contentStub = sinon.stub(element, '_contentChanged');
+    const contentConfigStub = sinon.stub(element, '_contentOrConfigChanged');
+    element.content = 'some text';
+    assert.isTrue(contentStub.called);
+    assert.isTrue(contentConfigStub.called);
+  });
 });
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
index b6cb8f9..1622365 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -34,11 +34,14 @@
   }
 }
 
-@customElement('gr-overlay')
-export class GrOverlay extends IronOverlayMixin(
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = IronOverlayMixin(
   PolymerElement,
   IronOverlayBehavior as IronOverlayBehavior
-) {
+);
+
+@customElement('gr-overlay')
+export class GrOverlay extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
index b2477dd..423a1a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
@@ -53,7 +53,7 @@
     this.bodyScrollHandler = () => this._handleBodyScroll();
   }
 
-  connectedCallback() {
+  override connectedCallback() {
     super.connectedCallback();
     window.addEventListener('scroll', this.bodyScrollHandler);
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
index 5b42318..7ed000b 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.ts
@@ -18,8 +18,8 @@
 import {GrCopyClipboard} from '../gr-copy-clipboard/gr-copy-clipboard';
 import {queryAndAssert} from '../../../utils/common-util';
 import {sharedStyles} from '../../../styles/shared-styles';
-import {GrLitElement} from '../../lit/gr-lit-element';
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -28,7 +28,7 @@
 }
 
 @customElement('gr-shell-command')
-export class GrShellCommand extends GrLitElement {
+export class GrShellCommand extends LitElement {
   @property({type: String})
   command: string | undefined;
 
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 3d5cb3d..ce1eec3 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -85,8 +85,11 @@
   }
 }
 
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = KeyboardShortcutMixin(PolymerElement);
+
 @customElement('gr-textarea')
-export class GrTextarea extends KeyboardShortcutMixin(PolymerElement) {
+export class GrTextarea extends base {
   static get template() {
     return htmlTemplate;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index feed8a6..2b9a868 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -15,10 +15,13 @@
  * limitations under the License.
  */
 import '../gr-icons/gr-icons';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-tooltip-content_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {customElement, property} from '@polymer/decorators';
+import '../gr-tooltip/gr-tooltip';
+import {getRootElement} from '../../../scripts/rootElement';
+import {GrTooltip} from '../gr-tooltip/gr-tooltip';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+
+const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -26,18 +29,202 @@
   }
 }
 
-/**
- * Transclude anything inside and wrap them to support tooltip functionality.
- */
 @customElement('gr-tooltip-content')
-export class GrTooltipContent extends TooltipMixin(PolymerElement) {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrTooltipContent extends LitElement {
+  @property({type: Boolean, attribute: 'has-tooltip', reflect: true})
+  hasTooltip = false;
 
-  @property({type: String, reflectToAttribute: true})
+  @property({type: Boolean, attribute: 'position-below', reflect: true})
+  positionBelow = false;
+
+  @property({type: String, attribute: 'max-width', reflect: true})
   maxWidth?: string;
 
   @property({type: Boolean})
   showIcon = false;
+
+  // Should be private but used in tests.
+  @state()
+  isTouchDevice = 'ontouchstart' in document.documentElement;
+
+  // Should be private but used in tests.
+  tooltip: GrTooltip | null = null;
+
+  @state()
+  private originalTitle = '';
+
+  private hasSetupTooltipListeners = false;
+
+  private readonly windowScrollHandler: () => void;
+
+  private readonly showHandler: () => void;
+
+  private readonly hideHandler: (e: Event) => void;
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
+  constructor() {
+    super();
+    this.windowScrollHandler = () => this._handleWindowScroll();
+    this.showHandler = () => this._handleShowTooltip();
+    this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
+  }
+
+  override disconnectedCallback() {
+    this._handleHideTooltip(undefined);
+    this.removeEventListener('mouseenter', this.showHandler);
+    window.removeEventListener('scroll', this.windowScrollHandler);
+    super.disconnectedCallback();
+  }
+
+  static override get styles() {
+    return [
+      css`
+        iron-icon {
+          width: var(--line-height-normal);
+          height: var(--line-height-normal);
+          vertical-align: top;
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <slot></slot>
+      ${this.renderIcon()}
+    `;
+  }
+
+  renderIcon() {
+    if (!this.showIcon) return;
+    return html`<iron-icon icon="gr-icons:info"></iron-icon>`;
+  }
+
+  override updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('hasTooltip')) {
+      this.setupTooltipListeners();
+    }
+  }
+
+  private setupTooltipListeners() {
+    if (!this.hasTooltip) {
+      if (this.hasSetupTooltipListeners) {
+        // if attribute set to false, remove the listener
+        this.removeEventListener('mouseenter', this.showHandler);
+        this.hasSetupTooltipListeners = false;
+      }
+      return;
+    }
+
+    if (this.hasSetupTooltipListeners) {
+      return;
+    }
+    this.hasSetupTooltipListeners = true;
+    this.addEventListener('mouseenter', this.showHandler);
+  }
+
+  _handleShowTooltip() {
+    if (this.isTouchDevice) {
+      return;
+    }
+
+    if (
+      !this.hasAttribute('title') ||
+      this.getAttribute('title') === '' ||
+      this.tooltip
+    ) {
+      return;
+    }
+
+    // Store the title attribute text then set it to an empty string to
+    // prevent it from showing natively.
+    this.originalTitle = this.getAttribute('title') || '';
+    this.setAttribute('title', '');
+
+    const tooltip = document.createElement('gr-tooltip');
+    tooltip.text = this.originalTitle;
+    tooltip.maxWidth = this.getAttribute('max-width') || '';
+    tooltip.positionBelow = this.hasAttribute('position-below');
+
+    // Set visibility to hidden before appending to the DOM so that
+    // calculations can be made based on the element’s size.
+    tooltip.style.visibility = 'hidden';
+    getRootElement().appendChild(tooltip);
+    this._positionTooltip(tooltip);
+    tooltip.style.visibility = 'initial';
+
+    this.tooltip = tooltip;
+    window.addEventListener('scroll', this.windowScrollHandler);
+    this.addEventListener('mouseleave', this.hideHandler);
+    this.addEventListener('click', this.hideHandler);
+    tooltip.addEventListener('mouseleave', this.hideHandler);
+  }
+
+  _handleHideTooltip(e: Event | undefined) {
+    if (this.isTouchDevice) {
+      return;
+    }
+    if (!this.hasAttribute('title') || !this.originalTitle) {
+      return;
+    }
+    // Do not hide if mouse left this or this.tooltip and came to this or
+    // this.tooltip
+    if (
+      (e as MouseEvent)?.relatedTarget === this.tooltip ||
+      (e as MouseEvent)?.relatedTarget === this
+    ) {
+      return;
+    }
+
+    window.removeEventListener('scroll', this.windowScrollHandler);
+    this.removeEventListener('mouseleave', this.hideHandler);
+    this.removeEventListener('click', this.hideHandler);
+    this.setAttribute('title', this.originalTitle);
+    this.tooltip?.removeEventListener('mouseleave', this.hideHandler);
+
+    if (this.tooltip?.parentNode) {
+      this.tooltip.parentNode.removeChild(this.tooltip);
+    }
+    this.tooltip = null;
+  }
+
+  _handleWindowScroll() {
+    if (!this.tooltip) {
+      return;
+    }
+    // This wait is needed for tooltips to be positioned correctly in Firefox
+    // and Safari.
+    this.updateComplete.then(() => this._positionTooltip(this.tooltip));
+  }
+
+  // private but used in tests.
+  async _positionTooltip(tooltip: GrTooltip | null) {
+    if (tooltip === null) return;
+    const rect = this.getBoundingClientRect();
+    const boxRect = tooltip.getBoundingClientRect();
+    if (!tooltip.parentElement) {
+      return;
+    }
+    const parentRect = tooltip.parentElement.getBoundingClientRect();
+    const top = rect.top - parentRect.top;
+    const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
+    const right = parentRect.width - left - boxRect.width;
+    if (left < 0) {
+      tooltip.updateStyles({
+        '--gr-tooltip-arrow-center-offset': `${left}px`,
+      });
+    } else if (right < 0) {
+      tooltip.updateStyles({
+        '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
+      });
+    }
+    tooltip.style.left = `${Math.max(0, left)}px`;
+
+    if (!this.positionBelow) {
+      tooltip.style.top = `${Math.max(0, top)}px`;
+      tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
+    } else {
+      tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
+    }
+  }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
deleted file mode 100644
index 952420d..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style>
-    iron-icon {
-      width: var(--line-height-normal);
-      height: var(--line-height-normal);
-      vertical-align: top;
-    }
-  </style>
-  <slot></slot
-  ><!--
- --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
index f905eaa..8d3bbb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -17,35 +17,162 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-tooltip-content.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-const basicFixture = fixtureFromTemplate(html`
-<gr-tooltip-content>
-    </gr-tooltip-content>
-`);
+const basicFixture = fixtureFromElement('gr-tooltip-content');
 
 suite('gr-tooltip-content tests', () => {
   let element;
-  setup(() => {
+
+  function makeTooltip(tooltipRect, parentRect) {
+    return {
+      getBoundingClientRect() { return tooltipRect; },
+      updateStyles: sinon.stub(),
+      style: {left: 0, top: 0},
+      parentElement: {
+        getBoundingClientRect() { return parentRect; },
+      },
+    };
+  }
+
+  setup(async () => {
     element = basicFixture.instantiate();
+    element.title = 'title';
+    await element.updateComplete;
   });
 
   test('icon is not visible by default', () => {
-    assert.equal(dom(element.root)
-        .querySelector('iron-icon').hidden, true);
+    assert.isNotOk(element.shadowRoot.querySelector('iron-icon'));
   });
 
-  test('position-below attribute is reflected', () => {
+  test('icon is visible with showIcon property', async () => {
+    element.showIcon = true;
+    await element.updateComplete;
+    assert.isOk(element.shadowRoot.querySelector('iron-icon'));
+  });
+
+  test('position-below attribute is reflected', async () => {
     assert.isFalse(element.hasAttribute('position-below'));
     element.positionBelow = true;
+    await element.updateComplete;
     assert.isTrue(element.hasAttribute('position-below'));
   });
 
-  test('icon is visible with showIcon property', () => {
-    element.showIcon = true;
-    assert.equal(dom(element.root)
-        .querySelector('iron-icon').hidden, false);
+  test('normal position', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+      return {top: 100, left: 100, width: 200};
+    });
+    const tooltip = makeTooltip(
+        {height: 30, width: 50},
+        {top: 0, left: 0, width: 1000});
+
+    element._positionTooltip(tooltip);
+    assert.isFalse(tooltip.updateStyles.called);
+    assert.equal(tooltip.style.left, '175px');
+    assert.equal(tooltip.style.top, '100px');
+  });
+
+  test('left side position', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+      return {top: 100, left: 10, width: 50};
+    });
+    const tooltip = makeTooltip(
+        {height: 30, width: 120},
+        {top: 0, left: 0, width: 1000});
+
+    element._positionTooltip(tooltip);
+    assert.isTrue(tooltip.updateStyles.called);
+    const offset = tooltip.updateStyles
+        .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+    assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+    assert.equal(tooltip.style.left, '0px');
+    assert.equal(tooltip.style.top, '100px');
+  });
+
+  test('right side position', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+      return {top: 100, left: 950, width: 50};
+    });
+    const tooltip = makeTooltip(
+        {height: 30, width: 120},
+        {top: 0, left: 0, width: 1000});
+
+    element._positionTooltip(tooltip);
+    assert.isTrue(tooltip.updateStyles.called);
+    const offset = tooltip.updateStyles
+        .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+    assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+    assert.equal(tooltip.style.left, '915px');
+    assert.equal(tooltip.style.top, '100px');
+  });
+
+  test('position to bottom', () => {
+    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+      return {top: 100, left: 950, width: 50, height: 50};
+    });
+    const tooltip = makeTooltip(
+        {height: 30, width: 120},
+        {top: 0, left: 0, width: 1000});
+
+    element.positionBelow = true;
+    element._positionTooltip(tooltip);
+    assert.isTrue(tooltip.updateStyles.called);
+    const offset = tooltip.updateStyles
+        .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+    assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+    assert.equal(tooltip.style.left, '915px');
+    assert.equal(tooltip.style.top, '157.2px');
+  });
+
+  test('hides tooltip when detached', async () => {
+    sinon.stub(element, '_handleHideTooltip');
+    element.remove();
+    await element.updateComplete;
+    assert.isTrue(element._handleHideTooltip.called);
+  });
+
+  test('sets up listeners when has-tooltip is changed', async () => {
+    const addListenerStub = sinon.stub(element, 'addEventListener');
+    element.hasTooltip = true;
+    await element.updateComplete;
+    assert.isTrue(addListenerStub.called);
+  });
+
+  test('clean up listeners when has-tooltip changed to false', async () => {
+    const removeListenerStub = sinon.stub(element, 'removeEventListener');
+    element.hasTooltip = true;
+    await element.updateComplete;
+    element.hasTooltip = false;
+    await element.updateComplete;
+    assert.isTrue(removeListenerStub.called);
+  });
+
+  test('do not display tooltips on touch devices', async () => {
+    // On touch devices, tooltips should not be shown.
+    element.isTouchDevice = true;
+    await element.updateComplete;
+
+    // fire mouse-enter
+    element._handleShowTooltip();
+    await element.updateComplete;
+    assert.isNotOk(element.tooltip);
+
+    // fire mouse-enter
+    element._handleHideTooltip();
+    await element.updateComplete;
+    assert.isNotOk(element.tooltip);
+
+    // On other devices, tooltips should be shown.
+    element.isTouchDevice = false;
+
+    // fire mouse-enter
+    element._handleShowTooltip();
+    await element.updateComplete;
+    assert.isOk(element.tooltip);
+
+    // fire mouse-enter
+    element._handleHideTooltip();
+    await element.updateComplete;
+    assert.isNotOk(element.tooltip);
   });
 });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 789637c..2fd3fb9 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -14,12 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {css, customElement, html, property} from 'lit-element';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
 import {ApprovalInfo, LabelInfo} from '../../../api/rest-api';
 import {appContext} from '../../../services/app-context';
 import {KnownExperimentId} from '../../../services/flags/flags';
-import {getVotingRangeOrDefault, valueString} from '../../../utils/label-util';
-import {GrLitElement} from '../../lit/gr-lit-element';
+import {
+  classForLabelStatus,
+  getLabelStatus,
+  valueString,
+} from '../../../utils/label-util';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -28,73 +32,99 @@
 }
 
 @customElement('gr-vote-chip')
-export class GrVoteChip extends GrLitElement {
+export class GrVoteChip extends LitElement {
   @property({type: Object})
   vote?: ApprovalInfo;
 
   @property({type: Object})
   label?: LabelInfo;
 
+  /** Show vote as a stack of same votes. */
+  @property({type: Boolean})
+  more = false;
+
   private readonly flagsService = appContext.flagsService;
 
-  static get styles() {
+  static override get styles() {
     return [
       css`
-        .chipVote {
-          display: flex;
-          justify-content: center;
-          margin-right: var(--spacing-s);
-          padding: 1px;
-          border-radius: var(--border-radius);
-          color: var(--vote-text-color);
-          border: 1px solid var(--border-color);
-          line-height: calc(var(--line-height-normal) - 4px);
-        }
-        .max {
+        .vote-chip.max {
           background-color: var(--vote-color-approved);
+          padding: 2px;
         }
-        .min {
+        .vote-chip.max.more {
+          padding: 1px;
+          border: 1px solid var(--vote-outline-recommended);
+        }
+        .vote-chip.min {
           background-color: var(--vote-color-rejected);
+          padding: 2px;
         }
-        .positive {
+        .vote-chip.min.more {
+          padding: 1px;
+          border: 1px solid var(--vote-outline-disliked);
+        }
+        .vote-chip.positive,
+        .chip-angle.max,
+        .chip-angle.positive {
           background-color: var(--vote-color-recommended);
           border: 1px solid var(--vote-outline-recommended);
           color: var(--chip-color);
         }
-        .negative {
+        .vote-chip.negative,
+        .chip-angle.min,
+        .chip-angle.negative {
           background-color: var(--vote-color-disliked);
           border: 1px solid var(--vote-outline-disliked);
           color: var(--chip-color);
         }
+        .vote-chip,
+        .chip-angle {
+          display: flex;
+          width: var(--gr-vote-chip-width, 16px);
+          height: var(--gr-vote-chip-height, 16px);
+          justify-content: center;
+          margin-right: var(--spacing-s);
+          padding: 1px;
+          border-radius: var(--border-radius);
+          line-height: var(--gr-vote-chip-width, 16px);
+        }
+        .vote-chip {
+          position: relative;
+          z-index: 2;
+        }
+        .chip-angle {
+          position: absolute;
+          top: 2px;
+          left: 2px;
+          z-index: 1;
+        }
+        .container {
+          position: relative;
+        }
       `,
     ];
   }
 
-  render() {
+  override render() {
     if (!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI))
       return;
     if (!this.vote?.value) return;
     const className = this.computeClass(this.vote.value, this.label);
-    return html`<div class="chipVote ${className}">
-      ${valueString(this.vote.value)}
-    </div>`;
+    return html`<span class="container">
+      <div class="vote-chip ${className} ${this.more ? 'more' : ''}">
+        ${valueString(this.vote.value)}
+      </div>
+      ${this.more
+        ? html`<div class="chip-angle ${className}">
+            ${valueString(this.vote.value)}
+          </div>`
+        : ''}
+    </span>`;
   }
 
-  computeClass(vote: Number, label?: LabelInfo) {
-    const votingRange = getVotingRangeOrDefault(label);
-    if (vote > 0) {
-      if (vote === votingRange.max) {
-        return 'max';
-      } else {
-        return 'positive';
-      }
-    } else if (vote < 0) {
-      if (vote === votingRange.min) {
-        return 'min';
-      } else {
-        return 'negative';
-      }
-    }
-    return '';
+  computeClass(vote: number, label?: LabelInfo) {
+    const status = getLabelStatus(label, vote);
+    return classForLabelStatus(status);
   }
 }
diff --git a/polygerrit-ui/app/empty_test.sh b/polygerrit-ui/app/empty_test.sh
deleted file mode 100755
index e69de29..0000000
--- a/polygerrit-ui/app/empty_test.sh
+++ /dev/null
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
deleted file mode 100644
index e3b75de..0000000
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin';
-import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-import {property} from '@polymer/decorators';
-import {ServerInfo} from '../../types/common';
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const ChangeTableMixin = dedupingMixin(
-  <T extends Constructor<PolymerElement>>(
-    superClass: T
-  ): T & Constructor<ChangeTableMixinInterface> => {
-    /**
-     * @polymer
-     * @mixinClass
-     */
-    class Mixin extends superClass {
-      @property({type: Array})
-      readonly columnNames: string[] = [
-        'Subject',
-        'Status',
-        'Owner',
-        'Assignee',
-        'Reviewers',
-        'Comments',
-        'Repo',
-        'Branch',
-        'Updated',
-        'Size',
-      ];
-
-      isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
-        if (!columnsToDisplay || !columnToCheck) {
-          return false;
-        }
-        return !columnsToDisplay.includes(columnToCheck);
-      }
-
-      /**
-       * Is the column disabled by a server config or experiment? For example the
-       * assignee feature might be disabled and thus the corresponding column is
-       * also disabled.
-       *
-       */
-      isColumnEnabled(
-        column: string,
-        config: ServerInfo,
-        experiments: string[]
-      ) {
-        if (!config || !config.change) return true;
-        if (column === 'Assignee') return !!config.change.enable_assignee;
-        if (column === 'Comments')
-          return experiments.includes('comments-column');
-        return true;
-      }
-
-      /**
-       * @return enabled columns, see isColumnEnabled().
-       */
-      getEnabledColumns(
-        columns: string[],
-        config: ServerInfo,
-        experiments: string[]
-      ) {
-        return columns.filter(col =>
-          this.isColumnEnabled(col, config, experiments)
-        );
-      }
-
-      /**
-       * The Project column was renamed to Repo, but some users may have
-       * preferences that use its old name. If that column is found, rename it
-       * before use.
-       *
-       * @return If the column was renamed, returns a new array
-       * with the corrected name. Otherwise, it returns the original param.
-       */
-      renameProjectToRepoColumn(columns: string[]) {
-        const projectIndex = columns.indexOf('Project');
-        if (projectIndex === -1) {
-          return columns;
-        }
-        const newColumns = [...columns];
-        newColumns[projectIndex] = 'Repo';
-        return newColumns;
-      }
-    }
-
-    return Mixin;
-  }
-);
-
-export interface ChangeTableMixinInterface {
-  readonly columnNames: string[];
-  isColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]): boolean;
-  isColumnEnabled(
-    column: string,
-    config: ServerInfo,
-    experiments: string[]
-  ): boolean;
-  getEnabledColumns(
-    columns: string[],
-    config: ServerInfo,
-    experiments: string[]
-  ): string[];
-  renameProjectToRepoColumn(columns: string[]): string[];
-}
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
deleted file mode 100644
index 0d6b4ad..0000000
--- a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {ChangeTableMixin} from './gr-change-table-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-
-class GrChangeTableMixinTestElement extends
-  ChangeTableMixin(PolymerElement) {
-  static get is() { return 'gr-change-table-mixin-test-element'; }
-}
-
-customElements.define(GrChangeTableMixinTestElement.is,
-    GrChangeTableMixinTestElement);
-
-const basicFixture = fixtureFromElement(
-    'gr-change-table-mixin-test-element');
-
-suite('gr-change-table-mixin tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('isColumnHidden', () => {
-    const columnToCheck = 'Repo';
-    let columnsToDisplay = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Repo',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay));
-
-    columnsToDisplay = [
-      'Subject',
-      'Status',
-      'Owner',
-      'Assignee',
-      'Branch',
-      'Updated',
-      'Size',
-    ];
-    assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay));
-  });
-
-  test('renameProjectToRepoColumn maps Project to Repo', () => {
-    const columns = [
-      'Subject',
-      'Status',
-      'Owner',
-    ];
-    assert.deepEqual(element.renameProjectToRepoColumn(columns),
-        columns.slice(0));
-    assert.deepEqual(
-        element.renameProjectToRepoColumn(columns.concat(['Project'])),
-        columns.slice(0).concat(['Repo']));
-  });
-});
-
diff --git a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.ts b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.ts
deleted file mode 100644
index af89194..0000000
--- a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {encodeURL, getBaseUrl} from '../../utils/url-util';
-import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin';
-import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const ListViewMixin = dedupingMixin(
-  <T extends Constructor<PolymerElement>>(
-    superClass: T
-  ): T & Constructor<ListViewMixinInterface> => {
-    /**
-     * @polymer
-     * @mixinClass
-     */
-    class Mixin extends superClass {
-      computeLoadingClass(loading: boolean): string {
-        return loading ? 'loading' : '';
-      }
-
-      computeShownItems<T>(items: T[]): T[] {
-        return items.slice(0, 25);
-      }
-
-      getUrl(path: string, item: string) {
-        return getBaseUrl() + path + encodeURL(item, true);
-      }
-
-      getFilterValue<T extends ListViewParams>(params: T): string {
-        if (!params) {
-          return '';
-        }
-        return params.filter || '';
-      }
-
-      getOffsetValue<T extends ListViewParams>(params: T): number {
-        if (params?.offset) {
-          return Number(params.offset);
-        }
-        return 0;
-      }
-    }
-
-    return Mixin;
-  }
-);
-
-export interface ListViewMixinInterface {
-  computeLoadingClass(loading: boolean): string;
-  computeShownItems<T>(items: T[]): T[];
-  getUrl(path: string, item: string): string;
-  getFilterValue<T extends ListViewParams>(params: T): string;
-  getOffsetValue<T extends ListViewParams>(params: T): number;
-}
-
-export interface ListViewParams {
-  filter?: string | null;
-  offset?: number | string;
-}
diff --git a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
deleted file mode 100644
index 407f29f..0000000
--- a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {ListViewMixin} from './gr-list-view-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-
-const basicFixture = fixtureFromElement(
-    'gr-list-view-mixin-test-element');
-
-class GrListViewMixinTestElement extends
-  ListViewMixin(PolymerElement) {
-  static get is() { return 'gr-list-view-mixin-test-element'; }
-}
-
-customElements.define(GrListViewMixinTestElement.is,
-    GrListViewMixinTestElement);
-
-suite('gr-list-view-mixin tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('computeLoadingClass', () => {
-    assert.equal(element.computeLoadingClass(true), 'loading');
-    assert.equal(element.computeLoadingClass(false), '');
-  });
-
-  test('computeShownItems', () => {
-    const myArr = new Array(26);
-    assert.equal(element.computeShownItems(myArr).length, 25);
-  });
-
-  test('getUrl', () => {
-    assert.equal(element.getUrl('/path/to/something/', 'item'),
-        '/path/to/something/item');
-    assert.equal(element.getUrl('/path/to/something/', 'item%test'),
-        '/path/to/something/item%2525test');
-  });
-
-  test('getFilterValue', () => {
-    let params;
-    assert.equal(element.getFilterValue(params), '');
-
-    params = {filter: null};
-    assert.equal(element.getFilterValue(params), '');
-
-    params = {filter: 'test'};
-    assert.equal(element.getFilterValue(params), 'test');
-  });
-
-  test('getOffsetValue', () => {
-    let params;
-    assert.equal(element.getOffsetValue(params), 0);
-
-    params = {offset: null};
-    assert.equal(element.getOffsetValue(params), 0);
-
-    params = {offset: 1};
-    assert.equal(element.getOffsetValue(params), 1);
-  });
-});
-
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
deleted file mode 100644
index 4d119eb..0000000
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../elements/shared/gr-tooltip/gr-tooltip';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {getRootElement} from '../../scripts/rootElement';
-import {property, observe} from '@polymer/decorators';
-import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin';
-import {GrTooltip} from '../../elements/shared/gr-tooltip/gr-tooltip';
-import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-
-const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
-
-/** The interface corresponding to TooltipMixin */
-export interface TooltipMixinInterface {
-  hasTooltip: boolean;
-  positionBelow: boolean;
-  _isTouchDevice: boolean;
-  _tooltip: GrTooltip | null;
-  _titleText: string;
-  _hasSetupTooltipListeners: boolean;
-}
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const TooltipMixin = dedupingMixin(
-  <T extends Constructor<PolymerElement>>(
-    superClass: T
-  ): T & Constructor<TooltipMixinInterface> => {
-    /**
-     * @polymer
-     * @mixinClass
-     */
-    class Mixin extends superClass {
-      @property({type: Boolean})
-      hasTooltip = false;
-
-      @property({type: Boolean, reflectToAttribute: true})
-      positionBelow = false;
-
-      @property({type: Boolean})
-      _isTouchDevice = 'ontouchstart' in document.documentElement;
-
-      @property({type: Object})
-      _tooltip: GrTooltip | null = null;
-
-      @property({type: String})
-      _titleText = '';
-
-      @property({type: Boolean})
-      _hasSetupTooltipListeners = false;
-
-      // Handler for mouseenter event
-      private mouseenterHandler?: (e: MouseEvent) => void;
-
-      // Handler for scrolling on window
-      private readonly windowScrollHandler: () => void;
-
-      // Handler for showing the tooltip, will be attached to certain events
-      private readonly showHandler: () => void;
-
-      // Handler for hiding the tooltip, will be attached to certain events
-      private readonly hideHandler: (e: Event) => void;
-
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
-      constructor(..._: any[]) {
-        super();
-        this.windowScrollHandler = () => this._handleWindowScroll();
-        this.showHandler = () => this._handleShowTooltip();
-        this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
-      }
-
-      override disconnectedCallback() {
-        // NOTE: if you define your own `detached` in your component
-        // then this won't take affect (as its not a class yet)
-        this._handleHideTooltip(undefined);
-        if (this.mouseenterHandler) {
-          this.removeEventListener('mouseenter', this.mouseenterHandler);
-        }
-        window.removeEventListener('scroll', this.windowScrollHandler);
-        super.disconnectedCallback();
-      }
-
-      @observe('hasTooltip')
-      _setupTooltipListeners() {
-        if (!this.mouseenterHandler) {
-          this.mouseenterHandler = this.showHandler;
-        }
-
-        if (!this.hasTooltip) {
-          // if attribute set to false, remove the listener
-          this.removeEventListener('mouseenter', this.mouseenterHandler);
-          this._hasSetupTooltipListeners = false;
-          return;
-        }
-
-        if (this._hasSetupTooltipListeners) {
-          return;
-        }
-        this._hasSetupTooltipListeners = true;
-
-        this.addEventListener('mouseenter', this.mouseenterHandler);
-      }
-
-      _handleShowTooltip() {
-        if (this._isTouchDevice) {
-          return;
-        }
-
-        if (
-          !this.hasAttribute('title') ||
-          this.getAttribute('title') === '' ||
-          this._tooltip
-        ) {
-          return;
-        }
-
-        // Store the title attribute text then set it to an empty string to
-        // prevent it from showing natively.
-        this._titleText = this.getAttribute('title') || '';
-        this.setAttribute('title', '');
-
-        const tooltip = document.createElement('gr-tooltip');
-        tooltip.text = this._titleText;
-        tooltip.maxWidth = this.getAttribute('max-width') || '';
-        tooltip.positionBelow = this.hasAttribute('position-below');
-
-        // Set visibility to hidden before appending to the DOM so that
-        // calculations can be made based on the element’s size.
-        tooltip.style.visibility = 'hidden';
-        getRootElement().appendChild(tooltip);
-        this._positionTooltip(tooltip);
-        tooltip.style.visibility = 'initial';
-
-        this._tooltip = tooltip;
-        window.addEventListener('scroll', this.windowScrollHandler);
-        this.addEventListener('mouseleave', this.hideHandler);
-        this.addEventListener('click', this.hideHandler);
-        tooltip.addEventListener('mouseleave', this.hideHandler);
-      }
-
-      _handleHideTooltip(e: Event | undefined) {
-        if (this._isTouchDevice) {
-          return;
-        }
-        if (!this.hasAttribute('title') || !this._titleText) {
-          return;
-        }
-        // Do not hide if mouse left this or this._tooltip and came to this or
-        // this._tooltip
-        if (
-          (e as MouseEvent)?.relatedTarget === this._tooltip ||
-          (e as MouseEvent)?.relatedTarget === this
-        ) {
-          return;
-        }
-
-        window.removeEventListener('scroll', this.windowScrollHandler);
-        this.removeEventListener('mouseleave', this.hideHandler);
-        this.removeEventListener('click', this.hideHandler);
-        this.setAttribute('title', this._titleText);
-        this._tooltip?.removeEventListener('mouseleave', this.hideHandler);
-
-        if (this._tooltip?.parentNode) {
-          this._tooltip.parentNode.removeChild(this._tooltip);
-        }
-        this._tooltip = null;
-      }
-
-      _handleWindowScroll() {
-        if (!this._tooltip) {
-          return;
-        }
-
-        this._positionTooltip(this._tooltip);
-      }
-
-      _positionTooltip(tooltip: GrTooltip) {
-        // This flush is needed for tooltips to be positioned correctly in Firefox
-        // and Safari.
-        flush();
-        const rect = this.getBoundingClientRect();
-        const boxRect = tooltip.getBoundingClientRect();
-        if (!tooltip.parentElement) {
-          return;
-        }
-        const parentRect = tooltip.parentElement.getBoundingClientRect();
-        const top = rect.top - parentRect.top;
-        const left =
-          rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
-        const right = parentRect.width - left - boxRect.width;
-        if (left < 0) {
-          tooltip.updateStyles({
-            '--gr-tooltip-arrow-center-offset': `${left}px`,
-          });
-        } else if (right < 0) {
-          tooltip.updateStyles({
-            '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
-          });
-        }
-        tooltip.style.left = `${Math.max(0, left)}px`;
-
-        if (!this.positionBelow) {
-          tooltip.style.top = `${Math.max(0, top)}px`;
-          tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
-        } else {
-          tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
-        }
-      }
-    }
-
-    return Mixin;
-  }
-);
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
deleted file mode 100644
index 209c83af..0000000
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {TooltipMixin} from './gr-tooltip-mixin.js';
-
-const basicFixture = fixtureFromElement('gr-tooltip-mixin-element');
-
-class GrTooltipMixinTestElement extends TooltipMixin(PolymerElement) {
-  static get is() {
-    return 'gr-tooltip-mixin-element';
-  }
-}
-
-customElements.define(GrTooltipMixinTestElement.is,
-    GrTooltipMixinTestElement);
-
-suite('gr-tooltip-mixin tests', () => {
-  let element;
-
-  function makeTooltip(tooltipRect, parentRect) {
-    return {
-      getBoundingClientRect() { return tooltipRect; },
-      updateStyles: sinon.stub(),
-      style: {left: 0, top: 0},
-      parentElement: {
-        getBoundingClientRect() { return parentRect; },
-      },
-    };
-  }
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('normal position', () => {
-    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
-      return {top: 100, left: 100, width: 200};
-    });
-    const tooltip = makeTooltip(
-        {height: 30, width: 50},
-        {top: 0, left: 0, width: 1000});
-
-    element._positionTooltip(tooltip);
-    assert.isFalse(tooltip.updateStyles.called);
-    assert.equal(tooltip.style.left, '175px');
-    assert.equal(tooltip.style.top, '100px');
-  });
-
-  test('left side position', () => {
-    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
-      return {top: 100, left: 10, width: 50};
-    });
-    const tooltip = makeTooltip(
-        {height: 30, width: 120},
-        {top: 0, left: 0, width: 1000});
-
-    element._positionTooltip(tooltip);
-    assert.isTrue(tooltip.updateStyles.called);
-    const offset = tooltip.updateStyles
-        .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
-    assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
-    assert.equal(tooltip.style.left, '0px');
-    assert.equal(tooltip.style.top, '100px');
-  });
-
-  test('right side position', () => {
-    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
-      return {top: 100, left: 950, width: 50};
-    });
-    const tooltip = makeTooltip(
-        {height: 30, width: 120},
-        {top: 0, left: 0, width: 1000});
-
-    element._positionTooltip(tooltip);
-    assert.isTrue(tooltip.updateStyles.called);
-    const offset = tooltip.updateStyles
-        .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
-    assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
-    assert.equal(tooltip.style.left, '915px');
-    assert.equal(tooltip.style.top, '100px');
-  });
-
-  test('position to bottom', () => {
-    sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
-      return {top: 100, left: 950, width: 50, height: 50};
-    });
-    const tooltip = makeTooltip(
-        {height: 30, width: 120},
-        {top: 0, left: 0, width: 1000});
-
-    element.positionBelow = true;
-    element._positionTooltip(tooltip);
-    assert.isTrue(tooltip.updateStyles.called);
-    const offset = tooltip.updateStyles
-        .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
-    assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
-    assert.equal(tooltip.style.left, '915px');
-    assert.equal(tooltip.style.top, '157.2px');
-  });
-
-  test('hides tooltip when detached', () => {
-    sinon.stub(element, '_handleHideTooltip');
-    element.remove();
-    flush();
-    assert.isTrue(element._handleHideTooltip.called);
-  });
-
-  test('sets up listeners when has-tooltip is changed', () => {
-    const addListenerStub = sinon.stub(element, 'addEventListener');
-    element.hasTooltip = true;
-    assert.isTrue(addListenerStub.called);
-  });
-
-  test('clean up listeners when has-tooltip changed to false', () => {
-    const removeListenerStub = sinon.stub(element, 'removeEventListener');
-    element.hasTooltip = true;
-    element.hasTooltip = false;
-    assert.isTrue(removeListenerStub.called);
-  });
-});
-
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index c48b179..9092919 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -99,7 +99,6 @@
 import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior';
 import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class';
-import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin';
 import {property} from '@polymer/decorators';
 import {PolymerElement} from '@polymer/polymer';
 import {check, Constructor} from '../../utils/common-util';
@@ -151,6 +150,8 @@
   TOGGLE_CHANGE_REVIEWED = 'TOGGLE_CHANGE_REVIEWED',
   TOGGLE_CHANGE_STAR = 'TOGGLE_CHANGE_STAR',
   REFRESH_CHANGE_LIST = 'REFRESH_CHANGE_LIST',
+  OPEN_SUBMIT_DIALOG = 'OPEN_SUBMIT_DIALOG',
+  TOGGLE_ATTENTION_SET = 'TOGGLE_ATTENTION_SET',
 
   OPEN_REPLY_DIALOG = 'OPEN_REPLY_DIALOG',
   OPEN_DOWNLOAD_DIALOG = 'OPEN_DOWNLOAD_DIALOG',
@@ -329,6 +330,16 @@
   ShortcutSection.ACTIONS,
   'Star/unstar change'
 );
+_describe(
+  Shortcut.OPEN_SUBMIT_DIALOG,
+  ShortcutSection.ACTIONS,
+  'Open submit dialog'
+);
+_describe(
+  Shortcut.TOGGLE_ATTENTION_SET,
+  ShortcutSection.ACTIONS,
+  'Toggle attention set status'
+);
 _describe(Shortcut.EDIT_TOPIC, ShortcutSection.ACTIONS, 'Add a change topic');
 _describe(
   Shortcut.DIFF_AGAINST_BASE,
@@ -742,337 +753,335 @@
  * @polymer
  * @mixinFunction
  */
-const InternalKeyboardShortcutMixin = dedupingMixin(
-  <T extends Constructor<PolymerElement> & IronA11yKeysMixinConstructor>(
-    superClass: T
-  ): T & Constructor<KeyboardShortcutMixinInterface> => {
+const InternalKeyboardShortcutMixin = <
+  T extends Constructor<PolymerElement> & IronA11yKeysMixinConstructor
+>(
+  superClass: T
+) => {
+  /**
+   * @polymer
+   * @mixinClass
+   */
+  class Mixin extends superClass {
+    @property({type: Number})
+    _shortcut_go_key_last_pressed: number | null = null;
+
+    @property({type: Number})
+    _shortcut_v_key_last_pressed: number | null = null;
+
+    @property({type: Object})
+    _shortcut_go_table: Map<string, string> = new Map<string, string>();
+
+    @property({type: Object})
+    _shortcut_v_table: Map<string, string> = new Map<string, string>();
+
+    Shortcut = Shortcut;
+
+    ShortcutSection = ShortcutSection;
+
+    private _disableKeyboardShortcuts = false;
+
+    private readonly restApiService = appContext.restApiService;
+
+    private reporting = appContext.reportingService;
+
+    /** Used to disable shortcuts when the element is not visible. */
+    private observer?: IntersectionObserver;
+
     /**
-     * @polymer
-     * @mixinClass
+     * Enabling shortcuts only when the element is visible (see `observer`
+     * above) is a great feature, but often what you want is for the *page* to
+     * be visible, not the specific child element that registers keyboard
+     * shortcuts. An example is the FileList in the ChangeView. So we allow
+     * a broader observer target to be specified here, and fall back to
+     * `this` as the default.
      */
-    class Mixin extends superClass {
-      @property({type: Number})
-      _shortcut_go_key_last_pressed: number | null = null;
+    @property({type: Object})
+    observerTarget: Element = this;
 
-      @property({type: Number})
-      _shortcut_v_key_last_pressed: number | null = null;
+    /** Are shortcuts currently enabled? True only when element is visible. */
+    private bindingsEnabled = false;
 
-      @property({type: Object})
-      _shortcut_go_table: Map<string, string> = new Map<string, string>();
-
-      @property({type: Object})
-      _shortcut_v_table: Map<string, string> = new Map<string, string>();
-
-      Shortcut = Shortcut;
-
-      ShortcutSection = ShortcutSection;
-
-      private _disableKeyboardShortcuts = false;
-
-      private readonly restApiService = appContext.restApiService;
-
-      private reporting = appContext.reportingService;
-
-      /** Used to disable shortcuts when the element is not visible. */
-      private observer?: IntersectionObserver;
-
-      /**
-       * Enabling shortcuts only when the element is visible (see `observer`
-       * above) is a great feature, but often what you want is for the *page* to
-       * be visible, not the specific child element that registers keyboard
-       * shortcuts. An example is the FileList in the ChangeView. So we allow
-       * a broader observer target to be specified here, and fall back to
-       * `this` as the default.
+    modifierPressed(event: CustomKeyboardEvent) {
+      /* We are checking for g/v as modifiers pressed. There are cases such as
+       * pressing v and then /, where we want the handler for / to be triggered.
+       * TODO(dhruvsri): find a way to support that keyboard combination
        */
-      @property({type: Object})
-      observerTarget: Element = this;
+      const e = getKeyboardEvent(event);
+      return (
+        isModifierPressed(e) || !!this._inGoKeyMode() || !!this.inVKeyMode()
+      );
+    }
 
-      /** Are shortcuts currently enabled? True only when element is visible. */
-      private bindingsEnabled = false;
-
-      modifierPressed(event: CustomKeyboardEvent) {
-        /* We are checking for g/v as modifiers pressed. There are cases such as
-         * pressing v and then /, where we want the handler for / to be triggered.
-         * TODO(dhruvsri): find a way to support that keyboard combination
-         */
-        const e = getKeyboardEvent(event);
-        return (
-          isModifierPressed(e) || !!this._inGoKeyMode() || !!this.inVKeyMode()
-        );
+    shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent) {
+      if (this._disableKeyboardShortcuts) return true;
+      const e = getKeyboardEvent(event);
+      // TODO(TS): maybe override the EventApi, narrow it down to Element always
+      const target = (dom(e) as EventApi).rootTarget as Element;
+      const tagName = target.tagName;
+      const type = target.getAttribute('type');
+      if (
+        // Suppress shortcuts on <input> and <textarea>, but not on
+        // checkboxes, because we want to enable workflows like 'click
+        // mark-reviewed and then press ] to go to the next file'.
+        (tagName === 'INPUT' && type !== 'checkbox') ||
+        tagName === 'TEXTAREA' ||
+        // Suppress shortcuts if the key is 'enter'
+        // and target is an anchor or button or paper-tab.
+        (e.keyCode === 13 &&
+          (tagName === 'A' || tagName === 'BUTTON' || tagName === 'PAPER-TAB'))
+      ) {
+        return true;
       }
-
-      shouldSuppressKeyboardShortcut(event: CustomKeyboardEvent) {
-        if (this._disableKeyboardShortcuts) return true;
-        const e = getKeyboardEvent(event);
-        // TODO(TS): maybe override the EventApi, narrow it down to Element always
-        const target = (dom(e) as EventApi).rootTarget as Element;
-        const tagName = target.tagName;
-        const type = target.getAttribute('type');
-        if (
-          // Suppress shortcuts on <input> and <textarea>, but not on
-          // checkboxes, because we want to enable workflows like 'click
-          // mark-reviewed and then press ] to go to the next file'.
-          (tagName === 'INPUT' && type !== 'checkbox') ||
-          tagName === 'TEXTAREA' ||
-          // Suppress shortcuts if the key is 'enter'
-          // and target is an anchor or button or paper-tab.
-          (e.keyCode === 13 &&
-            (tagName === 'A' ||
-              tagName === 'BUTTON' ||
-              tagName === 'PAPER-TAB'))
-        ) {
+      for (let i = 0; e.path && i < e.path.length; i++) {
+        // TODO(TS): narrow this down to Element from EventTarget first
+        if ((e.path[i] as Element).tagName === 'GR-OVERLAY') {
           return true;
         }
-        for (let i = 0; e.path && i < e.path.length; i++) {
-          // TODO(TS): narrow this down to Element from EventTarget first
-          if ((e.path[i] as Element).tagName === 'GR-OVERLAY') {
-            return true;
-          }
-        }
-
-        // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
-        let key = `${(e as unknown as KeyboardEvent).key}:${e.type}`;
-        if (this._inGoKeyMode()) key = 'g+' + key;
-        if (this.inVKeyMode()) key = 'v+' + key;
-        if (e.shiftKey) key = 'shift+' + key;
-        if (e.ctrlKey) key = 'ctrl+' + key;
-        if (e.metaKey) key = 'meta+' + key;
-        if (e.altKey) key = 'alt+' + key;
-        this.reporting.reportInteraction('shortcut-triggered', {
-          key,
-          from: this.nodeName ?? 'unknown',
-        });
-        return false;
       }
 
-      // Alias for getKeyboardEvent.
-      getKeyboardEvent(e: CustomKeyboardEvent) {
-        return getKeyboardEvent(e);
+      // eg: {key: "k:keydown", ..., from: "gr-diff-view"}
+      let key = `${(e as unknown as KeyboardEvent).key}:${e.type}`;
+      if (this._inGoKeyMode()) key = 'g+' + key;
+      if (this.inVKeyMode()) key = 'v+' + key;
+      if (e.shiftKey) key = 'shift+' + key;
+      if (e.ctrlKey) key = 'ctrl+' + key;
+      if (e.metaKey) key = 'meta+' + key;
+      if (e.altKey) key = 'alt+' + key;
+      this.reporting.reportInteraction('shortcut-triggered', {
+        key,
+        from: this.nodeName ?? 'unknown',
+      });
+      return false;
+    }
+
+    // Alias for getKeyboardEvent.
+    getKeyboardEvent(e: CustomKeyboardEvent) {
+      return getKeyboardEvent(e);
+    }
+
+    bindShortcut(shortcut: Shortcut, ...bindings: string[]) {
+      shortcutManager.bindShortcut(shortcut, ...bindings);
+    }
+
+    createTitle(shortcutName: Shortcut, section: ShortcutSection) {
+      const desc = shortcutManager.getDescription(section, shortcutName);
+      const shortcut = shortcutManager.getShortcut(shortcutName);
+      return desc && shortcut ? `${desc} (shortcut: ${shortcut})` : '';
+    }
+
+    _addOwnKeyBindings(shortcut: Shortcut, handler: string) {
+      const bindings = shortcutManager.getBindingsForShortcut(shortcut);
+      if (!bindings) {
+        return;
       }
-
-      bindShortcut(shortcut: Shortcut, ...bindings: string[]) {
-        shortcutManager.bindShortcut(shortcut, ...bindings);
+      if (bindings[0] === SPECIAL_SHORTCUT.DOC_ONLY) {
+        return;
       }
-
-      createTitle(shortcutName: Shortcut, section: ShortcutSection) {
-        const desc = shortcutManager.getDescription(section, shortcutName);
-        const shortcut = shortcutManager.getShortcut(shortcutName);
-        return desc && shortcut ? `${desc} (shortcut: ${shortcut})` : '';
-      }
-
-      _addOwnKeyBindings(shortcut: Shortcut, handler: string) {
-        const bindings = shortcutManager.getBindingsForShortcut(shortcut);
-        if (!bindings) {
-          return;
-        }
-        if (bindings[0] === SPECIAL_SHORTCUT.DOC_ONLY) {
-          return;
-        }
-        if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
-          bindings
-            .slice(1)
-            .forEach(binding => this._shortcut_go_table.set(binding, handler));
-        } else if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
-          // for each binding added with the go/v key, we set the handler to be
-          // handleVKeyAction. handleVKeyAction then looks up in th
-          // shortcut_table to see what the relevant handler should be
-          bindings
-            .slice(1)
-            .forEach(binding => this._shortcut_v_table.set(binding, handler));
-        } else {
-          this.addOwnKeyBinding(bindings.join(' '), handler);
-        }
-      }
-
-      override connectedCallback() {
-        super.connectedCallback();
-        this.restApiService.getPreferences().then(prefs => {
-          if (prefs?.disable_keyboard_shortcuts) {
-            this._disableKeyboardShortcuts = true;
-          }
-        });
-        this.createVisibilityObserver();
-        this.enableBindings();
-      }
-
-      override disconnectedCallback() {
-        this.destroyVisibilityObserver();
-        this.disableBindings();
-        super.disconnectedCallback();
-      }
-
-      /**
-       * Creates an intersection observer that enables bindings when the
-       * element is visible and disables them when the element is hidden.
-       */
-      private createVisibilityObserver() {
-        if (!this.hasKeyboardShortcuts()) return;
-        if (this.observer) return;
-        this.observer = new IntersectionObserver(entries => {
-          check(entries.length === 1, 'Expected one observer entry.');
-          const isVisible = entries[0].isIntersecting;
-          if (isVisible) {
-            this.enableBindings();
-          } else {
-            this.disableBindings();
-          }
-        });
-        this.observer.observe(this.observerTarget);
-      }
-
-      private destroyVisibilityObserver() {
-        if (this.observer) this.observer.unobserve(this.observerTarget);
-      }
-
-      /**
-       * Enables all the shortcuts returned by keyboardShortcuts().
-       * This is a private method being called when the element becomes
-       * connected or visible.
-       */
-      private enableBindings() {
-        if (!this.hasKeyboardShortcuts()) return;
-        if (this.bindingsEnabled) return;
-        this.bindingsEnabled = true;
-
-        const shortcuts = new Map<string, string>(
-          Object.entries(this.keyboardShortcuts())
-        );
-        shortcutManager.attachHost(this, shortcuts);
-
-        for (const [key, value] of shortcuts.entries()) {
-          this._addOwnKeyBindings(key as Shortcut, value);
-        }
-
-        // each component that uses this behaviour must be aware if go key is
-        // pressed or not, since it needs to check it as a modifier
-        this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown');
-        this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp');
-
-        // If any of the shortcuts utilized GO_KEY, then they are handled
-        // directly by this behavior.
-        if (this._shortcut_go_table.size > 0) {
-          this._shortcut_go_table.forEach((_, key) => {
-            this.addOwnKeyBinding(key, '_handleGoAction');
-          });
-        }
-
-        this.addOwnKeyBinding('v:keydown', '_handleVKeyDown');
-        this.addOwnKeyBinding('v:keyup', '_handleVKeyUp');
-        if (this._shortcut_v_table.size > 0) {
-          this._shortcut_v_table.forEach((_, key) => {
-            this.addOwnKeyBinding(key, '_handleVAction');
-          });
-        }
-      }
-
-      /**
-       * Disables all the shortcuts returned by keyboardShortcuts().
-       * This is a private method being called when the element becomes
-       * disconnected or invisible.
-       */
-      private disableBindings() {
-        if (!this.bindingsEnabled) return;
-        this.bindingsEnabled = false;
-        if (shortcutManager.detachHost(this)) {
-          this.removeOwnKeyBindings();
-        }
-      }
-
-      private hasKeyboardShortcuts() {
-        return Object.entries(this.keyboardShortcuts()).length > 0;
-      }
-
-      keyboardShortcuts() {
-        return {};
-      }
-
-      addKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
-        shortcutManager.addListener(listener);
-      }
-
-      removeKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
-        shortcutManager.removeListener(listener);
-      }
-
-      _handleVKeyDown(e: CustomKeyboardEvent) {
-        if (this.shouldSuppressKeyboardShortcut(e)) return;
-        this._shortcut_v_key_last_pressed = Date.now();
-      }
-
-      _handleVKeyUp() {
-        setTimeout(() => {
-          this._shortcut_v_key_last_pressed = null;
-        }, V_KEY_TIMEOUT_MS);
-      }
-
-      private inVKeyMode() {
-        return !!(
-          this._shortcut_v_key_last_pressed &&
-          Date.now() - this._shortcut_v_key_last_pressed <= V_KEY_TIMEOUT_MS
-        );
-      }
-
-      _handleVAction(e: CustomKeyboardEvent) {
-        if (
-          !this.inVKeyMode() ||
-          !this._shortcut_v_table.has(e.detail.key) ||
-          this.shouldSuppressKeyboardShortcut(e)
-        ) {
-          return;
-        }
-        e.preventDefault();
-        const handler = this._shortcut_v_table.get(e.detail.key);
-        if (handler) {
-          // TODO(TS): should fix this
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-          (this as any)[handler](e);
-        }
-      }
-
-      _handleGoKeyDown(e: CustomKeyboardEvent) {
-        if (this.shouldSuppressKeyboardShortcut(e)) return;
-        this._shortcut_go_key_last_pressed = Date.now();
-      }
-
-      _handleGoKeyUp() {
-        // Set go_key_last_pressed to null `GO_KEY_TIMEOUT_MS` after keyup event
-        // so that users can trigger `g + i` by pressing g and i quickly.
-        setTimeout(() => {
-          this._shortcut_go_key_last_pressed = null;
-        }, GO_KEY_TIMEOUT_MS);
-      }
-
-      _inGoKeyMode() {
-        return !!(
-          this._shortcut_go_key_last_pressed &&
-          Date.now() - this._shortcut_go_key_last_pressed <= GO_KEY_TIMEOUT_MS
-        );
-      }
-
-      _handleGoAction(e: CustomKeyboardEvent) {
-        if (
-          !this._inGoKeyMode() ||
-          !this._shortcut_go_table.has(e.detail.key) ||
-          this.shouldSuppressKeyboardShortcut(e)
-        ) {
-          return;
-        }
-        e.preventDefault();
-        const handler = this._shortcut_go_table.get(e.detail.key);
-        if (handler) {
-          // TODO(TS): should fix this
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-          (this as any)[handler](e);
-        }
+      if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
+        bindings
+          .slice(1)
+          .forEach(binding => this._shortcut_go_table.set(binding, handler));
+      } else if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
+        // for each binding added with the go/v key, we set the handler to be
+        // handleVKeyAction. handleVKeyAction then looks up in th
+        // shortcut_table to see what the relevant handler should be
+        bindings
+          .slice(1)
+          .forEach(binding => this._shortcut_v_table.set(binding, handler));
+      } else {
+        this.addOwnKeyBinding(bindings.join(' '), handler);
       }
     }
 
-    return Mixin;
+    override connectedCallback() {
+      super.connectedCallback();
+      this.restApiService.getPreferences().then(prefs => {
+        if (prefs?.disable_keyboard_shortcuts) {
+          this._disableKeyboardShortcuts = true;
+        }
+      });
+      this.createVisibilityObserver();
+      this.enableBindings();
+    }
+
+    override disconnectedCallback() {
+      this.destroyVisibilityObserver();
+      this.disableBindings();
+      super.disconnectedCallback();
+    }
+
+    /**
+     * Creates an intersection observer that enables bindings when the
+     * element is visible and disables them when the element is hidden.
+     */
+    private createVisibilityObserver() {
+      if (!this.hasKeyboardShortcuts()) return;
+      if (this.observer) return;
+      this.observer = new IntersectionObserver(entries => {
+        check(entries.length === 1, 'Expected one observer entry.');
+        const isVisible = entries[0].isIntersecting;
+        if (isVisible) {
+          this.enableBindings();
+        } else {
+          this.disableBindings();
+        }
+      });
+      this.observer.observe(this.observerTarget);
+    }
+
+    private destroyVisibilityObserver() {
+      if (this.observer) this.observer.unobserve(this.observerTarget);
+    }
+
+    /**
+     * Enables all the shortcuts returned by keyboardShortcuts().
+     * This is a private method being called when the element becomes
+     * connected or visible.
+     */
+    private enableBindings() {
+      if (!this.hasKeyboardShortcuts()) return;
+      if (this.bindingsEnabled) return;
+      this.bindingsEnabled = true;
+
+      const shortcuts = new Map<string, string>(
+        Object.entries(this.keyboardShortcuts())
+      );
+      shortcutManager.attachHost(this, shortcuts);
+
+      for (const [key, value] of shortcuts.entries()) {
+        this._addOwnKeyBindings(key as Shortcut, value);
+      }
+
+      // each component that uses this behaviour must be aware if go key is
+      // pressed or not, since it needs to check it as a modifier
+      this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown');
+      this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp');
+
+      // If any of the shortcuts utilized GO_KEY, then they are handled
+      // directly by this behavior.
+      if (this._shortcut_go_table.size > 0) {
+        this._shortcut_go_table.forEach((_, key) => {
+          this.addOwnKeyBinding(key, '_handleGoAction');
+        });
+      }
+
+      this.addOwnKeyBinding('v:keydown', '_handleVKeyDown');
+      this.addOwnKeyBinding('v:keyup', '_handleVKeyUp');
+      if (this._shortcut_v_table.size > 0) {
+        this._shortcut_v_table.forEach((_, key) => {
+          this.addOwnKeyBinding(key, '_handleVAction');
+        });
+      }
+    }
+
+    /**
+     * Disables all the shortcuts returned by keyboardShortcuts().
+     * This is a private method being called when the element becomes
+     * disconnected or invisible.
+     */
+    private disableBindings() {
+      if (!this.bindingsEnabled) return;
+      this.bindingsEnabled = false;
+      if (shortcutManager.detachHost(this)) {
+        this.removeOwnKeyBindings();
+      }
+    }
+
+    private hasKeyboardShortcuts() {
+      return Object.entries(this.keyboardShortcuts()).length > 0;
+    }
+
+    keyboardShortcuts() {
+      return {};
+    }
+
+    addKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
+      shortcutManager.addListener(listener);
+    }
+
+    removeKeyboardShortcutDirectoryListener(listener: ShortcutListener) {
+      shortcutManager.removeListener(listener);
+    }
+
+    _handleVKeyDown(e: CustomKeyboardEvent) {
+      if (this.shouldSuppressKeyboardShortcut(e)) return;
+      this._shortcut_v_key_last_pressed = Date.now();
+    }
+
+    _handleVKeyUp() {
+      setTimeout(() => {
+        this._shortcut_v_key_last_pressed = null;
+      }, V_KEY_TIMEOUT_MS);
+    }
+
+    private inVKeyMode() {
+      return !!(
+        this._shortcut_v_key_last_pressed &&
+        Date.now() - this._shortcut_v_key_last_pressed <= V_KEY_TIMEOUT_MS
+      );
+    }
+
+    _handleVAction(e: CustomKeyboardEvent) {
+      if (
+        !this.inVKeyMode() ||
+        !this._shortcut_v_table.has(e.detail.key) ||
+        this.shouldSuppressKeyboardShortcut(e)
+      ) {
+        return;
+      }
+      e.preventDefault();
+      const handler = this._shortcut_v_table.get(e.detail.key);
+      if (handler) {
+        // TODO(TS): should fix this
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        (this as any)[handler](e);
+      }
+    }
+
+    _handleGoKeyDown(e: CustomKeyboardEvent) {
+      if (this.shouldSuppressKeyboardShortcut(e)) return;
+      this._shortcut_go_key_last_pressed = Date.now();
+    }
+
+    _handleGoKeyUp() {
+      // Set go_key_last_pressed to null `GO_KEY_TIMEOUT_MS` after keyup event
+      // so that users can trigger `g + i` by pressing g and i quickly.
+      setTimeout(() => {
+        this._shortcut_go_key_last_pressed = null;
+      }, GO_KEY_TIMEOUT_MS);
+    }
+
+    _inGoKeyMode() {
+      return !!(
+        this._shortcut_go_key_last_pressed &&
+        Date.now() - this._shortcut_go_key_last_pressed <= GO_KEY_TIMEOUT_MS
+      );
+    }
+
+    _handleGoAction(e: CustomKeyboardEvent) {
+      if (
+        !this._inGoKeyMode() ||
+        !this._shortcut_go_table.has(e.detail.key) ||
+        this.shouldSuppressKeyboardShortcut(e)
+      ) {
+        return;
+      }
+      e.preventDefault();
+      const handler = this._shortcut_go_table.get(e.detail.key);
+      if (handler) {
+        // TODO(TS): should fix this
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        (this as any)[handler](e);
+      }
+    }
   }
-);
+
+  return Mixin as T & Constructor<KeyboardShortcutMixinInterface>;
+};
 
 // The following doesn't work (IronA11yKeysBehavior crashes):
-// const KeyboardShortcutMixin = dedupingMixin(superClass => {
+// const KeyboardShortcutMixin = superClass => {
 //    class Mixin extends mixinBehaviors([IronA11yKeysBehavior], superClass) {
 //    ...
 //    }
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
index d5e7fe7..4536ecd 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
@@ -22,6 +22,7 @@
 } from './keyboard-shortcut-mixin.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {mockPromise} from '../../test/test-utils.js';
 
 const basicFixture =
     fixtureFromElement('keyboard-shortcut-mixin-test-element');
@@ -271,69 +272,81 @@
     });
   });
 
-  test('doesn’t block kb shortcuts for non-allowed els', done => {
+  test('doesn’t block kb shortcuts for non-allowed els', async () => {
     const divEl = document.createElement('div');
     element.appendChild(divEl);
+    const promise = mockPromise();
     element._handleKey = e => {
       assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
-      done();
+      promise.resolve();
     };
     MockInteractions.keyDownOn(divEl, 75, null, 'k');
+    await promise;
   });
 
-  test('blocks kb shortcuts for input els', done => {
+  test('blocks kb shortcuts for input els', async () => {
     const inputEl = document.createElement('input');
     element.appendChild(inputEl);
+    const promise = mockPromise();
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
-      done();
+      promise.resolve();
     };
     MockInteractions.keyDownOn(inputEl, 75, null, 'k');
+    await promise;
   });
 
-  test('doesn’t block kb shortcuts for checkboxes', done => {
+  test('doesn’t block kb shortcuts for checkboxes', async () => {
     const inputEl = document.createElement('input');
     inputEl.setAttribute('type', 'checkbox');
     element.appendChild(inputEl);
+    const promise = mockPromise();
     element._handleKey = e => {
       assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
-      done();
+      promise.resolve();
     };
     MockInteractions.keyDownOn(inputEl, 75, null, 'k');
+    await promise;
   });
 
-  test('blocks kb shortcuts for textarea els', done => {
+  test('blocks kb shortcuts for textarea els', async () => {
     const textareaEl = document.createElement('textarea');
     element.appendChild(textareaEl);
+    const promise = mockPromise();
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
-      done();
+      promise.resolve();
     };
     MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
+    await promise;
   });
 
-  test('blocks kb shortcuts for anything in a gr-overlay', done => {
+  test('blocks kb shortcuts for anything in a gr-overlay', async () => {
     const divEl = document.createElement('div');
     const element =
         overlay.querySelector('keyboard-shortcut-mixin-test-element');
     element.appendChild(divEl);
+    const promise = mockPromise();
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
-      done();
+      promise.resolve();
     };
     MockInteractions.keyDownOn(divEl, 75, null, 'k');
+    await promise;
   });
 
-  test('blocks enter shortcut on an anchor', done => {
+  test('blocks enter shortcut on an anchor', async () => {
     const anchorEl = document.createElement('a');
     const element =
         overlay.querySelector('keyboard-shortcut-mixin-test-element');
     element.appendChild(anchorEl);
+    const promise = mockPromise();
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
-      done();
+      promise.resolve();
     };
     MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
+    await promise;
   });
 
   test('modifierPressed returns accurate values', () => {
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index bcdab0e..ede84ff 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -45,6 +45,12 @@
 /** List of licenses texts. Add the licenses here if there is no text file with license
  * in package. For details - see comments for {@link LicenseInfo} and {@link PackageInfo} */
 class SharedLicenses {
+  public static Lit: LicenseInfo = {
+    name: "Lit",
+    type: LicenseTypes.Bsd3,
+    sharedLicenseFile: "lit.txt",
+  };
+
   public static Polymer2014: LicenseInfo = {
     name: "Polymer-2014",
     type: LicenseTypes.Bsd3,
@@ -97,6 +103,10 @@
 
 const packages: PackageInfo[] = [
   {
+    name: "@lit/reactive-element",
+    license: SharedLicenses.Lit,
+  },
+  {
     name: "@polymer/decorators",
     license: SharedLicenses.Polymer2017,
   },
@@ -304,6 +314,14 @@
     }
   },
   {
+    name: "@types/trusted-types",
+    license: {
+      name: 'DefinitelyTyped',
+      type: LicenseTypes.Mit,
+      packageLicenseFile: "LICENSE"
+    }
+  },
+  {
     name: "@webcomponents/shadycss",
     license: SharedLicenses.Polymer2017
   },
@@ -373,20 +391,16 @@
       "src/operators", "src/testing", "src/webSocket"],
   },
   {
+    name: "lit",
+    license: SharedLicenses.Lit,
+  },
+  {
     name: "lit-element",
-    license: {
-      name: "lit-element",
-      type: LicenseTypes.Bsd3,
-      packageLicenseFile: "LICENSE"
-    },
+    license: SharedLicenses.Lit,
   },
   {
     name: "lit-html",
-    license: {
-      name: "lit-html",
-      type: LicenseTypes.Bsd3,
-      packageLicenseFile: "LICENSE"
-    },
+    license: SharedLicenses.Lit,
   },
   {
     name: "tslib",
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses/lit.txt b/polygerrit-ui/app/node_modules_licenses/licenses/lit.txt
new file mode 100644
index 0000000..c8ed226
--- /dev/null
+++ b/polygerrit-ui/app/node_modules_licenses/licenses/lit.txt
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2017 Google LLC. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/polygerrit-ui/app/node_modules_licenses/tsconfig.json b/polygerrit-ui/app/node_modules_licenses/tsconfig.json
index c562a0c..f0a540b 100644
--- a/polygerrit-ui/app/node_modules_licenses/tsconfig.json
+++ b/polygerrit-ui/app/node_modules_licenses/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es6",
-    "module": "commonjs",
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index a2e9c32..4be6241 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -38,7 +38,7 @@
     "ba-linkify": "^1.0.1",
     "codemirror-minified": "^5.62.2",
     "immer": "^9.0.5",
-    "lit-element": "^2.5.1",
+    "lit": "2.0.0-rc.3",
     "page": "^1.11.6",
     "polymer-bridges": "file:../../polymer-bridges/",
     "polymer-resin": "^2.0.1",
diff --git a/polygerrit-ui/app/polymer.json b/polygerrit-ui/app/polymer.json
index 02ffd26..4348ba8 100644
--- a/polygerrit-ui/app/polymer.json
+++ b/polygerrit-ui/app/polymer.json
@@ -9,10 +9,13 @@
   ],
   "lint": {
     "rules": ["polymer-3"],
-    "ignoreWarnings": ["deprecated-dom-call"],
+    "ignoreWarnings": [
+      "deprecated-dom-call",
+      "multiple-global-declarations"
+    ],
     "filesToIgnore": [
-        "**/gr-plugin-rest-api.js",
-        "**/.cache/**/gr-plugin-rest-api.js"
+      "**/gr-plugin-rest-api.js",
+      "**/.cache/**/gr-plugin-rest-api.js"
     ]
   }
 }
diff --git a/polygerrit-ui/app/samples/repo-command.js b/polygerrit-ui/app/samples/repo-command.js
index acecd7d..6abb120 100644
--- a/polygerrit-ui/app/samples/repo-command.js
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -32,7 +32,7 @@
         margin-bottom: var(--spacing-xxl);
       }
       </style>
-      <h3>Plugin Bork</h3>
+      <h3 class="heading-3">Plugin Bork</h3>
       <gr-button on-click="_handleCommandTap">Bork</gr-button>
     `;
   }
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
index 66681ee..989bafb 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
@@ -35,17 +35,15 @@
     provider = new GrEmailSuggestionsProvider(appContext.restApiService);
   });
 
-  test('getSuggestions', done => {
+  test('getSuggestions', async () => {
     const getSuggestedAccountsStub =
         stubRestApi('getSuggestedAccounts').returns(
             Promise.resolve([account1, account2]));
 
-    provider.getSuggestions('Some input').then(res => {
-      assert.deepEqual(res, [account1, account2]);
-      assert.isTrue(getSuggestedAccountsStub.calledOnce);
-      assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
-      done();
-    });
+    const res = await provider.getSuggestions('Some input');
+    assert.deepEqual(res, [account1, account2]);
+    assert.isTrue(getSuggestedAccountsStub.calledOnce);
+    assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
   });
 
   test('makeSuggestionItem', () => {
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
index 1a14abf..67f9433 100644
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
@@ -36,7 +36,7 @@
     provider = new GrGroupSuggestionsProvider(appContext.restApiService);
   });
 
-  test('getSuggestions', done => {
+  test('getSuggestions', async () => {
     const getSuggestedAccountsStub =
         stubRestApi('getSuggestedGroups')
             .returns(Promise.resolve({
@@ -44,12 +44,10 @@
               'Other name': {id: 3, url: 'abcd'},
             }));
 
-    provider.getSuggestions('Some input').then(res => {
-      assert.deepEqual(res, [group1, group2]);
-      assert.isTrue(getSuggestedAccountsStub.calledOnce);
-      assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
-      done();
-    });
+    const res = await provider.getSuggestions('Some input');
+    assert.deepEqual(res, [group1, group2]);
+    assert.isTrue(getSuggestedAccountsStub.calledOnce);
+    assert.equal(getSuggestedAccountsStub.lastCall.args[0], 'Some input');
   });
 
   test('makeSuggestionItem', () => {
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
index d3cad45..762d36c 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
@@ -54,7 +54,7 @@
   let redundantSuggestion3;
   let change;
 
-  setup(done => {
+  setup(async () => {
     owner = makeAccount();
     existingReviewer1 = makeAccount();
     existingReviewer2 = makeAccount();
@@ -78,15 +78,15 @@
       },
     };
 
-    return flush(done);
+    await flush();
   });
 
   suite('allowAnyUser set to false', () => {
-    setup(done => {
+    setup(async () => {
       provider = GrReviewerSuggestionsProvider.create(
           appContext.restApiService, change._number,
           SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
-      provider.init().then(done);
+      await provider.init();
     });
     suite('stubbed values for _getReviewerSuggestions', () => {
       let getChangeSuggestedReviewersStub;
@@ -165,17 +165,15 @@
         });
       });
 
-      test('getSuggestions', done => {
-        provider.getSuggestions()
-            .then(reviewers => {
-              // Default is no filtering.
-              assert.equal(reviewers.length, 6);
-              assert.deepEqual(reviewers,
-                  [redundantSuggestion1, redundantSuggestion2,
-                    redundantSuggestion3, suggestion1,
-                    suggestion2, suggestion3]);
-            })
-            .then(done);
+      test('getSuggestions', async () => {
+        const reviewers = await provider.getSuggestions();
+
+        // Default is no filtering.
+        assert.equal(reviewers.length, 6);
+        assert.deepEqual(reviewers,
+            [redundantSuggestion1, redundantSuggestion2,
+              redundantSuggestion3, suggestion1,
+              suggestion2, suggestion3]);
       });
 
       test('getSuggestions short circuits when logged out', () => {
@@ -190,41 +188,37 @@
       });
     });
 
-    test('getChangeSuggestedReviewers is used', done => {
+    test('getChangeSuggestedReviewers is used', async () => {
       const suggestReviewerStub = stubRestApi('getChangeSuggestedReviewers')
           .returns(Promise.resolve([]));
       const suggestAccountStub = stubRestApi('getSuggestedAccounts')
           .returns(Promise.resolve([]));
 
-      provider.getSuggestions('').then(() => {
-        assert.isTrue(suggestReviewerStub.calledOnce);
-        assert.isTrue(suggestReviewerStub.calledWith(42, ''));
-        assert.isFalse(suggestAccountStub.called);
-        done();
-      });
+      await provider.getSuggestions('');
+      assert.isTrue(suggestReviewerStub.calledOnce);
+      assert.isTrue(suggestReviewerStub.calledWith(42, ''));
+      assert.isFalse(suggestAccountStub.called);
     });
   });
 
   suite('allowAnyUser set to true', () => {
-    setup(done => {
+    setup(async () => {
       provider = GrReviewerSuggestionsProvider.create(
           appContext.restApiService, change._number,
           SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
-      provider.init().then(done);
+      await provider.init();
     });
 
-    test('getSuggestedAccounts is used', done => {
+    test('getSuggestedAccounts is used', async () => {
       const suggestReviewerStub = stubRestApi('getChangeSuggestedReviewers')
           .returns(Promise.resolve([]));
       const suggestAccountStub = stubRestApi('getSuggestedAccounts')
           .returns(Promise.resolve([]));
 
-      provider.getSuggestions('').then(() => {
-        assert.isFalse(suggestReviewerStub.called);
-        assert.isTrue(suggestAccountStub.calledOnce);
-        assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
-        done();
-      });
+      await provider.getSuggestions('');
+      assert.isFalse(suggestReviewerStub.called);
+      assert.isTrue(suggestAccountStub.calledOnce);
+      assert.isTrue(suggestAccountStub.calledWith('cansee:42 '));
     });
   });
 });
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index f55631b..75c24b6 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -30,6 +30,7 @@
 import {PatchSetNumber} from '../../types/common';
 import {AttemptDetail, createAttemptMap} from './checks-util';
 import {assertIsDefined} from '../../utils/common-util';
+import {deepEqualStringDict, equalArray} from '../../utils/compare-util';
 
 /**
  * The checks model maintains the state of checks for two patchsets: the latest
@@ -85,6 +86,13 @@
 interface ChecksProviderState {
   pluginName: string;
   loading: boolean;
+  /**
+   * Allows to distinguish whether loading:true is the *first* time of loading
+   * something for this provider. Or just a subsequent background update.
+   * Note that this is initially true even before loading is being set to true,
+   * so you may want to check loading && firstTimeLoad.
+   */
+  firstTimeLoad: boolean;
   /** Presence of errorMessage implicitly means that the provider is in ERROR state. */
   errorMessage?: string;
   /** Presence of loginCallback implicitly means that the provider is in NOT_LOGGED_IN state. */
@@ -160,6 +168,15 @@
   distinctUntilChanged()
 );
 
+export const someProvidersAreLoadingFirstTime$ = checksLatest$.pipe(
+  map(state =>
+    Object.values(state).some(
+      provider => provider.loading && provider.firstTimeLoad
+    )
+  ),
+  distinctUntilChanged()
+);
+
 export const someProvidersAreLoadingLatest$ = checksLatest$.pipe(
   map(state =>
     Object.values(state).some(providerState => providerState.loading)
@@ -184,6 +201,23 @@
   distinctUntilChanged()
 );
 
+export interface ErrorMessages {
+  /* Maps plugin name to error message. */
+  [name: string]: string;
+}
+
+export const errorMessagesLatest$ = checksLatest$.pipe(
+  map(state => {
+    const errorMessages: ErrorMessages = {};
+    for (const providerState of Object.values(state)) {
+      if (providerState.errorMessage === undefined) continue;
+      errorMessages[providerState.pluginName] = providerState.errorMessage;
+    }
+    return errorMessages;
+  }),
+  distinctUntilChanged(deepEqualStringDict)
+);
+
 export const loginCallbackLatest$ = checksLatest$.pipe(
   map(
     state =>
@@ -194,6 +228,19 @@
   distinctUntilChanged()
 );
 
+export const topLevelActionsLatest$ = checksLatest$.pipe(
+  map(state =>
+    Object.values(state).reduce(
+      (allActions: Action[], providerState: ChecksProviderState) => [
+        ...allActions,
+        ...providerState.actions,
+      ],
+      []
+    )
+  ),
+  distinctUntilChanged<Action[]>(equalArray)
+);
+
 export const topLevelActionsSelected$ = checksSelected$.pipe(
   map(state =>
     Object.values(state).reduce(
@@ -203,19 +250,21 @@
       ],
       []
     )
-  )
+  ),
+  distinctUntilChanged<Action[]>(equalArray)
 );
 
 export const topLevelLinksSelected$ = checksSelected$.pipe(
   map(state =>
     Object.values(state).reduce(
-      (allActions: Link[], providerState: ChecksProviderState) => [
-        ...allActions,
+      (allLinks: Link[], providerState: ChecksProviderState) => [
+        ...allLinks,
         ...providerState.links,
       ],
       []
     )
-  )
+  ),
+  distinctUntilChanged<Link[]>(equalArray)
 );
 
 export const allRunsLatestPatchset$ = checksLatest$.pipe(
@@ -227,7 +276,8 @@
       ],
       []
     )
-  )
+  ),
+  distinctUntilChanged<CheckRun[]>(equalArray)
 );
 
 export const allRunsSelectedPatchset$ = checksSelected$.pipe(
@@ -239,7 +289,8 @@
       ],
       []
     )
-  )
+  ),
+  distinctUntilChanged<CheckRun[]>(equalArray)
 );
 
 export const allRunsLatestPatchsetLatestAttempt$ = allRunsLatestPatchset$.pipe(
@@ -287,6 +338,7 @@
   pluginState[pluginName] = {
     pluginName,
     loading: false,
+    firstTimeLoad: true,
     runs: [],
     actions: [],
     links: [],
@@ -301,7 +353,7 @@
 export const fakeRun0: CheckRun = {
   pluginName: 'f0',
   internalRunId: 'f0',
-  checkName: 'FAKE Error Finder',
+  checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder',
   labelName: 'Presubmit',
   isSingleAttempt: true,
   isLatestAttempt: true,
@@ -493,8 +545,8 @@
 export const fakeRun4_1: CheckRun = {
   pluginName: 'f4',
   internalRunId: 'f4',
-  checkName: 'FAKE Elimination',
-  status: RunStatus.COMPLETED,
+  checkName: 'FAKE Elimination Long Long Long Long Long',
+  status: RunStatus.RUNNABLE,
   attempt: 1,
   isSingleAttempt: false,
   isLatestAttempt: false,
@@ -504,7 +556,7 @@
 export const fakeRun4_2: CheckRun = {
   pluginName: 'f4',
   internalRunId: 'f4',
-  checkName: 'FAKE Elimination',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
   status: RunStatus.COMPLETED,
   attempt: 2,
   isSingleAttempt: false,
@@ -522,7 +574,7 @@
 export const fakeRun4_3: CheckRun = {
   pluginName: 'f4',
   internalRunId: 'f4',
-  checkName: 'FAKE Elimination',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
   status: RunStatus.COMPLETED,
   attempt: 3,
   isSingleAttempt: false,
@@ -540,13 +592,13 @@
 export const fakeRun4_4: CheckRun = {
   pluginName: 'f4',
   internalRunId: 'f4',
-  checkName: 'FAKE Elimination',
+  checkName: 'FAKE Elimination Long Long Long Long Long',
   checkDescription: 'Shows you the possible eliminations.',
   checkLink: 'https://www.google.com',
-  status: RunStatus.RUNNING,
+  status: RunStatus.COMPLETED,
   statusDescription: 'Everything was eliminated already.',
   statusLink: 'https://www.google.com',
-  attempt: 4,
+  attempt: 40,
   scheduledTimestamp: new Date('2021-04-02T03:14:15'),
   startedTimestamp: new Date('2021-04-02T04:24:25'),
   finishedTimestamp: new Date('2021-04-02T04:25:44'),
@@ -568,8 +620,56 @@
       ],
     },
   ],
+  actions: [
+    {
+      name: 'Re-Run',
+      tooltip: 'small',
+      primary: true,
+      callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+    },
+  ],
 };
 
+export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] {
+  const runs: CheckRun[] = [];
+  for (let i = from; i < to; i++) {
+    runs.push(fakeRun4CreateAttempt(i));
+  }
+  return runs;
+}
+
+export function fakeRun4CreateAttempt(attempt: number): CheckRun {
+  return {
+    pluginName: 'f4',
+    internalRunId: 'f4',
+    checkName: 'FAKE Elimination Long Long Long Long Long',
+    status: RunStatus.COMPLETED,
+    attempt,
+    isSingleAttempt: false,
+    isLatestAttempt: false,
+    attemptDetails: [],
+    results:
+      attempt % 2 === 0
+        ? [
+            {
+              internalResultId: 'f43r0',
+              category: Category.ERROR,
+              summary:
+                'Without eliminating all the TODOs your change will break!',
+            },
+          ]
+        : [],
+  };
+}
+
+export const fakeRun4Att = [
+  fakeRun4_1,
+  fakeRun4_2,
+  fakeRun4_3,
+  ...fakeRun4CreateAttempts(5, 40),
+  fakeRun4_4,
+];
+
 export const fakeActions: Action[] = [
   {
     name: 'Fake Action 1',
@@ -587,6 +687,7 @@
   },
   {
     name: 'Fake Action 3',
+    summary: true,
     primary: false,
     tooltip: 'Tooltip for Fake Action 3',
     callback: () => Promise.resolve({message: 'fake action 3 triggered'}),
@@ -674,6 +775,7 @@
   pluginState[pluginName] = {
     ...pluginState[pluginName],
     loading: false,
+    firstTimeLoad: false,
     errorMessage,
     loginCallback: undefined,
     runs: [],
@@ -692,6 +794,7 @@
   pluginState[pluginName] = {
     ...pluginState[pluginName],
     loading: false,
+    firstTimeLoad: false,
     errorMessage: undefined,
     loginCallback,
     runs: [],
@@ -718,6 +821,7 @@
   pluginState[pluginName] = {
     ...pluginState[pluginName],
     loading: false,
+    firstTimeLoad: false,
     errorMessage: undefined,
     loginCallback: undefined,
     runs: runs.map(run => {
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index f05facb..dbd3f86 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -20,6 +20,7 @@
   _testOnly_getState,
   _testOnly_resetState,
   ChecksPatchset,
+  updateStateSetLoading,
   updateStateSetProvider,
   updateStateSetResults,
   updateStateUpdateResult,
@@ -45,34 +46,55 @@
   },
 ];
 
+function current() {
+  return _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
+}
+
 suite('checks-model tests', () => {
   test('updateStateSetProvider', () => {
     _testOnly_resetState();
     updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
-    const state = _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
-    assert.deepEqual(state, {
+    assert.deepEqual(current(), {
       pluginName: PLUGIN_NAME,
       loading: false,
+      firstTimeLoad: true,
       runs: [],
       actions: [],
       links: [],
     });
   });
 
+  test('loading and first time load', () => {
+    _testOnly_resetState();
+    updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.isFalse(current().loading);
+    assert.isTrue(current().firstTimeLoad);
+    updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.isTrue(current().loading);
+    assert.isTrue(current().firstTimeLoad);
+    updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
+    assert.isFalse(current().loading);
+    assert.isFalse(current().firstTimeLoad);
+    updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
+    assert.isTrue(current().loading);
+    assert.isFalse(current().firstTimeLoad);
+    updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
+    assert.isFalse(current().loading);
+    assert.isFalse(current().firstTimeLoad);
+  });
+
   test('updateStateSetResults', () => {
     _testOnly_resetState();
     updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
-    const state = _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
-    assert.lengthOf(state.runs, 1);
-    assert.lengthOf(state.runs[0].results!, 1);
+    assert.lengthOf(current().runs, 1);
+    assert.lengthOf(current().runs[0].results!, 1);
   });
 
   test('updateStateUpdateResult', () => {
     _testOnly_resetState();
     updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
-    let state = _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
     assert.equal(
-      state.runs[0].results![0].summary,
+      current().runs[0].results![0].summary,
       RUNS[0]!.results![0].summary
     );
     const result = RUNS[0].results![0];
@@ -83,9 +105,8 @@
       updatedResult,
       ChecksPatchset.LATEST
     );
-    state = _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
-    assert.lengthOf(state.runs, 1);
-    assert.lengthOf(state.runs[0].results!, 1);
-    assert.equal(state.runs[0].results![0].summary, 'new');
+    assert.lengthOf(current().runs, 1);
+    assert.lengthOf(current().runs[0].results!, 1);
+    assert.equal(current().runs[0].results![0].summary, 'new');
   });
 });
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
index 164074b..5ebc13c 100644
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ b/polygerrit-ui/app/services/checks/checks-service.ts
@@ -14,16 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 import {
   catchError,
   filter,
   switchMap,
+  takeUntil,
   takeWhile,
   throttleTime,
   withLatestFrom,
 } from 'rxjs/operators';
 import {
+  Action,
   ChangeData,
   CheckResult,
   CheckRun,
@@ -54,13 +55,14 @@
   Subject,
   timer,
 } from 'rxjs';
-import {ChangeInfo, PatchSetNumber} from '../../types/common';
+import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common';
 import {getCurrentRevision} from '../../utils/change-util';
 import {getShaByPatchNum} from '../../utils/patch-set-util';
 import {assertIsDefined} from '../../utils/common-util';
 import {ReportingService} from '../gr-reporting/gr-reporting';
 import {routerPatchNum$} from '../router/router-model';
 import {Execution} from '../../constants/reporting';
+import {fireAlert, fireEvent} from '../../utils/event-util';
 
 export class ChecksService {
   private readonly providers: {[name: string]: ChecksProvider} = {};
@@ -69,11 +71,14 @@
 
   private checkToPluginMap = new Map<string, string>();
 
+  private changeNum?: NumericChangeId;
+
   private latestPatchNum?: PatchSetNumber;
 
   private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
 
   constructor(readonly reporting: ReportingService) {
+    changeNum$.subscribe(x => (this.changeNum = x));
     checkToPluginMap$.subscribe(map => {
       this.checkToPluginMap = map;
     });
@@ -120,11 +125,50 @@
     updateStateUpdateResult(pluginName, run, result, ChecksPatchset.SELECTED);
   }
 
+  triggerAction(action?: Action, run?: CheckRun) {
+    if (!action?.callback) return;
+    if (!this.changeNum) return;
+    const patchSet = run?.patchset ?? this.latestPatchNum;
+    if (!patchSet) return;
+    const promise = action.callback(
+      this.changeNum,
+      patchSet,
+      run?.attempt,
+      run?.externalId,
+      run?.checkName,
+      action.name
+    );
+    // If plugins return undefined or not a promise, then show no toast.
+    if (!promise?.then) return;
+
+    fireAlert(document, `Triggering action '${action.name}' ...`);
+    from(promise)
+      // If the action takes longer than 5 seconds, then most likely the
+      // user is either not interested or the result not relevant anymore.
+      .pipe(takeUntil(timer(5000)))
+      .subscribe(result => {
+        if (result.errorMessage || result.message) {
+          fireAlert(document, `${result.message ?? result.errorMessage}`);
+        } else {
+          fireEvent(document, 'hide-alert');
+        }
+        if (result.shouldReload) {
+          this.reloadForCheck(run?.checkName);
+        }
+      });
+  }
+
   register(
     pluginName: string,
     provider: ChecksProvider,
     config: ChecksApiConfig
   ) {
+    if (this.providers[pluginName]) {
+      console.warn(
+        `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.`
+      );
+      return;
+    }
     this.providers[pluginName] = provider;
     this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
     updateStateSetProvider(pluginName, ChecksPatchset.LATEST);
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 28c73d3..18cc076 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -157,7 +157,6 @@
 export enum PRIMARY_STATUS_ACTIONS {
   RERUN = 'rerun',
   RUN = 'run',
-  CANCEL = 'cancel',
 }
 
 export function toCanonicalAction(action: Action, status: RunStatus) {
@@ -165,20 +164,30 @@
   if (status === RunStatus.COMPLETED && (name === 'run' || name === 're-run')) {
     name = PRIMARY_STATUS_ACTIONS.RERUN;
   }
-  if (status === RunStatus.RUNNING && name === 'stop') {
-    name = PRIMARY_STATUS_ACTIONS.CANCEL;
-  }
   return {...action, name};
 }
 
-export function primaryActionName(status: RunStatus) {
+export function headerForStatus(status: RunStatus) {
+  switch (status) {
+    case RunStatus.COMPLETED:
+      return 'Completed';
+    case RunStatus.RUNNABLE:
+      return 'Not run';
+    case RunStatus.RUNNING:
+      return 'Running';
+    default:
+      assertNever(status, `Unsupported status: ${status}`);
+  }
+}
+
+function primaryActionName(status: RunStatus) {
   switch (status) {
     case RunStatus.COMPLETED:
       return PRIMARY_STATUS_ACTIONS.RERUN;
     case RunStatus.RUNNABLE:
       return PRIMARY_STATUS_ACTIONS.RUN;
     case RunStatus.RUNNING:
-      return PRIMARY_STATUS_ACTIONS.CANCEL;
+      return undefined;
     default:
       assertNever(status, `Unsupported status: ${status}`);
   }
@@ -278,21 +287,6 @@
   }
 }
 
-export function fireActionTriggered(
-  target: EventTarget,
-  action?: Action,
-  run?: CheckRun
-) {
-  if (!action) return;
-  target.dispatchEvent(
-    new CustomEvent('action-triggered', {
-      detail: {action, run},
-      composed: true,
-      bubbles: true,
-    })
-  );
-}
-
 export interface AttemptDetail {
   attempt: number | undefined;
   icon: string;
diff --git a/polygerrit-ui/app/services/comments/comments-model.ts b/polygerrit-ui/app/services/comments/comments-model.ts
index f7e316a..850acbc 100644
--- a/polygerrit-ui/app/services/comments/comments-model.ts
+++ b/polygerrit-ui/app/services/comments/comments-model.ts
@@ -91,12 +91,16 @@
   distinctUntilChanged()
 );
 
+function publishState(state: CommentState) {
+  privateState$.next(state);
+}
+
 export function updateStateComments(comments?: {
   [path: string]: CommentInfo[];
 }) {
   const nextState = {...privateState$.getValue()};
   nextState.comments = addPath(comments) || {};
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStateRobotComments(robotComments?: {
@@ -104,13 +108,13 @@
 }) {
   const nextState = {...privateState$.getValue()};
   nextState.robotComments = addPath(robotComments) || {};
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStateDrafts(drafts?: {[path: string]: DraftInfo[]}) {
   const nextState = {...privateState$.getValue()};
   nextState.drafts = addPath(drafts) || {};
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStatePortedComments(
@@ -118,19 +122,19 @@
 ) {
   const nextState = {...privateState$.getValue()};
   nextState.portedComments = portedComments || {};
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStatePortedDrafts(portedDrafts?: PathToCommentsInfoMap) {
   const nextState = {...privateState$.getValue()};
   nextState.portedDrafts = portedDrafts || {};
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStateAddDiscardedDraft(draft: DraftInfo) {
   const nextState = {...privateState$.getValue()};
   nextState.discardedDrafts = [...nextState.discardedDrafts, draft];
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStateUndoDiscardedDraft(draftID?: string) {
@@ -142,7 +146,7 @@
   }
   drafts.splice(index, 1);
   nextState.discardedDrafts = drafts;
-  privateState$.next(nextState);
+  publishState(nextState);
 }
 
 export function updateStateAddDraft(draft: DraftInfo) {
@@ -162,7 +166,25 @@
   } else {
     drafts[draft.path].push(draft);
   }
-  privateState$.next(nextState);
+  publishState(nextState);
+}
+
+export function updateStateUpdateDraft(draft: DraftInfo) {
+  const nextState = {...privateState$.getValue()};
+  if (!draft.path) throw new Error('draft path undefined');
+  nextState.drafts = {...nextState.drafts};
+  const drafts = nextState.drafts;
+  if (!drafts[draft.path])
+    throw new Error('draft: trying to edit non-existent draft');
+  drafts[draft.path] = [...drafts[draft.path]];
+  const index = drafts[draft.path].findIndex(
+    d =>
+      (d.__draftID && d.__draftID === draft.__draftID) ||
+      (d.id && d.id === draft.id)
+  );
+  if (index === -1) return;
+  drafts[draft.path][index] = draft;
+  publishState(nextState);
 }
 
 export function updateStateDeleteDraft(draft: DraftInfo) {
@@ -179,6 +201,6 @@
   const discardedDraft = drafts[draft.path][index];
   drafts[draft.path] = [...drafts[draft.path]];
   drafts[draft.path].splice(index, 1);
-  privateState$.next(nextState);
+  publishState(nextState);
   updateStateAddDiscardedDraft(discardedDraft);
 }
diff --git a/polygerrit-ui/app/services/comments/comments-service.ts b/polygerrit-ui/app/services/comments/comments-service.ts
index 9aa297a..16ee2f7 100644
--- a/polygerrit-ui/app/services/comments/comments-service.ts
+++ b/polygerrit-ui/app/services/comments/comments-service.ts
@@ -23,6 +23,7 @@
 import {
   updateStateAddDraft,
   updateStateDeleteDraft,
+  updateStateUpdateDraft,
   updateStateComments,
   updateStateRobotComments,
   updateStateDrafts,
@@ -96,6 +97,14 @@
     updateStateAddDraft(draft);
   }
 
+  cancelDraft(draft: DraftInfo) {
+    updateStateUpdateDraft(draft);
+  }
+
+  editDraft(draft: DraftInfo) {
+    updateStateUpdateDraft(draft);
+  }
+
   deleteDraft(draft: DraftInfo) {
     updateStateDeleteDraft(draft);
   }
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index ef5fde2..2839874 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -27,6 +27,5 @@
   NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
   TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
   CHECKS_DEVELOPER = 'UiFeature__checks_developer',
-  NEW_REPLY_DIALOG = 'UiFeature__new_reply_dialog',
   SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
 }
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index 024fa2a..c254284 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -108,7 +108,7 @@
         // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
         try {
           res.clone().text();
-        } catch (error) {
+        } catch {
           // Ignore error
         }
 
@@ -306,7 +306,7 @@
         // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
         try {
           response.clone().text();
-        } catch (error) {
+        } catch {
           // Ignore error
         }
       }
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.js b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
index ac93a7d..debba6d 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
@@ -34,40 +34,32 @@
       fakeFetch = sinon.stub(window, 'fetch');
     });
 
-    test('auth-check returns 403', done => {
+    test('auth-check returns 403', async () => {
       fakeFetch.returns(Promise.resolve({status: 403}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        done();
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
     });
 
-    test('auth-check returns 204', done => {
+    test('auth-check returns 204', async () => {
       fakeFetch.returns(Promise.resolve({status: 204}));
-      auth.authCheck().then(authed => {
-        assert.isTrue(authed);
-        assert.equal(auth.status, Auth.STATUS.AUTHED);
-        done();
-      });
+      const authed = await auth.authCheck();
+      assert.isTrue(authed);
+      assert.equal(auth.status, Auth.STATUS.AUTHED);
     });
 
-    test('auth-check returns 502', done => {
+    test('auth-check returns 502', async () => {
       fakeFetch.returns(Promise.resolve({status: 502}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        done();
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
     });
 
-    test('auth-check failed', done => {
+    test('auth-check failed', async () => {
       fakeFetch.returns(Promise.reject(new Error('random error')));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.ERROR);
-        done();
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.ERROR);
     });
   });
 
@@ -80,129 +72,105 @@
       fakeFetch = sinon.stub(window, 'fetch');
     });
 
-    test('cache auth-check result', done => {
+    test('cache auth-check result', async () => {
       fakeFetch.returns(Promise.resolve({status: 403}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        fakeFetch.returns(Promise.resolve({status: 204}));
-        auth.authCheck().then(authed2 => {
-          assert.isFalse(authed);
-          assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+      fakeFetch.returns(Promise.resolve({status: 204}));
+      const authed2 = await auth.authCheck();
+      assert.isFalse(authed2);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
     });
 
-    test('clearCache should refetch auth-check result', done => {
+    test('clearCache should refetch auth-check result', async () => {
       fakeFetch.returns(Promise.resolve({status: 403}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        fakeFetch.returns(Promise.resolve({status: 204}));
-        auth.clearCache();
-        auth.authCheck().then(authed2 => {
-          assert.isTrue(authed2);
-          assert.equal(auth.status, Auth.STATUS.AUTHED);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+      fakeFetch.returns(Promise.resolve({status: 204}));
+      auth.clearCache();
+      const authed2 = await auth.authCheck();
+      assert.isTrue(authed2);
+      assert.equal(auth.status, Auth.STATUS.AUTHED);
     });
 
-    test('cache expired on auth-check after certain time', done => {
+    test('cache expired on auth-check after certain time', async () => {
       fakeFetch.returns(Promise.resolve({status: 403}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        clock.tick(1000 * 10000);
-        fakeFetch.returns(Promise.resolve({status: 204}));
-        auth.authCheck().then(authed2 => {
-          assert.isTrue(authed2);
-          assert.equal(auth.status, Auth.STATUS.AUTHED);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+      clock.tick(1000 * 10000);
+      fakeFetch.returns(Promise.resolve({status: 204}));
+      const authed2 = await auth.authCheck();
+      assert.isTrue(authed2);
+      assert.equal(auth.status, Auth.STATUS.AUTHED);
     });
 
-    test('no cache if auth-check failed', done => {
+    test('no cache if auth-check failed', async () => {
       fakeFetch.returns(Promise.reject(new Error('random error')));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.ERROR);
-        assert.equal(fakeFetch.callCount, 1);
-        auth.authCheck().then(() => {
-          assert.equal(fakeFetch.callCount, 2);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.ERROR);
+      assert.equal(fakeFetch.callCount, 1);
+      await auth.authCheck();
+      assert.equal(fakeFetch.callCount, 2);
     });
 
-    test('fire event when switch from authed to unauthed', done => {
+    test('fire event when switch from authed to unauthed', async () => {
       fakeFetch.returns(Promise.resolve({status: 204}));
-      auth.authCheck().then(authed => {
-        assert.isTrue(authed);
-        assert.equal(auth.status, Auth.STATUS.AUTHED);
-        clock.tick(1000 * 10000);
-        fakeFetch.returns(Promise.resolve({status: 403}));
-        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
-        auth.authCheck().then(authed2 => {
-          assert.isFalse(authed2);
-          assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-          assert.isTrue(emitStub.called);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isTrue(authed);
+      assert.equal(auth.status, Auth.STATUS.AUTHED);
+      clock.tick(1000 * 10000);
+      fakeFetch.returns(Promise.resolve({status: 403}));
+      const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+      const authed2 = await auth.authCheck();
+      assert.isFalse(authed2);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+      assert.isTrue(emitStub.called);
     });
 
-    test('fire event when switch from authed to error', done => {
+    test('fire event when switch from authed to error', async () => {
       fakeFetch.returns(Promise.resolve({status: 204}));
-      auth.authCheck().then(authed => {
-        assert.isTrue(authed);
-        assert.equal(auth.status, Auth.STATUS.AUTHED);
-        clock.tick(1000 * 10000);
-        fakeFetch.returns(Promise.reject(new Error('random error')));
-        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
-        auth.authCheck().then(authed2 => {
-          assert.isFalse(authed2);
-          assert.isTrue(emitStub.called);
-          assert.equal(auth.status, Auth.STATUS.ERROR);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isTrue(authed);
+      assert.equal(auth.status, Auth.STATUS.AUTHED);
+      clock.tick(1000 * 10000);
+      fakeFetch.returns(Promise.reject(new Error('random error')));
+      const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+      const authed2 = await auth.authCheck();
+      assert.isFalse(authed2);
+      assert.isTrue(emitStub.called);
+      assert.equal(auth.status, Auth.STATUS.ERROR);
     });
 
-    test('no event from non-authed to other status', done => {
+    test('no event from non-authed to other status', async () => {
       fakeFetch.returns(Promise.resolve({status: 403}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        clock.tick(1000 * 10000);
-        fakeFetch.returns(Promise.resolve({status: 204}));
-        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
-        auth.authCheck().then(authed2 => {
-          assert.isTrue(authed2);
-          assert.isFalse(emitStub.called);
-          assert.equal(auth.status, Auth.STATUS.AUTHED);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+      clock.tick(1000 * 10000);
+      fakeFetch.returns(Promise.resolve({status: 204}));
+      const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+      const authed2 = await auth.authCheck();
+      assert.isTrue(authed2);
+      assert.isFalse(emitStub.called);
+      assert.equal(auth.status, Auth.STATUS.AUTHED);
     });
 
-    test('no event from non-authed to other status', done => {
+    test('no event from non-authed to other status', async () => {
       fakeFetch.returns(Promise.resolve({status: 403}));
-      auth.authCheck().then(authed => {
-        assert.isFalse(authed);
-        assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
-        clock.tick(1000 * 10000);
-        fakeFetch.returns(Promise.reject(new Error('random error')));
-        const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
-        auth.authCheck().then(authed2 => {
-          assert.isFalse(authed2);
-          assert.isFalse(emitStub.called);
-          assert.equal(auth.status, Auth.STATUS.ERROR);
-          done();
-        });
-      });
+      const authed = await auth.authCheck();
+      assert.isFalse(authed);
+      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
+      clock.tick(1000 * 10000);
+      fakeFetch.returns(Promise.reject(new Error('random error')));
+      const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+      const authed2 = await auth.authCheck();
+      assert.isFalse(authed2);
+      assert.isFalse(emitStub.called);
+      assert.equal(auth.status, Auth.STATUS.ERROR);
     });
   });
 
@@ -211,26 +179,22 @@
       sinon.stub(window, 'fetch').returns(Promise.resolve({ok: true}));
     });
 
-    test('GET', done => {
-      auth.fetch('/url', {bar: 'bar'}).then(() => {
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/url');
-        assert.equal(options.credentials, 'same-origin');
-        done();
-      });
+    test('GET', async () => {
+      await auth.fetch('/url', {bar: 'bar'});
+      const [url, options] = fetch.lastCall.args;
+      assert.equal(url, '/url');
+      assert.equal(options.credentials, 'same-origin');
     });
 
-    test('POST', done => {
+    test('POST', async () => {
       sinon.stub(auth, '_getCookie')
           .withArgs('XSRF_TOKEN')
           .returns('foobar');
-      auth.fetch('/url', {method: 'POST'}).then(() => {
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/url');
-        assert.equal(options.credentials, 'same-origin');
-        assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
-        done();
-      });
+      await auth.fetch('/url', {method: 'POST'});
+      const [url, options] = fetch.lastCall.args;
+      assert.equal(url, '/url');
+      assert.equal(options.credentials, 'same-origin');
+      assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
     });
   });
 
@@ -254,45 +218,36 @@
       auth.setup(getToken);
     });
 
-    test('base url support', done => {
+    test('base url support', async () => {
       const baseUrl = 'http://foo';
       stubBaseUrl(baseUrl);
-      auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => {
-        const [url] = fetch.lastCall.args;
-        assert.equal(url, 'http://foo/a/url?access_token=zbaz');
-        done();
-      });
+      await auth.fetch(baseUrl + '/url', {bar: 'bar'});
+      const [url] = fetch.lastCall.args;
+      assert.equal(url, 'http://foo/a/url?access_token=zbaz');
     });
 
-    test('fetch not signed in', done => {
+    test('fetch not signed in', async () => {
       getToken.returns(Promise.resolve());
-      auth.fetch('/url', {bar: 'bar'}).then(() => {
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/url');
-        assert.equal(options.bar, 'bar');
-        assert.equal(Object.keys(options.headers).length, 0);
-        done();
-      });
+      await auth.fetch('/url', {bar: 'bar'});
+      const [url, options] = fetch.lastCall.args;
+      assert.equal(url, '/url');
+      assert.equal(options.bar, 'bar');
+      assert.equal(Object.keys(options.headers).length, 0);
     });
 
-    test('fetch signed in', done => {
-      auth.fetch('/url', {bar: 'bar'}).then(() => {
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/a/url?access_token=zbaz');
-        assert.equal(options.bar, 'bar');
-        done();
-      });
+    test('fetch signed in', async () => {
+      await auth.fetch('/url', {bar: 'bar'});
+      const [url, options] = fetch.lastCall.args;
+      assert.equal(url, '/a/url?access_token=zbaz');
+      assert.equal(options.bar, 'bar');
     });
 
-    test('getToken calls are cached', done => {
-      Promise.all([
-        auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => {
-        assert.equal(getToken.callCount, 1);
-        done();
-      });
+    test('getToken calls are cached', async () => {
+      await Promise.all([auth.fetch('/url-one'), auth.fetch('/url-two')]);
+      assert.equal(getToken.callCount, 1);
     });
 
-    test('getToken refreshes token', done => {
+    test('getToken refreshes token', async () => {
       sinon.stub(auth, '_isTokenValid');
       auth._isTokenValid
           .onFirstCall().returns(true)
@@ -300,27 +255,21 @@
           .returns(false)
           .onThirdCall()
           .returns(true);
-      auth.fetch('/url-one')
-          .then(() => {
-            getToken.returns(Promise.resolve(makeToken('bzzbb')));
-            return auth.fetch('/url-two');
-          })
-          .then(() => {
-            const [[firstUrl], [secondUrl]] = fetch.args;
-            assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
-            assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
-            done();
-          });
+      await auth.fetch('/url-one');
+      getToken.returns(Promise.resolve(makeToken('bzzbb')));
+      await auth.fetch('/url-two');
+
+      const [[firstUrl], [secondUrl]] = fetch.args;
+      assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
+      assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
     });
 
-    test('signed in token error falls back to anonymous', done => {
+    test('signed in token error falls back to anonymous', async () => {
       getToken.returns(Promise.resolve('rubbish'));
-      auth.fetch('/url', {bar: 'bar'}).then(() => {
-        const [url, options] = fetch.lastCall.args;
-        assert.equal(url, '/url');
-        assert.equal(options.bar, 'bar');
-        done();
-      });
+      await auth.fetch('/url', {bar: 'bar'});
+      const [url, options] = fetch.lastCall.args;
+      assert.equal(url, '/url');
+      assert.equal(options.bar, 'bar');
     });
 
     test('_isTokenValid', () => {
@@ -337,37 +286,33 @@
       }));
     });
 
-    test('HTTP PUT with content type', done => {
+    test('HTTP PUT with content type', async () => {
       const originalOptions = {
         method: 'PUT',
         headers: new Headers({'Content-Type': 'mail/pigeon'}),
       };
-      auth.fetch('/url', originalOptions).then(() => {
-        assert.isTrue(getToken.called);
-        const [url, options] = fetch.lastCall.args;
-        assert.include(url, '$ct=mail%2Fpigeon');
-        assert.include(url, '$m=PUT');
-        assert.include(url, 'access_token=zbaz');
-        assert.equal(options.method, 'POST');
-        assert.equal(options.headers.get('Content-Type'), 'text/plain');
-        done();
-      });
+      await auth.fetch('/url', originalOptions);
+      assert.isTrue(getToken.called);
+      const [url, options] = fetch.lastCall.args;
+      assert.include(url, '$ct=mail%2Fpigeon');
+      assert.include(url, '$m=PUT');
+      assert.include(url, 'access_token=zbaz');
+      assert.equal(options.method, 'POST');
+      assert.equal(options.headers.get('Content-Type'), 'text/plain');
     });
 
-    test('HTTP PUT without content type', done => {
+    test('HTTP PUT without content type', async () => {
       const originalOptions = {
         method: 'PUT',
       };
-      auth.fetch('/url', originalOptions).then(() => {
-        assert.isTrue(getToken.called);
-        const [url, options] = fetch.lastCall.args;
-        assert.include(url, '$ct=text%2Fplain');
-        assert.include(url, '$m=PUT');
-        assert.include(url, 'access_token=zbaz');
-        assert.equal(options.method, 'POST');
-        assert.equal(options.headers.get('Content-Type'), 'text/plain');
-        done();
-      });
+      await auth.fetch('/url', originalOptions);
+      assert.isTrue(getToken.called);
+      const [url, options] = fetch.lastCall.args;
+      assert.include(url, '$ct=text%2Fplain');
+      assert.include(url, '$m=PUT');
+      assert.include(url, 'access_token=zbaz');
+      assert.equal(options.method, 'POST');
+      assert.equal(options.headers.get('Content-Type'), 'text/plain');
     });
   });
 });
diff --git a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
index 6ce5eea..54a0f72e 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface_test.js
@@ -16,6 +16,7 @@
  */
 
 import '../../test/common-test-setup-karma.js';
+import {mockPromise} from '../../test/test-utils.js';
 import {EventEmitter} from './gr-event-interface_impl.js';
 
 suite('gr-event-interface tests', () => {
@@ -29,37 +30,42 @@
       gerrit.removeAllListeners();
     });
 
-    test('communicate between plugin and Gerrit', done => {
+    test('communicate between plugin and Gerrit', async () => {
       const eventName = 'test-plugin-event';
       let p;
+      const promise = mockPromise();
       gerrit.on(eventName, e => {
         assert.equal(e.value, 'test');
         assert.equal(e.plugin, p);
-        done();
+        promise.resolve();
       });
       gerrit.install(plugin => {
         p = plugin;
         gerrit.emit(eventName, {value: 'test', plugin});
       }, '0.1',
       'http://test.com/plugins/testplugin/static/test.js');
+      await promise;
     });
 
-    test('listen on events from core', done => {
+    test('listen on events from core', async () => {
       const eventName = 'test-plugin-event';
+      const promise = mockPromise();
       gerrit.on(eventName, e => {
         assert.equal(e.value, 'test');
-        done();
+        promise.resolve();
       });
 
       gerrit.emit(eventName, {value: 'test'});
+      await promise;
     });
 
-    test('communicate across plugins', done => {
+    test('communicate across plugins', async () => {
       const eventName = 'test-plugin-event';
+      const promise = mockPromise();
       gerrit.install(plugin => {
         gerrit.on(eventName, e => {
           assert.equal(e.plugin.getPluginName(), 'testB');
-          done();
+          promise.resolve();
         });
       }, '0.1',
       'http://test.com/plugins/testA/static/testA.js');
@@ -68,6 +74,7 @@
         gerrit.emit(eventName, {plugin});
       }, '0.1',
       'http://test.com/plugins/testB/static/testB.js');
+      await promise;
     });
   });
 
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 1445a59..06f1a0c 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -57,6 +57,7 @@
   reportExtension(name: string): void;
   pluginLoaded(name: string): void;
   pluginsLoaded(pluginsList?: string[]): void;
+  pluginsFailed(pluginsList?: string[]): void;
   error(err: Error, reporter?: string, details?: EventDetails): void;
   /**
    * Reset named timer.
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 0df7d12..65a5784 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -636,6 +636,18 @@
     );
   }
 
+  pluginsFailed(pluginsList?: string[]) {
+    if (!pluginsList || pluginsList.length === 0) return;
+    this.reporter(
+      LIFECYCLE.TYPE,
+      LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+      LifeCycle.PLUGINS_FAILED,
+      undefined,
+      {pluginsList: pluginsList || []},
+      true
+    );
+  }
+
   /**
    * Reset named Timing.
    */
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
index 13461bf..337cf2f 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -56,6 +56,7 @@
   },
   pluginLoaded: () => {},
   pluginsLoaded: () => {},
+  pluginsFailed: () => {},
   recordDraftInteraction: () => {},
   reporter: () => {},
   reportErrorDialog: (message: string) => {
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.ts b/polygerrit-ui/app/styles/dashboard-header-styles.ts
index 2bbca63..643a76a 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.ts
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
diff --git a/polygerrit-ui/app/styles/gr-a11y-styles.ts b/polygerrit-ui/app/styles/gr-a11y-styles.ts
new file mode 100644
index 0000000..a1fa62b
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-a11y-styles.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {css} from 'lit';
+
+export const a11yStyles = css`
+  .assistive-tech-only {
+    user-select: none;
+    clip: rect(1px, 1px, 1px, 1px);
+    height: 1px;
+    margin: 0;
+    overflow: hidden;
+    padding: 0;
+    position: absolute;
+    white-space: nowrap;
+    width: 1px;
+    z-index: -1000;
+  }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-a11y-styles">
+  <template>
+    <style>
+    ${a11yStyles.cssText}
+    </style>
+  </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/gr-font-styles.ts b/polygerrit-ui/app/styles/gr-font-styles.ts
new file mode 100644
index 0000000..422a7c5
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-font-styles.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {css} from 'lit';
+
+export const fontStyles = css`
+  .font-normal {
+    font-size: var(--font-size-normal);
+    font-weight: var(--font-weight-normal);
+    line-height: var(--line-height-normal);
+  }
+  .font-small {
+    font-size: var(--font-size-small);
+    font-weight: var(--font-weight-normal);
+    line-height: var(--line-height-small);
+  }
+  .heading-1 {
+    font-family: var(--header-font-family);
+    font-size: var(--font-size-h1);
+    font-weight: var(--font-weight-h1);
+    line-height: var(--line-height-h1);
+  }
+  .heading-2 {
+    font-family: var(--header-font-family);
+    font-size: var(--font-size-h2);
+    font-weight: var(--font-weight-h2);
+    line-height: var(--line-height-h2);
+  }
+  .heading-3 {
+    font-family: var(--header-font-family);
+    font-size: var(--font-size-h3);
+    font-weight: var(--font-weight-h3);
+    line-height: var(--line-height-h3);
+  }
+  strong {
+    font-weight: var(--font-weight-bold);
+  }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-font-styles">
+  <template>
+    <style>
+    ${fontStyles.cssText}
+    </style>
+  </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/gr-form-styles.ts b/polygerrit-ui/app/styles/gr-form-styles.ts
index b9aecff9..34a6936 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.ts
+++ b/polygerrit-ui/app/styles/gr-form-styles.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 export const formStyles = css`
   .gr-form-styles input {
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.ts b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
index 0471a4e..5f58571 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.ts
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 export const menuPageStyles = css`
   :host {
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.ts b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
index 1b3830a..f928848 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.ts
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
diff --git a/polygerrit-ui/app/styles/gr-spinner-styles.ts b/polygerrit-ui/app/styles/gr-spinner-styles.ts
index 6fb1ae6..6015be4 100644
--- a/polygerrit-ui/app/styles/gr-spinner-styles.ts
+++ b/polygerrit-ui/app/styles/gr-spinner-styles.ts
@@ -14,14 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {css} from 'lit-element';
-
-// Mark the file as a module. Otherwise typescript assumes this is a script
-// and $_documentContainer is a global variable.
-// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
-
-const $_documentContainer = document.createElement('template');
+import {css} from 'lit';
 
 export const spinnerStyles = css`
   .loadingSpin {
@@ -43,6 +36,7 @@
   }
 `;
 
+const $_documentContainer = document.createElement('template');
 $_documentContainer.innerHTML = `<dom-module id="gr-spinner-styles">
   <template>
     <style>
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.ts b/polygerrit-ui/app/styles/gr-subpage-styles.ts
index fa3e55f..e426a7d 100644
--- a/polygerrit-ui/app/styles/gr-subpage-styles.ts
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 export const subpageStyles = css`
   .main {
diff --git a/polygerrit-ui/app/styles/gr-table-styles.ts b/polygerrit-ui/app/styles/gr-table-styles.ts
index 72e36e1..6871499 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.ts
+++ b/polygerrit-ui/app/styles/gr-table-styles.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {css} from 'lit-element';
+import {css} from 'lit';
 
 export const tableStyles = css`
   .genericList {
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.ts b/polygerrit-ui/app/styles/gr-voting-styles.ts
index cb8b0be8..a623d99 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.ts
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -18,33 +18,26 @@
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
 // See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
+import {css} from 'lit';
+
+export const votingStyles = css`
+  .voteChip {
+    border: 1px solid var(--border-color);
+    /* max rounded */
+    border-radius: 1em;
+    box-shadow: none;
+    box-sizing: border-box;
+    min-width: 3em;
+    color: var(--vote-text-color);
+  }
+`;
 
 const $_documentContainer = document.createElement('template');
-
 $_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
   <template>
     <style>
-      :host {
-        --vote-chip-styles: {
-          border-style: solid;
-          border-color: var(--border-color);
-          border-top-left-radius: 1em;
-          border-top-right-radius: 1em;
-          border-bottom-right-radius: 1em;
-          border-bottom-left-radius: 1em;
-          border-top-width: 1px;
-          border-right-width: 1px;
-          border-bottom-width: 1px;
-          border-left-width: 1px;
-          box-shadow: none;
-          box-sizing: border-box;
-          min-width: 3em;
-          color: var(--vote-text-color);
-        }
-      }
+    ${votingStyles.cssText}
     </style>
   </template>
 </dom-module>`;
-
 document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index a8274cc..98f6eb2 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -14,15 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import {css} from 'lit-element';
-
-// Mark the file as a module. Otherwise typescript assumes this is a script
-// and $_documentContainer is a global variable.
-// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
-
-const $_documentContainer = document.createElement('template');
+import {css} from 'lit';
 
 export const sharedStyles = css`
   /* CSS reset */
@@ -176,36 +168,6 @@
     border-spacing: 0;
   }
 
-  /* Fonts */
-
-  .font-normal {
-    font-size: var(--font-size-normal);
-    font-weight: var(--font-weight-normal);
-    line-height: var(--line-height-normal);
-  }
-  .font-small {
-    font-size: var(--font-size-small);
-    font-weight: var(--font-weight-normal);
-    line-height: var(--line-height-small);
-  }
-  .heading-1 {
-    font-family: var(--header-font-family);
-    font-size: var(--font-size-h1);
-    font-weight: var(--font-weight-h1);
-    line-height: var(--line-height-h1);
-  }
-  .heading-2 {
-    font-family: var(--header-font-family);
-    font-size: var(--font-size-h2);
-    font-weight: var(--font-weight-h2);
-    line-height: var(--line-height-h2);
-  }
-  .heading-3 {
-    font-family: var(--header-font-family);
-    font-size: var(--font-size-h3);
-    font-weight: var(--font-weight-h3);
-    line-height: var(--line-height-h3);
-  }
   iron-icon {
     color: var(--deemphasized-text-color);
     vertical-align: top;
@@ -261,22 +223,6 @@
     /** This is needed for firefox */
     --iron-autogrow-textarea_-_white-space: pre-wrap;
   }
-  strong {
-    font-weight: var(--font-weight-bold);
-  }
-
-  .assistive-tech-only {
-    user-select: none;
-    clip: rect(1px, 1px, 1px, 1px);
-    height: 1px;
-    margin: 0;
-    overflow: hidden;
-    padding: 0;
-    position: absolute;
-    white-space: nowrap;
-    width: 1px;
-    z-index: -1000;
-  }
 
   /**
    * TODO: Remove these rules and change (plugin) users to rely on
@@ -303,6 +249,7 @@
   /** END: loading spiner */
 `;
 
+const $_documentContainer = document.createElement('template');
 $_documentContainer.innerHTML = `<dom-module id="shared-styles">
   <template>
     <style>
@@ -310,5 +257,4 @@
     </style>
   </template>
 </dom-module>`;
-
 document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/test/@types/sinon-esm.d.ts b/polygerrit-ui/app/test/@types/sinon-esm.d.ts
deleted file mode 100644
index 9074a7a..0000000
--- a/polygerrit-ui/app/test/@types/sinon-esm.d.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-declare module 'sinon/pkg/sinon-esm' {
-  // sinon-esm doesn't have it's own d.ts, reexport all types from sinon
-  // This is a trick - @types/sinon adds interfaces and sinon instance
-  // to a global variables/namespace. We reexport it here, so we
-  // can use in our code when importing sinon-esm
-  // eslint-disable-next-line import/no-default-export
-  export default sinon;
-  const sinon: Sinon.SinonStatic;
-  export {SinonSpy, SinonFakeTimers, SinonStubbedMember};
-}
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 5096e09..550d3df 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -33,7 +33,6 @@
   TestKeyboardShortcutBinder,
 } from './test-utils';
 import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import sinon from 'sinon/pkg/sinon-esm';
 import {safeTypesBridge} from '../utils/safe-types-util';
 import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
 import {initGlobalVariables} from '../elements/gr-app-global-var-init';
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index c82c15b..db84043 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -106,6 +106,8 @@
   SubmitRequirementResultInfo,
   SubmitRequirementStatus,
 } from '../api/rest-api';
+import {RunResult} from '../services/checks/checks-model';
+import {Category, RunStatus} from '../api/checks';
 
 export function dateToTimestamp(date: Date): Timestamp {
   const nanosecondSuffix = '.000000000';
@@ -688,3 +690,19 @@
     submittability_expression_result: createSubmitRequirementExpressionInfo(),
   };
 }
+
+export function createRunResult(): RunResult {
+  return {
+    attemptDetails: [],
+    category: Category.INFO,
+    checkName: 'test-name',
+    internalResultId: 'test-internal-result-id',
+    internalRunId: 'test-internal-run-id',
+    isLatestAttempt: true,
+    isSingleAttempt: true,
+    pluginName: 'test-plugin-name',
+    status: RunStatus.COMPLETED,
+    summary: 'This is the test summary.',
+    message: 'This is the test message.',
+  };
+}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 03a9525..a60c1d1 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -23,11 +23,12 @@
 } from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {appContext} from '../services/app-context';
 import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
-import {SinonSpy} from 'sinon/pkg/sinon-esm';
+import {SinonSpy} from 'sinon';
 import {StorageService} from '../services/storage/gr-storage';
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {ReportingService} from '../services/gr-reporting/gr-reporting';
 import {CommentsService} from '../services/comments/comments-service';
+export {query, queryAll, queryAndAssert} from '../utils/common-util';
 
 export interface MockPromise extends Promise<unknown> {
   resolve: (value?: unknown) => void;
@@ -47,33 +48,6 @@
   return getComputedStyle(el).display === 'none';
 }
 
-export function queryAll<E extends Element = Element>(
-  el: Element | undefined,
-  selector: string
-): NodeListOf<E> {
-  if (!el) assert.fail('element not defined');
-  const root = el.shadowRoot ?? el;
-  return root.querySelectorAll<E>(selector);
-}
-
-export function query<E extends Element = Element>(
-  el: Element | undefined,
-  selector: string
-): E | undefined {
-  if (!el) return undefined;
-  const root = el.shadowRoot ?? el;
-  return root.querySelector<E>(selector) ?? undefined;
-}
-
-export function queryAndAssert<E extends Element = Element>(
-  el: Element | undefined,
-  selector: string
-): E {
-  const found = query<E>(el, selector);
-  if (!found) assert.fail(`selector '${selector}' did not match anything'`);
-  return found;
-}
-
 export function isVisible(el: Element) {
   assert.ok(el);
   return getComputedStyle(el).getPropertyValue('display') !== 'none';
@@ -218,6 +192,27 @@
   el.parentNode?.removeChild(el);
 }
 
+export function waitUntil(
+  predicate: () => boolean,
+  maxMillis = 100
+): Promise<void> {
+  const start = Date.now();
+  let sleep = 1;
+  return new Promise((resolve, reject) => {
+    const waiter = () => {
+      if (predicate()) {
+        return resolve();
+      }
+      if (Date.now() - start >= maxMillis) {
+        return reject(new Error('Took to long to waitUntil'));
+      }
+      setTimeout(waiter, sleep);
+      sleep *= 2;
+    };
+    waiter();
+  });
+}
+
 /**
  * Promisify an event callback to simplify async...await tests.
  *
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index a533a0f..7b01226 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
     /* Basic Options */
-    "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
     "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "allowJs": true, /* Allow javascript files to be compiled. */
     "checkJs": false, /* Report errors in .js files. */
@@ -25,6 +25,7 @@
     "noUnusedLocals": true, /* Report errors on unused locals. */
     "noUnusedParameters": true, /* Report errors on unused parameters. */
     "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+    "noImplicitOverride": true,
     "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
 
     "skipLibCheck": true, /* Do not check node_modules */
@@ -39,7 +40,13 @@
     "incremental": true,
     "experimentalDecorators": true,
 
-    "allowUmdGlobalAccess": true
+    "allowUmdGlobalAccess": true,
+
+    /* typeRoots for IDE (see tsconfig_bazel.json for Bazel) */
+    "typeRoots": [
+      "node_modules/@types",
+      "../node_modules/@types"
+    ]
   },
   // With the * pattern (without an extension), only supported files
   // are included. The supported files are .ts, .tsx, .d.ts.
diff --git a/polygerrit-ui/app/tsconfig_bazel_test.json b/polygerrit-ui/app/tsconfig_bazel_test.json
index 9c2ff93..7137e23 100644
--- a/polygerrit-ui/app/tsconfig_bazel_test.json
+++ b/polygerrit-ui/app/tsconfig_bazel_test.json
@@ -2,7 +2,6 @@
   "extends": "./tsconfig_bazel.json",
   "compilerOptions": {
     "typeRoots": [
-      "./test/@types",
       "../../external/ui_dev_npm/node_modules/@polymer/iron-test-helpers",
       "../../external/ui_npm/node_modules/@types",
       "../../external/ui_dev_npm/node_modules/@types"
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 696ab63..1617aa3 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -37,6 +37,7 @@
 import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
 import {
   AccountId,
+  AccountDetailInfo,
   AccountInfo,
   AccountsConfigInfo,
   ActionInfo,
@@ -125,6 +126,7 @@
 
 export {
   AccountId,
+  AccountDetailInfo,
   AccountInfo,
   AccountsConfigInfo,
   ActionInfo,
@@ -285,14 +287,6 @@
 }
 
 /**
- * The AccountDetailInfo entity contains detailed information about an account.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-detail-info
- */
-export interface AccountDetailInfo extends AccountInfo {
-  registered_on: Timestamp;
-}
-
-/**
  * The AccountExternalIdInfo entity contains information for an external id of
  * an account.
  * https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-external-id-info
@@ -1149,6 +1143,7 @@
   default_base_for_merges: DefaultBase;
   publish_comments_on_push?: boolean;
   disable_keyboard_shortcuts?: boolean;
+  disable_token_highlighting?: boolean;
   work_in_progress_by_default?: boolean;
   // The email_format doesn't mentioned in doc, but exists in Java class GeneralPreferencesInfo
   email_format?: EmailFormat;
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index a06c2c4..b5bd2aa 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -75,4 +75,8 @@
     lineNumber?: number; // non-standard property
     columnNumber?: number; // non-standard property
   }
+
+  interface ShadowRoot {
+    getSelection?: () => Selection | null;
+  }
 }
diff --git a/polygerrit-ui/app/utils/access-util.ts b/polygerrit-ui/app/utils/access-util.ts
index 44830e2..165eacf 100644
--- a/polygerrit-ui/app/utils/access-util.ts
+++ b/polygerrit-ui/app/utils/access-util.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {LabelName} from '../types/common';
+import {GitRef, LabelName} from '../types/common';
 
 export enum AccessPermissionId {
   ABANDON = 'abandon',
@@ -156,7 +156,7 @@
 }
 
 export interface PermissionArrayItem<T> {
-  id: string;
+  id: GitRef;
   value: T;
 }
 
@@ -175,7 +175,7 @@
   return Object.keys(obj)
     .map(key => {
       return {
-        id: key,
+        id: key as GitRef,
         value: obj[key],
       };
     })
diff --git a/polygerrit-ui/app/utils/admin-nav-util_test.js b/polygerrit-ui/app/utils/admin-nav-util_test.js
index ae2e7a9..2a13904 100644
--- a/polygerrit-ui/app/utils/admin-nav-util_test.js
+++ b/polygerrit-ui/app/utils/admin-nav-util_test.js
@@ -27,71 +27,69 @@
     menuLinkStub = sinon.stub().returns([]);
   });
 
-  const testAdminLinks = (account, options, expected, done) => {
-    getAdminLinks(account,
+  const testAdminLinks = async (account, options, expected) => {
+    const res = await getAdminLinks(account,
         capabilityStub,
         menuLinkStub,
-        options)
-        .then(res => {
-          assert.equal(expected.totalLength, res.links.length);
-          assert.equal(res.links[0].name, 'Repositories');
-          // Repos
-          if (expected.groupListShown) {
-            assert.equal(res.links[1].name, 'Groups');
-          }
+        options);
 
-          if (expected.pluginListShown) {
-            assert.equal(res.links[2].name, 'Plugins');
-            assert.isNotOk(res.links[2].subsection);
-          }
+    assert.equal(expected.totalLength, res.links.length);
+    assert.equal(res.links[0].name, 'Repositories');
+    // Repos
+    if (expected.groupListShown) {
+      assert.equal(res.links[1].name, 'Groups');
+    }
 
-          if (expected.projectPageShown) {
-            assert.isOk(res.links[0].subsection);
-            assert.equal(res.links[0].subsection.children.length, 6);
-          } else {
-            assert.isNotOk(res.links[0].subsection);
-          }
-          // Groups
-          if (expected.groupPageShown) {
-            assert.isOk(res.links[1].subsection);
-            assert.equal(res.links[1].subsection.children.length,
-                expected.groupSubpageLength);
-          } else if ( expected.totalLength > 1) {
-            assert.isNotOk(res.links[1].subsection);
-          }
+    if (expected.pluginListShown) {
+      assert.equal(res.links[2].name, 'Plugins');
+      assert.isNotOk(res.links[2].subsection);
+    }
 
-          if (expected.pluginGeneratedLinks) {
-            for (const link of expected.pluginGeneratedLinks) {
-              const linkMatch = res.links
-                  .find(l => (l.url === link.url && l.name === link.text));
-              assert.isTrue(!!linkMatch);
+    if (expected.projectPageShown) {
+      assert.isOk(res.links[0].subsection);
+      assert.equal(res.links[0].subsection.children.length, 6);
+    } else {
+      assert.isNotOk(res.links[0].subsection);
+    }
+    // Groups
+    if (expected.groupPageShown) {
+      assert.isOk(res.links[1].subsection);
+      assert.equal(res.links[1].subsection.children.length,
+          expected.groupSubpageLength);
+    } else if ( expected.totalLength > 1) {
+      assert.isNotOk(res.links[1].subsection);
+    }
 
-              // External links should open in new tab.
-              if (link.url[0] !== '/') {
-                assert.equal(linkMatch.target, '_blank');
-              } else {
-                assert.isNotOk(linkMatch.target);
-              }
-            }
-          }
+    if (expected.pluginGeneratedLinks) {
+      for (const link of expected.pluginGeneratedLinks) {
+        const linkMatch = res.links
+            .find(l => (l.url === link.url && l.name === link.text));
+        assert.isTrue(!!linkMatch);
 
-          // Current section
-          if (expected.projectPageShown || expected.groupPageShown) {
-            assert.isOk(res.expandedSection);
-            assert.isOk(res.expandedSection.children);
-          } else {
-            assert.isNotOk(res.expandedSection);
-          }
-          if (expected.projectPageShown) {
-            assert.equal(res.expandedSection.name, 'my-repo');
-            assert.equal(res.expandedSection.children.length, 6);
-          } else if (expected.groupPageShown) {
-            assert.equal(res.expandedSection.name, 'my-group');
-            assert.equal(res.expandedSection.children.length,
-                expected.groupSubpageLength);
-          }
-          done();
-        });
+        // External links should open in new tab.
+        if (link.url[0] !== '/') {
+          assert.equal(linkMatch.target, '_blank');
+        } else {
+          assert.isNotOk(linkMatch.target);
+        }
+      }
+    }
+
+    // Current section
+    if (expected.projectPageShown || expected.groupPageShown) {
+      assert.isOk(res.expandedSection);
+      assert.isOk(res.expandedSection.children);
+    } else {
+      assert.isNotOk(res.expandedSection);
+    }
+    if (expected.projectPageShown) {
+      assert.equal(res.expandedSection.name, 'my-repo');
+      assert.equal(res.expandedSection.children.length, 6);
+    } else if (expected.groupPageShown) {
+      assert.equal(res.expandedSection.name, 'my-group');
+      assert.equal(res.expandedSection.children.length,
+          expected.groupSubpageLength);
+    }
   };
 
   suite('logged out', () => {
@@ -106,25 +104,25 @@
       };
     });
 
-    test('without a specific repo or group', done => {
+    test('without a specific repo or group', async () => {
       let options;
       expected = Object.assign(expected, {
         totalLength: 1,
         projectPageShown: false,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('with a repo', done => {
+    test('with a repo', async () => {
       const options = {repoName: 'my-repo'};
       expected = Object.assign(expected, {
         totalLength: 1,
         projectPageShown: true,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('with plugin generated links', done => {
+    test('with plugin generated links', async () => {
       let options;
       const generatedLinks = [
         {text: 'internal link text', url: '/internal/link/url'},
@@ -136,7 +134,7 @@
         projectPageShown: false,
         pluginGeneratedLinks: generatedLinks,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
   });
 
@@ -154,17 +152,17 @@
       capabilityStub.returns(Promise.resolve({}));
     });
 
-    test('without a specific project or group', done => {
+    test('without a specific project or group', async () => {
       let options;
       expected = Object.assign(expected, {
         projectPageShown: false,
         groupListShown: true,
         groupPageShown: false,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('with a repo', done => {
+    test('with a repo', async () => {
       const account = {
         name: 'test-user',
       };
@@ -174,7 +172,7 @@
         groupListShown: true,
         groupPageShown: false,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
   });
 
@@ -193,25 +191,25 @@
       };
     });
 
-    test('without a specific repo or group', done => {
+    test('without a specific repo or group', async () => {
       let options;
       expected = Object.assign(expected, {
         projectPageShown: false,
         groupPageShown: false,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('with a repo', done => {
+    test('with a repo', async () => {
       const options = {repoName: 'my-repo'};
       expected = Object.assign(expected, {
         projectPageShown: true,
         groupPageShown: false,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('admin with internal group', done => {
+    test('admin with internal group', async () => {
       const options = {
         groupId: 'a15262',
         groupName: 'my-group',
@@ -224,10 +222,10 @@
         groupPageShown: true,
         groupSubpageLength: 2,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('group owner with internal group', done => {
+    test('group owner with internal group', async () => {
       const options = {
         groupId: 'a15262',
         groupName: 'my-group',
@@ -240,10 +238,10 @@
         groupPageShown: true,
         groupSubpageLength: 2,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('non owner or admin with internal group', done => {
+    test('non owner or admin with internal group', async () => {
       const options = {
         groupId: 'a15262',
         groupName: 'my-group',
@@ -256,10 +254,10 @@
         groupPageShown: true,
         groupSubpageLength: 1,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
 
-    test('admin with external group', done => {
+    test('admin with external group', async () => {
       const options = {
         groupId: 'a15262',
         groupName: 'my-group',
@@ -272,7 +270,7 @@
         groupPageShown: true,
         groupSubpageLength: 0,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
   });
 
@@ -287,7 +285,7 @@
       expected = {};
     });
 
-    test('with plugin with capabilities', done => {
+    test('with plugin with capabilities', async () => {
       let options;
       const generatedLinks = [
         {text: 'without capability', url: '/without'},
@@ -300,7 +298,7 @@
         totalLength: 4,
         pluginGeneratedLinks: generatedLinks,
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
   });
 
@@ -315,7 +313,7 @@
       expected = {};
     });
 
-    test('with plugin with capabilities', done => {
+    test('with plugin with capabilities', async () => {
       let options;
       const generatedLinks = [
         {text: 'without capability', url: '/without'},
@@ -328,7 +326,7 @@
         totalLength: 3,
         pluginGeneratedLinks: [generatedLinks[0]],
       });
-      testAdminLinks(account, options, expected, done);
+      await testAdminLinks(account, options, expected);
     });
   });
 });
diff --git a/polygerrit-ui/app/utils/attention-set-util.ts b/polygerrit-ui/app/utils/attention-set-util.ts
index bcb2d66..dcd2863 100644
--- a/polygerrit-ui/app/utils/attention-set-util.ts
+++ b/polygerrit-ui/app/utils/attention-set-util.ts
@@ -16,6 +16,7 @@
  */
 
 import {AccountInfo, ChangeInfo, ServerInfo} from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
 import {
   getAccountTemplate,
   isServiceUser,
@@ -29,7 +30,7 @@
 
 export function hasAttention(
   account?: AccountInfo,
-  change?: ChangeInfo
+  change?: ChangeInfo | ParsedChangeInfo
 ): boolean {
   return (
     canHaveAttention(account) &&
@@ -41,7 +42,7 @@
 export function getReason(
   config?: ServerInfo,
   account?: AccountInfo,
-  change?: ChangeInfo
+  change?: ChangeInfo | ParsedChangeInfo
 ) {
   if (!hasAttention(account, change)) return '';
   if (change?.attention_set === undefined) return '';
@@ -76,6 +77,16 @@
   return `${getAccountTemplate(account, config)} replied on the change`;
 }
 
+export function getRemovedByIconClickReason(
+  account?: AccountInfo,
+  config?: ServerInfo
+) {
+  return `Removed by ${getAccountTemplate(
+    account,
+    config
+  )} by clicking the attention icon`;
+}
+
 export function getLastUpdate(account?: AccountInfo, change?: ChangeInfo) {
   if (!hasAttention(account, change)) return '';
   const entry = change!.attention_set![account!._account_id!];
diff --git a/polygerrit-ui/app/utils/change-metadata-util.ts b/polygerrit-ui/app/utils/change-metadata-util.ts
index a4865dc..9692ab31 100644
--- a/polygerrit-ui/app/utils/change-metadata-util.ts
+++ b/polygerrit-ui/app/utils/change-metadata-util.ts
@@ -15,9 +15,7 @@
  * limitations under the License.
  */
 
-import {SubmitRequirementResultInfo} from '../api/rest-api';
 import {ParsedChangeInfo} from '../types/types';
-import {unique} from './common-util';
 
 export enum Metadata {
   OWNER = 'Owner',
@@ -83,19 +81,3 @@
   }
   return true;
 }
-
-export function extractAssociatedLabels(
-  requirement: SubmitRequirementResultInfo
-): string[] {
-  const pattern = new RegExp('label[0-9]*:([\\w-]+)', 'g');
-  const labels = [];
-  let match;
-  while (
-    (match = pattern.exec(
-      requirement.submittability_expression_result.expression
-    )) !== null
-  ) {
-    labels.push(match[1]);
-  }
-  return labels.filter(unique);
-}
diff --git a/polygerrit-ui/app/utils/change-metadata-util_test.ts b/polygerrit-ui/app/utils/change-metadata-util_test.ts
deleted file mode 100644
index 888114d..0000000
--- a/polygerrit-ui/app/utils/change-metadata-util_test.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../test/common-test-setup-karma';
-import {
-  createSubmitRequirementExpressionInfo,
-  createSubmitRequirementResultInfo,
-} from '../test/test-data-generators';
-import {extractAssociatedLabels} from './change-metadata-util';
-
-suite('change-metadata-util', () => {
-  suite('extractAssociatedLabels()', () => {
-    function createSubmitRequirementExpressionInfoWith(expression: string) {
-      return {
-        ...createSubmitRequirementResultInfo(),
-        submittability_expression_result: {
-          ...createSubmitRequirementExpressionInfo(),
-          expression,
-        },
-      };
-    }
-
-    test('1 label', () => {
-      const submitRequirement = createSubmitRequirementExpressionInfoWith(
-        'label:Verified=MAX -label:Verified=MIN'
-      );
-      const labels = extractAssociatedLabels(submitRequirement);
-      assert.deepEqual(labels, ['Verified']);
-    });
-    test('label with number', () => {
-      const submitRequirement = createSubmitRequirementExpressionInfoWith(
-        'label2:verified=MAX'
-      );
-      const labels = extractAssociatedLabels(submitRequirement);
-      assert.deepEqual(labels, ['verified']);
-    });
-    test('2 labels', () => {
-      const submitRequirement = createSubmitRequirementExpressionInfoWith(
-        'label:Verified=MAX -label:Code-Review=MIN'
-      );
-      const labels = extractAssociatedLabels(submitRequirement);
-      assert.deepEqual(labels, ['Verified', 'Code-Review']);
-    });
-  });
-});
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
index c94493b..278e7f3 100644
--- a/polygerrit-ui/app/utils/change-util.ts
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -215,7 +215,7 @@
 }
 
 export function isUploader(
-  change?: ChangeInfo,
+  change?: ChangeInfo | ParsedChangeInfo,
   account?: AccountInfo
 ): boolean {
   if (!change || !account) return false;
@@ -224,7 +224,7 @@
 }
 
 export function isInvolved(
-  change?: ChangeInfo,
+  change?: ChangeInfo | ParsedChangeInfo,
   account?: AccountInfo
 ): boolean {
   const owner = isOwner(change, account);
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 48d6947..5b08fab 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -129,6 +129,7 @@
     const newThread: CommentThread = {
       comments: [comment],
       patchNum: comment.patch_set,
+      diffSide: Side.LEFT,
       commentSide: comment.side ?? CommentSide.REVISION,
       mergeParentNum: comment.parent,
       path: comment.path,
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 36c3657..0002254 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -90,8 +90,17 @@
   }
 }
 
-function query<E extends Element = Element>(
-  el: Element | undefined,
+export function queryAll<E extends Element = Element>(
+  el: Element,
+  selector: string
+): NodeListOf<E> {
+  if (!el) throw new Error('element not defined');
+  const root = el.shadowRoot ?? el;
+  return root.querySelectorAll<E>(selector);
+}
+
+export function query<E extends Element = Element>(
+  el: Element | null | undefined,
   selector: string
 ): E | undefined {
   if (!el) return undefined;
@@ -100,7 +109,7 @@
 }
 
 export function queryAndAssert<E extends Element = Element>(
-  el: Element | undefined,
+  el: Element | null | undefined,
   selector: string
 ): E {
   const found = query<E>(el, selector);
diff --git a/polygerrit-ui/app/utils/compare-util.ts b/polygerrit-ui/app/utils/compare-util.ts
new file mode 100644
index 0000000..dd20915
--- /dev/null
+++ b/polygerrit-ui/app/utils/compare-util.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export function deepEqualStringDict(
+  a: {[name: string]: string},
+  b: {[name: string]: string}
+): boolean {
+  const aKeys = Object.keys(a);
+  const bKeys = Object.keys(b);
+  if (aKeys.length !== bKeys.length) return false;
+  for (const key of aKeys) {
+    if (a[key] !== b[key]) return false;
+  }
+  return true;
+}
+
+export function equalArray(a?: unknown[], b?: unknown[]): boolean {
+  if (a === b) return true;
+  if (a === undefined) return b === undefined;
+  if (b === undefined) return a === undefined;
+  if (a.length !== b.length) return false;
+  for (let i = 0; i < a.length; i++) {
+    if (a[i] !== b[i]) return false;
+  }
+  return true;
+}
diff --git a/polygerrit-ui/app/utils/compare-util_test.ts b/polygerrit-ui/app/utils/compare-util_test.ts
new file mode 100644
index 0000000..7cd71bf
--- /dev/null
+++ b/polygerrit-ui/app/utils/compare-util_test.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../test/common-test-setup-karma';
+import {deepEqualStringDict, equalArray} from './compare-util';
+
+suite('compare-utils tests', () => {
+  test('deepEqual', () => {
+    assert.isTrue(deepEqualStringDict({}, {}));
+    assert.isTrue(deepEqualStringDict({x: 'y'}, {x: 'y'}));
+    assert.isTrue(deepEqualStringDict({x: 'y', p: 'q'}, {p: 'q', x: 'y'}));
+
+    assert.isFalse(deepEqualStringDict({}, {x: 'y'}));
+    assert.isFalse(deepEqualStringDict({x: 'y'}, {x: 'z'}));
+    assert.isFalse(deepEqualStringDict({x: 'y'}, {z: 'y'}));
+  });
+
+  test('equalArray', () => {
+    assert.isTrue(equalArray(undefined, undefined));
+    assert.isTrue(equalArray([], []));
+    assert.isTrue(equalArray([1], [1]));
+    assert.isTrue(equalArray(['a', 'b'], ['a', 'b']));
+
+    assert.isFalse(equalArray(undefined, []));
+    assert.isFalse(equalArray([], undefined));
+    assert.isFalse(equalArray([], [1]));
+    assert.isFalse(equalArray([1], [2]));
+    assert.isFalse(equalArray([1, 2], [1]));
+  });
+});
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index e7cc956..7b1f3e3 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -171,7 +171,7 @@
  *  getEventPath(e); // eg: div.class1>p#pid.class2
  * }
  */
-export function getEventPath<T extends PolymerEvent>(e?: T) {
+export function getEventPath<T extends MouseEvent>(e?: T) {
   if (!e) return '';
 
   let path = e.composedPath();
@@ -227,7 +227,7 @@
 // document.activeElement is not enough, because it's not getting activeElement
 // without looking inside of shadow roots. This will find best activeElement.
 export function findActiveElement(
-  root: DocumentOrShadowRoot | null,
+  root: Document | ShadowRoot | null,
   ignoreDialogs?: boolean
 ): HTMLElement | null {
   if (root === null) {
@@ -257,7 +257,7 @@
 export function isSafari() {
   return (
     /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
-    (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream)
+    /iPad|iPhone|iPod/.test(navigator.userAgent)
   );
 }
 
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 92bbdac..cde9a45 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -15,6 +15,11 @@
  * limitations under the License.
  */
 import {
+  isQuickLabelInfo,
+  SubmitRequirementResultInfo,
+  SubmitRequirementStatus,
+} from '../api/rest-api';
+import {
   AccountInfo,
   ApprovalInfo,
   DetailedLabelInfo,
@@ -23,6 +28,7 @@
   LabelNameToInfoMap,
   VotingRangeInfo,
 } from '../types/common';
+import {assertNever, unique} from './common-util';
 
 // Name of the standard Code-Review label.
 export const CODE_REVIEW = 'Code-Review';
@@ -64,8 +70,11 @@
   return max > -min ? max : min;
 }
 
-export function getLabelStatus(label?: DetailedLabelInfo): LabelStatus {
-  const value = getRepresentativeValue(label);
+export function getLabelStatus(
+  label?: DetailedLabelInfo,
+  vote?: number
+): LabelStatus {
+  const value = vote ?? getRepresentativeValue(label);
   const range = getVotingRangeOrDefault(label);
   if (value < 0) {
     return value === range.min ? LabelStatus.REJECTED : LabelStatus.DISLIKED;
@@ -76,6 +85,23 @@
   return LabelStatus.NEUTRAL;
 }
 
+export function classForLabelStatus(status: LabelStatus) {
+  switch (status) {
+    case LabelStatus.APPROVED:
+      return 'max';
+    case LabelStatus.RECOMMENDED:
+      return 'positive';
+    case LabelStatus.DISLIKED:
+      return 'negative';
+    case LabelStatus.REJECTED:
+      return 'min';
+    case LabelStatus.NEUTRAL:
+      return 'neutral';
+    default:
+      assertNever(status, `Unsupported status: ${status}`);
+  }
+}
+
 export function valueString(value?: number) {
   if (!value) return ' 0';
   let s = `${value}`;
@@ -96,6 +122,19 @@
   return label.all?.filter(x => x._account_id === account._account_id)[0];
 }
 
+export function hasVotes(labelInfo: LabelInfo): boolean {
+  if (isDetailedLabelInfo(labelInfo)) {
+    return (labelInfo.all ?? []).some(
+      approval =>
+        getLabelStatus(labelInfo, approval.value) !== LabelStatus.NEUTRAL
+    );
+  }
+  if (isQuickLabelInfo(labelInfo)) {
+    return !!labelInfo.rejected || !!labelInfo.approved;
+  }
+  return false;
+}
+
 export function labelCompare(labelName1: string, labelName2: string) {
   if (labelName1 === CODE_REVIEW && labelName2 === CODE_REVIEW) return 0;
   if (labelName1 === CODE_REVIEW) return -1;
@@ -114,3 +153,34 @@
   }
   return;
 }
+
+export function extractAssociatedLabels(
+  requirement: SubmitRequirementResultInfo
+): string[] {
+  const pattern = new RegExp('label[0-9]*:([\\w-]+)', 'g');
+  const labels = [];
+  let match;
+  while (
+    (match = pattern.exec(
+      requirement.submittability_expression_result.expression
+    )) !== null
+  ) {
+    labels.push(match[1]);
+  }
+  return labels.filter(unique);
+}
+
+export function iconForStatus(status: SubmitRequirementStatus) {
+  switch (status) {
+    case SubmitRequirementStatus.SATISFIED:
+      return 'check';
+    case SubmitRequirementStatus.UNSATISFIED:
+      return 'close';
+    case SubmitRequirementStatus.OVERRIDDEN:
+      return 'warning';
+    case SubmitRequirementStatus.NOT_APPLICABLE:
+      return 'info';
+    default:
+      assertNever(status, `Unsupported status: ${status}`);
+  }
+}
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 56941ba..196c5e9 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -17,6 +17,7 @@
 
 import '../test/common-test-setup-karma';
 import {
+  extractAssociatedLabels,
   getApprovalInfo,
   getLabelStatus,
   getMaxAccounts,
@@ -32,6 +33,10 @@
   ApprovalInfo,
   DetailedLabelInfo,
 } from '../types/common';
+import {
+  createSubmitRequirementExpressionInfo,
+  createSubmitRequirementResultInfo,
+} from '../test/test-data-generators';
 
 const VALUES_0 = {
   '0': 'neutral',
@@ -186,4 +191,38 @@
     labelInfo = {all: [{value: 0}, {value: -2}]};
     assert.equal(getRepresentativeValue(labelInfo), -2);
   });
+
+  suite('extractAssociatedLabels()', () => {
+    function createSubmitRequirementExpressionInfoWith(expression: string) {
+      return {
+        ...createSubmitRequirementResultInfo(),
+        submittability_expression_result: {
+          ...createSubmitRequirementExpressionInfo(),
+          expression,
+        },
+      };
+    }
+
+    test('1 label', () => {
+      const submitRequirement = createSubmitRequirementExpressionInfoWith(
+        'label:Verified=MAX -label:Verified=MIN'
+      );
+      const labels = extractAssociatedLabels(submitRequirement);
+      assert.deepEqual(labels, ['Verified']);
+    });
+    test('label with number', () => {
+      const submitRequirement = createSubmitRequirementExpressionInfoWith(
+        'label2:verified=MAX'
+      );
+      const labels = extractAssociatedLabels(submitRequirement);
+      assert.deepEqual(labels, ['verified']);
+    });
+    test('2 labels', () => {
+      const submitRequirement = createSubmitRequirementExpressionInfoWith(
+        'label:Verified=MAX -label:Code-Review=MIN'
+      );
+      const labels = extractAssociatedLabels(submitRequirement);
+      assert.deepEqual(labels, ['Verified', 'Code-Review']);
+    });
+  });
 });
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index ca04edf..395ef64 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -2,6 +2,11 @@
 # yarn lockfile v1
 
 
+"@lit/reactive-element@^1.0.0-rc.2":
+  version "1.0.0-rc.3"
+  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.0-rc.3.tgz#5032f493fbf39781b187a7e2dd5d256537c8760c"
+  integrity sha512-Rs2px1keOQUNJUo5B+WExl5v244ZNCiN/iMVNO9evFdJjAdWCIupR/p14zRPkNHsciRBELLTcOZ379cI9O6PDg==
+
 "@mapbox/node-pre-gyp@^1.0.0":
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950"
@@ -420,6 +425,11 @@
   resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz#d8e6c2f830e2650dc06fe74464472ff64b54a302"
   integrity sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg==
 
+"@types/trusted-types@^1.0.1":
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.6.tgz#569b8a08121d3203398290d602d84d73c8dcf5da"
+  integrity sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==
+
 "@webcomponents/shadycss@^1.10.2", "@webcomponents/shadycss@^1.9.1":
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.0.tgz#73e289996c002d8be694cd3be0e83c46ad25e7e0"
@@ -642,17 +652,29 @@
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
 
-lit-element@^2.5.1:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-2.5.1.tgz#3fa74b121a6cd22902409ae3859b7847d01aa6b6"
-  integrity sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==
+lit-element@^3.0.0-rc.2:
+  version "3.0.0-rc.3"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.0-rc.3.tgz#cece8f092d28eb6f9c6b23e4138ff5d7260897ef"
+  integrity sha512-NDe7yjW18gfYQb1GIEQr1T8sB1GUAb1HB62pdAEw+SK6lUW7OFPKQqCOlRhZ6qJXsw9KxMnyYIprLZT4FZdYdQ==
   dependencies:
-    lit-html "^1.1.1"
+    "@lit/reactive-element" "^1.0.0-rc.2"
+    lit-html "^2.0.0-rc.4"
 
-lit-html@^1.1.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.4.1.tgz#0c6f3ee4ad4eb610a49831787f0478ad8e9ae5e0"
-  integrity sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==
+lit-html@^2.0.0-rc.4:
+  version "2.0.0-rc.4"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.0-rc.4.tgz#1015fa8f1f7c8c5b79999ed0bc11c3b79ff1aab5"
+  integrity sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==
+  dependencies:
+    "@types/trusted-types" "^1.0.1"
+
+lit@2.0.0-rc.3:
+  version "2.0.0-rc.3"
+  resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.0-rc.3.tgz#8b6a85268aba287c11125dfe57e88e0bc09beaff"
+  integrity sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==
+  dependencies:
+    "@lit/reactive-element" "^1.0.0-rc.2"
+    lit-element "^3.0.0-rc.2"
+    lit-html "^2.0.0-rc.4"
 
 lru-cache@^6.0.0:
   version "6.0.0"
diff --git a/polygerrit-ui/karma.conf.js b/polygerrit-ui/karma.conf.js
index fe3fa0c..a3b694f 100644
--- a/polygerrit-ui/karma.conf.js
+++ b/polygerrit-ui/karma.conf.js
@@ -22,6 +22,7 @@
   if(runUnderBazel) {
     // Run under bazel
     return [
+      `external/plugins_npm/node_modules`,
       `external/ui_npm/node_modules`,
       `external/ui_dev_npm/node_modules`
     ];
@@ -58,20 +59,34 @@
 }
 
 module.exports = function(config) {
-  const localDirName = path.resolve(__dirname, '../.ts-out/polygerrit-ui/app');
-  const rootDir = runUnderBazel ?
-      'polygerrit-ui/app/_pg_with_tests_out/' : localDirName + '/';
-  const testFilesLocationPattern =
-      `${rootDir}**/!(template_test_srcs)/`;
+  let root = config.root;
+  if (!root) {
+    console.warn(`--root argument not set. Falling back to __dirname.`)
+    root = path.resolve(__dirname) + '/';
+  }
   // Use --test-files to specify pattern for a test files.
   // It can be just a file name, without a path:
   // --test-files async-foreach-behavior_test.js
   // If you specify --test-files without pattern, it gets true value
-  // In this case we ill run all tests (usefull for package.json "debugtest"
+  // In this case we will run all tests (usefull for package.json "debugtest"
   // script)
-  const testFilesPattern = (typeof config.testFiles == 'string') ?
-      testFilesLocationPattern + config.testFiles :
-      testFilesLocationPattern + '*_test.js';
+  // We will convert a .ts argument to .js and fill in .js if no extension is
+  // given.
+  let filePattern;
+  if (typeof config.testFiles === "string") {
+    if (config.testFiles.endsWith('.ts')) {
+      filePattern = config.testFiles.substr(0, config.testFiles.lastIndexOf(".")) + ".js";
+    } else if (config.testFiles.endsWith('.js')) {
+      filePattern = config.testFiles;
+    } else {
+      filePattern = config.testFiles + '.js';
+    }
+  } else {
+    filePattern = '*_test.js';
+  }
+  const testFilesPattern = root + '**/' + filePattern;
+
+  console.info(`Karma test file pattern: ${testFilesPattern}`)
   // Special patch for grep parameters (see details in the grep-patch-karam.js)
   const additionalFiles = runUnderBazel ? [] : ['polygerrit-ui/grep-patch-karma.js'];
   config.set({
diff --git a/polygerrit-ui/karma_test.sh b/polygerrit-ui/karma_test.sh
index 5fab442..940b969 100755
--- a/polygerrit-ui/karma_test.sh
+++ b/polygerrit-ui/karma_test.sh
@@ -1,4 +1,6 @@
 #!/bin/bash
 
 set -euo pipefail
-./$1 start $2 --single-run
+./$1 start $2 \
+  --root 'polygerrit-ui/app/_pg_with_tests_out/**/' \
+  --test-files '*_test.js'
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index ddfaeb4..2a433fb 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -184,10 +184,10 @@
 		//   'page/page.mjs' -> '/node_modules/page.mjs'
 		//   '@polymer/iron-icon' -> '/node_modules/@polymer/iron-icon.js'
 		//   './element/file' -> './element/file.js'
-		moduleImportRegexp = regexp.MustCompile(`(?m)^(import.*|export.* from )['"](.*?)(\.(m?)js)?['"];$`)
+		moduleImportRegexp = regexp.MustCompile(`(import[^'";]*|export[^'";]*from ?)['"]([^;\s]*?)(\.(m?)js)?['"];`)
 		data = moduleImportRegexp.ReplaceAll(data, []byte("$1'$2.${4}js';"))
 
-		moduleImportRegexp = regexp.MustCompile(`(?m)^(import.*|export.* from )['"]([^/.].*)['"];$`)
+		moduleImportRegexp = regexp.MustCompile(`(import[^'";]*|export[^'";]*from ?)['"]([^/.;\s][^;\s]*)['"];`)
 		data = moduleImportRegexp.ReplaceAll(data, []byte("$1'/node_modules/$2';"))
 
 		// The es module version of rxjs can be found in the _esm2015/ directory.
@@ -198,9 +198,12 @@
 		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)tslib.js';$")
 		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}tslib/tslib.es6.js';"))
 
-		// 'lit-element' imports and exports have to be resolved to 'lit-element/lit-element.js'.
-		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)lit-(element|html).js';$")
-		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}lit-${3}/lit-${3}.js';"))
+		// 'lit.js' has to be resolved as 'lit/index.js'.
+		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)lit.js';$")
+		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}lit/index.js';"))
+		// Some lit imports 'a.js' have to be resolved as 'a/a.js'.
+		moduleImportRegexp = regexp.MustCompile(`((import|export)[^'";]*'/node_modules/(@lit/)?)(lit-element|lit-html|reactive-element).js';`)
+		data = moduleImportRegexp.ReplaceAll(data, []byte("${1}${4}/${4}.js';"))
 
 		// 'immer' imports and exports have to be resolved to 'immer/dist/immer.esm.js'.
 		moduleImportRegexp = regexp.MustCompile("(?m)^((import|export).*'/node_modules/)immer.js';$")
diff --git a/proto/cache.proto b/proto/cache.proto
index aa04555..16e5e95 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -268,13 +268,14 @@
 // com.google.gerrit.server.account.externalids.AllExternalIds.
 // Next ID: 2
 message AllExternalIdsProto {
-  // Next ID: 6
+  // Next ID: 7
   message ExternalIdProto {
     string key = 1;
     int32 accountId = 2;
     string email = 3;
     string password = 4;
     bytes blobId = 5;
+    bool isCaseInsensitive = 6;
   }
   repeated ExternalIdProto external_id = 1;
 }
@@ -483,7 +484,7 @@
 }
 
 // Serialized form of com.google.gerrit.entities.SubmitRequirementResult.
-// Next ID: 6
+// Next ID: 7
 message SubmitRequirementResultProto {
   SubmitRequirementProto submit_requirement = 1;
   SubmitRequirementExpressionResultProto applicability_expression_result = 2;
@@ -492,6 +493,9 @@
 
   // Patchset commit ID at which the submit requirements are evaluated.
   bytes commit = 5;
+
+  // Whether this result was created from a legacy submit record.
+  bool legacy = 6;
 }
 
 // Serialized form of com.google.gerrit.entities.SubmitRequirementExpressionResult.
diff --git a/proto/entities.proto b/proto/entities.proto
index d4ff736..de8f647 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -133,6 +133,7 @@
   optional string tag = 6;
   optional Account_Id real_account_id = 7;
   optional bool post_submit = 8;
+  optional bool copied = 9;
 
   // Deleted fields, should not be reused:
   reserved 4;  // changeOpen
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeader.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 5dfe671..3d0edab 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeader.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -20,10 +20,10 @@
   {@param attentionSet: ?}
   {if $attentionSet}
     Attention is currently required from:{sp}
-    {for $attentionSetUser in $attentionSet}
+    {for $attentionSetUser, $index in $attentionSet}
       {$attentionSetUser}
       // add commas or dot.
-      {if isLast($attentionSetUser)}.
+      {if $index == length($attentionSet) - 1}.
       {else},{sp}
       {/if}
     {/for}
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index 191737f..0d8da38 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -21,10 +21,10 @@
   {@param attentionSet: ?}
   {if $attentionSet}
     <p> Attention is currently required from:{sp}
-    {for $attentionSetUser in $attentionSet}
+    {for $attentionSetUser, $index in $attentionSet}
       {$attentionSetUser}
       //add commas or dot.
-      {if isLast($attentionSetUser)}.
+      {if $index == length($attentionSet) - 1}.
       {else},{sp}
       {/if}
     {/for} </p>
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index 4b923e6..4b66401 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -48,8 +48,8 @@
         {\n}
       {/if}
 
-      {for $line in $comment.lines}
-        {if isFirst($line)}
+      {for $line, $index in $comment.lines}
+        {if $index == 0}
           {if $comment.startLine != 0}
             {$comment.link}
           {/if}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
index a3ed3ee..ae2a9c4 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -26,8 +26,8 @@
   {@param email: ?}
   {@param fromName: ?}
   {$fromName} has removed{sp}
-  {for $reviewerName in $email.reviewerNames}
-    {if not isFirst($reviewerName)},{sp}{/if}
+  {for $reviewerName, $index in $email.reviewerNames}
+    {if $index > 0},{sp}{/if}
     {$reviewerName}
   {/for}{sp}
   from this change.{sp}
diff --git a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
index 76a9199..fdcbbe7 100644
--- a/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
+++ b/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -25,9 +25,9 @@
     {$fromName}{sp}
     <strong>
       removed{sp}
-      {for $reviewerName in $email.reviewerNames}
-        {if not isFirst($reviewerName)}
-          {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+      {for $reviewerName, $index in $email.reviewerNames}
+        {if $index > 0}
+          {if $index == (length($email.reviewerNames) - 1)}{sp}and{else},{/if}{sp}
         {/if}
         {$reviewerName}
       {/for}
diff --git a/resources/com/google/gerrit/server/mail/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index e268a31..b8a19fc 100644
--- a/resources/com/google/gerrit/server/mail/Merged.soy
+++ b/resources/com/google/gerrit/server/mail/Merged.soy
@@ -30,7 +30,7 @@
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
 
-  {$email.stickyApprovalDiff}
+  {if $email.stickyApprovalDiff} ( {$email.stickyApprovalDiff} ){/if}
 
   Change subject: {$change.subject}{\n}
   ......................................................................{\n}
diff --git a/resources/com/google/gerrit/server/mail/MergedHtml.soy b/resources/com/google/gerrit/server/mail/MergedHtml.soy
index d2f7bfd..ac4afb3 100644
--- a/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -32,9 +32,11 @@
     </p>
   {/if}
 
-  {call mailTemplate.UnifiedDiff}
-    {param diffLines: $email.stickyApprovalDiffHtml /}
-  {/call}
+  {if $email.stickyApprovalDiffHtml}
+    {call mailTemplate.UnifiedDiff}
+      {param diffLines: $email.stickyApprovalDiffHtml /}
+    {/call}
+  {/if}
 
   <div style="white-space:pre-wrap">{$email.approvals}</div>
 
diff --git a/resources/com/google/gerrit/server/mail/NewChange.soy b/resources/com/google/gerrit/server/mail/NewChange.soy
index aa2b946..c5f34b4 100644
--- a/resources/com/google/gerrit/server/mail/NewChange.soy
+++ b/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -30,8 +30,8 @@
   {if $email.reviewerNames or $email.removedReviewerNames}
    {if $email.reviewerNames}
       Hello{sp}
-      {for $reviewerName in $email.reviewerNames}
-        {if not isFirst($reviewerName)},{sp}{/if}
+      {for $reviewerName, $index in $email.reviewerNames}
+        {if $index > 0},{sp}{/if}
         {$reviewerName}
       {/for},
 
@@ -43,8 +43,8 @@
     {/if}
     {if $email.removedReviewerNames}
       {$fromName} has removed{sp}
-      {for $reviewerName in $email.removedReviewerNames}
-        {if not isFirst($reviewerName)},{sp}{/if}
+      {for $reviewerName, $index in $email.removedReviewerNames}
+        {if $index > 0},{sp}{/if}
         {$reviewerName}
       {/for}{sp}
       from this change.{sp}
diff --git a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
index 272c3ef..008226f 100644
--- a/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
+++ b/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -29,9 +29,9 @@
     {if $email.reviewerNames or $email.removedReviewerNames}
       {if $email.reviewerNames}
         {$fromName} would like{sp}
-        {for $reviewerName in $email.reviewerNames}
-          {if not isFirst($reviewerName)}
-            {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+        {for $reviewerName, $index in $email.reviewerNames}
+          {if $index > 0}
+            {if $index == length($email.reviewerNames) - 1}{sp}and{else},{/if}{sp}
           {/if}
           {$reviewerName}
         {/for}{sp}
@@ -44,9 +44,9 @@
           {$fromName}{sp}
           <strong>
             removed{sp}
-            {for $reviewerName in $email.removedReviewerNames}
-              {if not isFirst($reviewerName)}
-                {if isLast($reviewerName)}{sp}and{else},{/if}{sp}
+            {for $reviewerName, $index in $email.removedReviewerNames}
+              {if $index > 0}
+                {if $index == length($email.removedReviewerNames) - 1}{sp}and{else},{/if}{sp}
               {/if}
               {$reviewerName}
             {/for}
diff --git a/tools/BUILD b/tools/BUILD
index 545a206..1bace53 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -68,77 +68,398 @@
 # Error Prone errors enabled by default; see ../.bazelrc for how this is
 # enabled. This warnings list is originally based on:
 # https://github.com/bazelbuild/BUILD_file_generator/blob/master/tools/bazel_defs/java.bzl
+# Additionally, items used internally in google is added. Such items have
+# the same or higher verbosity level than in google.
 # However, feel free to add any additional errors. Thus far they have all been pretty useful.
-# TODO(davido): Enable ImmutableAnnotationChecker again when these issues are fixed:
-# https://github.com/google/error-prone/issues/1348
-# https://github.com/bazelbuild/bazel/issues/9378
+# All warnings are commented to avoid noise in the output.
+# Newer versions of error-prone have XepDisableAllWarnings flag which could
+# be used instead of commenting. Bazel should be updated to use a new version
+# of error-prone.
 java_package_configuration(
     name = "error_prone",
     javacopts = [
         "-XepDisableWarningsInGeneratedCode",
+        # The XepDisableWarningsInGeneratedCode disables only warnings, but
+        # not errors. We should manually exclude all files generated by
+        # AutoValue; such files always start $AutoValue_.....
+        # XepExcludedPaths is a regexp. If you need more paths - use | as
+        # separator.
+        "-XepExcludedPaths:.*/\\\\$$AutoValue_.*\\.java",
+        "-Xep:AlmostJavadoc:ERROR",
+        "-Xep:AlwaysThrows:ERROR",
         "-Xep:AmbiguousMethodReference:ERROR",
+        "-Xep:AnnotateFormatMethod:ERROR",
+        "-Xep:ArgumentSelectionDefectChecker:ERROR",
+        "-Xep:ArrayAsKeyOfSetOrMap:ERROR",
+        "-Xep:ArrayEquals:ERROR",
+        "-Xep:ArrayFillIncompatibleType:ERROR",
+        "-Xep:ArrayHashCode:ERROR",
+        "-Xep:ArrayToString:ERROR",
+        "-Xep:ArraysAsListPrimitiveArray:ERROR",
+        "-Xep:AssertEqualsArgumentOrderChecker:ERROR",
+        "-Xep:AssertionFailureIgnored:ERROR",
+        "-Xep:AsyncCallableReturnsNull:ERROR",
+        "-Xep:AsyncFunctionReturnsNull:ERROR",
+        "-Xep:AutoValueConstructorOrderChecker:ERROR",
         "-Xep:AutoValueFinalMethods:ERROR",
+        # "-Xep:AutoValueImmutableFields:WARN",
+        # "-Xep:AutoValueSubclassLeaked:WARN",
         "-Xep:BadAnnotationImplementation:ERROR",
         "-Xep:BadComparable:ERROR",
+        # "-Xep:BadImport:WARN",
+        "-Xep:BadInstanceof:ERROR",
+        "-Xep:BadShiftAmount:ERROR",
+        "-Xep:BanSerializableRead:ERROR",
+        "-Xep:BigDecimalEquals:ERROR",
+        "-Xep:BigDecimalLiteralDouble:ERROR",
         "-Xep:BoxedPrimitiveConstructor:ERROR",
+        "-Xep:BoxedPrimitiveEquality:ERROR",
+        "-Xep:BundleDeserializationCast:ERROR",
+        "-Xep:ByteBufferBackingArray:ERROR",
+        "-Xep:CacheLoaderNull:ERROR",
         "-Xep:CannotMockFinalClass:ERROR",
+        "-Xep:CanonicalDuration:ERROR",
+        # "-Xep:CatchAndPrintStackTrace:WARN",
+        "-Xep:CatchFail:ERROR",
+        "-Xep:ChainedAssertionLosesContext:ERROR",
+        "-Xep:ChainingConstructorIgnoresParameter:ERROR",
+        "-Xep:CharacterGetNumericValue:ERROR",
+        "-Xep:CheckNotNullMultipleTimes:ERROR",
+        "-Xep:CheckReturnValue:ERROR",
         "-Xep:ClassCanBeStatic:ERROR",
+        "-Xep:ClassName:ERROR",
         "-Xep:ClassNewInstance:ERROR",
+        "-Xep:CollectionIncompatibleType:ERROR",
+        "-Xep:CollectionToArraySafeParameter:ERROR",
+        "-Xep:CollectionUndefinedEquality:ERROR",
+        "-Xep:CollectorShouldNotUseState:ERROR",
+        "-Xep:ComparableAndComparator:ERROR",
+        "-Xep:ComparableType:ERROR",
+        "-Xep:CompareToZero:ERROR",
+        "-Xep:ComparingThisWithNull:ERROR",
+        "-Xep:ComparisonOutOfRange:ERROR",
+        "-Xep:CompatibleWithAnnotationMisuse:ERROR",
+        "-Xep:CompileTimeConstant:ERROR",
+        "-Xep:ComplexBooleanConstant:ERROR",
+        "-Xep:ComputeIfAbsentAmbiguousReference:ERROR",
+        "-Xep:ConditionalExpressionNumericPromotion:ERROR",
+        "-Xep:ConstantOverflow:ERROR",
+        "-Xep:DaggerProvidesNull:ERROR",
+        "-Xep:DangerousLiteralNull:ERROR",
+        "-Xep:DateChecker:ERROR",
         "-Xep:DateFormatConstant:ERROR",
+        "-Xep:DeadException:ERROR",
+        "-Xep:DeadThread:ERROR",
         "-Xep:DefaultCharset:ERROR",
+        # "-Xep:DefaultPackage:WARN",
+        "-Xep:DepAnn:ERROR",
+        "-Xep:DeprecatedVariable:ERROR",
+        "-Xep:DiscardedPostfixExpression:ERROR",
+        "-Xep:DoNotCall:ERROR",
+        "-Xep:DoNotCallSuggester:ERROR",
+        "-Xep:DoNotClaimAnnotations:ERROR",
+        "-Xep:DoNotMock:ERROR",
+        "-Xep:DoNotMockAutoValue:ERROR",
+        "-Xep:DoubleBraceInitialization:ERROR",
         "-Xep:DoubleCheckedLocking:ERROR",
-        "-Xep:ElementsCountedInLoop:ERROR",
+        "-Xep:DuplicateMapKeys:ERROR",
+        "-Xep:DurationFrom:ERROR",
+        "-Xep:DurationGetTemporalUnit:ERROR",
+        "-Xep:DurationTemporalUnit:ERROR",
+        "-Xep:DurationToLongTimeUnit:ERROR",
+        "-Xep:EmptyBlockTag:ERROR",
+        # "-Xep:EmptyCatch:WARN",
+        "-Xep:EmptySetMultibindingContributions:ERROR",
+        # "-Xep:EqualsGetClass:WARN",
         "-Xep:EqualsHashCode:ERROR",
         "-Xep:EqualsIncompatibleType:ERROR",
+        "-Xep:EqualsNaN:ERROR",
+        "-Xep:EqualsNull:ERROR",
+        "-Xep:EqualsReference:ERROR",
+        "-Xep:EqualsUnsafeCast:ERROR",
+        "-Xep:EqualsUsingHashCode:ERROR",
+        "-Xep:EqualsWrongThing:ERROR",
+        "-Xep:ErroneousThreadPoolConstructorChecker:ERROR",
+        # "-Xep:EscapedEntity:WARN",
         "-Xep:ExpectedExceptionChecker:ERROR",
+        "-Xep:ExtendingJUnitAssert:ERROR",
+        "-Xep:ExtendsAutoValue:ERROR",
+        "-Xep:FallThrough:ERROR",
         "-Xep:Finally:ERROR",
+        "-Xep:FloatCast:ERROR",
+        "-Xep:FloatingPointAssertionWithinEpsilon:ERROR",
         "-Xep:FloatingPointLiteralPrecision:ERROR",
+        "-Xep:FloggerArgumentToString:ERROR",
+        "-Xep:FloggerFormatString:ERROR",
+        "-Xep:FloggerLogVarargs:ERROR",
+        "-Xep:FloggerSplitLogStatement:ERROR",
+        "-Xep:FloggerStringConcatenation:ERROR",
+        "-Xep:ForOverride:ERROR",
+        "-Xep:FormatString:ERROR",
         "-Xep:FormatStringAnnotation:ERROR",
         "-Xep:FragmentInjection:ERROR",
         "-Xep:FragmentNotInstantiable:ERROR",
+        "-Xep:FromTemporalAccessor:ERROR",
         "-Xep:FunctionalInterfaceClash:ERROR",
+        "-Xep:FunctionalInterfaceMethodChanged:ERROR",
         "-Xep:FutureReturnValueIgnored:ERROR",
+        "-Xep:FuturesGetCheckedIllegalExceptionType:ERROR",
+        "-Xep:GetClassOnAnnotation:ERROR",
+        "-Xep:GetClassOnClass:ERROR",
         "-Xep:GetClassOnEnum:ERROR",
-        "-Xep:ImmutableAnnotationChecker:OFF",
+        "-Xep:GuardedBy:ERROR",
+        "-Xep:GuiceAssistedInjectScoping:ERROR",
+        "-Xep:GuiceAssistedParameters:ERROR",
+        "-Xep:HashtableContains:ERROR",
+        "-Xep:HidingField:ERROR",
+        "-Xep:IdentityBinaryExpression:ERROR",
+        "-Xep:IdentityHashMapBoxing:ERROR",
+        "-Xep:IdentityHashMapUsage:ERROR",
+        "-Xep:IgnoredPureGetter:ERROR",
+        "-Xep:Immutable:ERROR",
+        "-Xep:ImmutableAnnotationChecker:ERROR",
         "-Xep:ImmutableEnumChecker:ERROR",
+        "-Xep:ImmutableModification:ERROR",
+        "-Xep:Incomparable:ERROR",
+        "-Xep:IncompatibleArgumentType:ERROR",
         "-Xep:IncompatibleModifiers:ERROR",
+        # "-Xep:InconsistentCapitalization:WARN",
+        "-Xep:InconsistentHashCode:ERROR",
+        "-Xep:IncrementInForLoopAndHeader:ERROR",
+        "-Xep:IndexOfChar:ERROR",
+        "-Xep:InexactVarargsConditional:ERROR",
+        "-Xep:InfiniteRecursion:ERROR",
         "-Xep:InjectOnConstructorOfAbstractClass:ERROR",
+        "-Xep:InheritDoc:ERROR",
+        # "-Xep:InlineFormatString:WARN",
+        "-Xep:InlineMeInliner:ERROR",
+        "-Xep:InlineMeSuggester:ERROR",
+        "-Xep:InlineMeValidator:ERROR",
         "-Xep:InputStreamSlowMultibyteRead:ERROR",
+        "-Xep:InsecureCryptoUsage:ERROR",
+        "-Xep:InstanceOfAndCastMatchWrongType:ERROR",
+        "-Xep:InstantTemporalUnit:ERROR",
+        "-Xep:IntLongMath:ERROR",
+        # "-Xep:InvalidBlockTag:WARN",
+        # "-Xep:InvalidInlineTag:WARN",
+        "-Xep:InvalidJavaTimeConstant:ERROR",
+        "-Xep:InvalidLink:ERROR",
+        # "-Xep:InvalidParam:WARN",
+        "-Xep:InvalidPatternSyntax:ERROR",
+        "-Xep:InvalidThrows:ERROR",
+        "-Xep:InvalidThrowsLink:ERROR",
+        "-Xep:InvalidTimeZoneID:ERROR",
+        "-Xep:InvalidZoneId:ERROR",
+        "-Xep:IsInstanceIncompatibleType:ERROR",
+        "-Xep:IsInstanceOfClass:ERROR",
+        "-Xep:IsLoggableTagLength:ERROR",
         "-Xep:IterableAndIterator:ERROR",
+        "-Xep:IterablePathParameter:ERROR",
         "-Xep:JUnit3FloatingPointComparisonWithoutDelta:ERROR",
+        "-Xep:JUnit3TestNotRun:ERROR",
+        "-Xep:JUnit4ClassAnnotationNonStatic:ERROR",
+        "-Xep:JUnit4ClassUsedInJUnit3:ERROR",
+        "-Xep:JUnit4SetUpNotRun:ERROR",
+        "-Xep:JUnit4TearDownNotRun:ERROR",
+        "-Xep:JUnit4TestNotRun:ERROR",
+        "-Xep:JUnit4TestsNotRunWithinEnclosed:ERROR",
         "-Xep:JUnitAmbiguousTestClass:ERROR",
-        "-Xep:LiteralClassName:ERROR",
+        "-Xep:JUnitAssertSameCheck:ERROR",
+        "-Xep:JUnitParameterMethodNotFound:ERROR",
+        "-Xep:JavaDurationGetSecondsGetNano:ERROR",
+        "-Xep:JavaDurationWithNanos:ERROR",
+        "-Xep:JavaDurationWithSeconds:ERROR",
+        "-Xep:JavaInstantGetSecondsGetNano:ERROR",
+        # "-Xep:JavaLangClash:WARN",
+        "-Xep:JavaLocalDateTimeGetNano:ERROR",
+        "-Xep:JavaLocalTimeGetNano:ERROR",
+        "-Xep:JavaPeriodGetDays:ERROR",
+        "-Xep:JavaTimeDefaultTimeZone:ERROR",
+        "-Xep:JavaUtilDate:ERROR",
+        # "-Xep:JdkObsolete:WARN",
+        "-Xep:JodaConstructors:ERROR",
+        "-Xep:JodaDateTimeConstants:ERROR",
+        "-Xep:JodaDurationWithMillis:ERROR",
+        "-Xep:JodaInstantWithMillis:ERROR",
+        "-Xep:JodaNewPeriod:ERROR",
+        "-Xep:JodaPlusMinusLong:ERROR",
+        "-Xep:JodaTimeConverterManager:ERROR",
+        "-Xep:JodaToSelf:ERROR",
+        "-Xep:JodaWithDurationAddedLong:ERROR",
+        "-Xep:LiteByteStringUtf8:ERROR",
+        "-Xep:LiteEnumValueOf:ERROR",
+        "-Xep:LiteProtoToString:ERROR",
+        "-Xep:LocalDateTemporalAmount:ERROR",
+        "-Xep:LockNotBeforeTry:ERROR",
+        "-Xep:LockOnBoxedPrimitive:ERROR",
+        "-Xep:LogicalAssignment:ERROR",
+        "-Xep:LongFloatConversion:ERROR",
+        "-Xep:LongLiteralLowerCaseSuffix:ERROR",
+        "-Xep:LoopConditionChecker:ERROR",
+        "-Xep:LoopOverCharArray:ERROR",
+        "-Xep:LossyPrimitiveCompare:ERROR",
+        "-Xep:MathAbsoluteRandom:ERROR",
+        "-Xep:MathRoundIntLong:ERROR",
+        "-Xep:MemoizeConstantVisitorStateLookups:ERROR",
+        "-Xep:MislabeledAndroidString:ERROR",
         "-Xep:MissingCasesInEnumSwitch:ERROR",
         "-Xep:MissingFail:ERROR",
         "-Xep:MissingOverride:ERROR",
+        "-Xep:MissingSummary:ERROR",
+        "-Xep:MissingSuperCall:ERROR",
+        "-Xep:MissingTestCall:ERROR",
+        "-Xep:MisusedDayOfYear:ERROR",
+        "-Xep:MisusedWeekYear:ERROR",
+        "-Xep:MixedDescriptors:ERROR",
+        # "-Xep:MixedMutabilityReturnType:WARN",
+        "-Xep:MockitoUsage:ERROR",
+        "-Xep:ModifiedButNotUsed:ERROR",
+        "-Xep:ModifyCollectionInEnhancedForLoop:ERROR",
+        "-Xep:ModifySourceCollectionInStream:ERROR",
+        "-Xep:ModifyingCollectionWithItself:ERROR",
+        "-Xep:MultipleParallelOrSequentialCalls:ERROR",
+        "-Xep:MultipleUnaryOperatorsInMethodCall:ERROR",
+        "-Xep:MustBeClosedChecker:ERROR",
         "-Xep:MutableConstantField:ERROR",
+        # "-Xep:MutablePublicArray:WARN",
+        "-Xep:NCopiesOfChar:ERROR",
         "-Xep:NarrowingCompoundAssignment:ERROR",
+        "-Xep:NestedInstanceOfConditions:ERROR",
         "-Xep:NonAtomicVolatileUpdate:ERROR",
+        "-Xep:NonCanonicalStaticImport:ERROR",
+        # "-Xep:NonCanonicalType:WARN",
+        "-Xep:NonFinalCompileTimeConstant:ERROR",
         "-Xep:NonOverridingEquals:ERROR",
+        "-Xep:NonRuntimeAnnotation:ERROR",
+        "-Xep:NullOptional:ERROR",
+        "-Xep:NullTernary:ERROR",
         "-Xep:NullableConstructor:ERROR",
         "-Xep:NullablePrimitive:ERROR",
+        "-Xep:NullablePrimitiveArray:ERROR",
         "-Xep:NullableVoid:ERROR",
+        "-Xep:ObjectEqualsForPrimitives:ERROR",
         "-Xep:ObjectToString:ERROR",
+        "-Xep:ObjectsHashCodePrimitive:ERROR",
         "-Xep:OperatorPrecedence:ERROR",
+        "-Xep:OptionalEquality:ERROR",
+        "-Xep:OptionalMapToOptional:ERROR",
+        "-Xep:OptionalMapUnusedValue:ERROR",
+        "-Xep:OptionalNotPresent:ERROR",
+        "-Xep:OptionalOfRedundantMethod:ERROR",
+        "-Xep:OrphanedFormatString:ERROR",
+        "-Xep:OutlineNone:ERROR",
+        "-Xep:OverlappingQualifierAndScopeAnnotation:ERROR",
+        "-Xep:OverrideThrowableToString:ERROR",
+        "-Xep:Overrides:ERROR",
         "-Xep:OverridesGuiceInjectableMethod:ERROR",
+        "-Xep:OverridesJavaxInjectableMethod:ERROR",
+        "-Xep:PackageInfo:ERROR",
+        # "-Xep:ParameterName:WARN",
+        "-Xep:ParametersButNotParameterized:ERROR",
+        "-Xep:ParcelableCreator:ERROR",
+        "-Xep:PeriodFrom:ERROR",
+        "-Xep:PeriodGetTemporalUnit:ERROR",
+        "-Xep:PeriodTimeMath:ERROR",
+        "-Xep:PreconditionsCheckNotNullRepeated:ERROR",
         "-Xep:PreconditionsInvalidPlaceholder:ERROR",
-        "-Xep:ProtoFieldPreconditionsCheckNotNull:ERROR",
+        "-Xep:PrimitiveAtomicReference:ERROR",
+        "-Xep:PrivateSecurityContractProtoAccess:ERROR",
+        # "-Xep:ProtectedMembersInFinalClass:WARN",
+        "-Xep:ProtoBuilderReturnValueIgnored:ERROR",
+        "-Xep:ProtoDurationGetSecondsGetNano:ERROR",
+        "-Xep:ProtoFieldNullComparison:ERROR",
+        "-Xep:ProtoRedundantSet:ERROR",
+        "-Xep:ProtoStringFieldReferenceEquality:ERROR",
+        "-Xep:ProtoTimestampGetSecondsGetNano:ERROR",
+        "-Xep:ProtoTruthMixedDescriptors:ERROR",
         "-Xep:ProtocolBufferOrdinal:ERROR",
+        "-Xep:ProvidesMethodOutsideOfModule:ERROR",
+        "-Xep:RandomCast:ERROR",
+        "-Xep:RandomModInteger:ERROR",
+        "-Xep:ReachabilityFenceUsage:ERROR",
+        "-Xep:RectIntersectReturnValueIgnored:ERROR",
         "-Xep:ReferenceEquality:ERROR",
+        "-Xep:RefersToDaggerCodegen:ERROR",
+        "-Xep:RemovedInJDK11:ERROR",
         "-Xep:RequiredModifiers:ERROR",
+        "-Xep:RestrictedApiChecker:ERROR",
+        "-Xep:RethrowReflectiveOperationExceptionAsLinkageError:ERROR",
+        "-Xep:ReturnFromVoid:ERROR",
+        "-Xep:ReturnValueIgnored:ERROR",
+        "-Xep:RxReturnValueIgnored:ERROR",
+        # "-Xep:SameNameButDifferent:WARN",
+        "-Xep:SelfAssignment:ERROR",
+        "-Xep:SelfComparison:ERROR",
+        "-Xep:SelfEquals:ERROR",
         "-Xep:ShortCircuitBoolean:ERROR",
-        "-Xep:SimpleDateFormatConstant:ERROR",
+        "-Xep:ShouldHaveEvenArgs:ERROR",
+        "-Xep:SizeGreaterThanOrEqualsZero:ERROR",
+        "-Xep:StaticAssignmentInConstructor:ERROR",
         "-Xep:StaticGuardedByInstance:ERROR",
+        "-Xep:StaticMockMember:ERROR",
+        "-Xep:StaticQualifiedUsingExpression:ERROR",
+        "-Xep:StreamToString:ERROR",
+        "-Xep:StringBuilderInitWithChar:ERROR",
         "-Xep:StringEquality:ERROR",
+        # "-Xep:StringSplitter:WARN",
+        "-Xep:SubstringOfZero:ERROR",
+        "-Xep:SuppressWarningsDeprecated:ERROR",
+        "-Xep:SwigMemoryLeak:ERROR",
         "-Xep:SynchronizeOnNonFinalField:ERROR",
+        "-Xep:TemporalAccessorGetChronoField:ERROR",
+        "-Xep:TestParametersNotInitialized:ERROR",
+        "-Xep:TheoryButNoTheories:ERROR",
+        # "-Xep:ThreadJoinLoop:WARN",
+        "-Xep:ThreadLocalUsage:ERROR",
+        # "-Xep:ThreadPriorityCheck:WARN",
+        "-Xep:ThreeLetterTimeZoneID:ERROR",
+        "-Xep:ThrowIfUncheckedKnownChecked:ERROR",
+        "-Xep:ThrowNull:ERROR",
+        "-Xep:TimeUnitConversionChecker:ERROR",
+        "-Xep:ToStringReturnsNull:ERROR",
+        "-Xep:TreeToString:ERROR",
+        # "-Xep:TruthAssertExpected:WARN",
         "-Xep:TruthConstantAsserts:ERROR",
+        "-Xep:TruthGetOrDefault:ERROR",
+        "-Xep:TruthIncompatibleType:ERROR",
+        "-Xep:TruthSelfEquals:ERROR",
+        "-Xep:TryFailThrowable:ERROR",
+        "-Xep:TypeEquals:ERROR",
+        "-Xep:TypeNameShadowing:ERROR",
+        "-Xep:TypeParameterQualifier:ERROR",
         "-Xep:TypeParameterShadowing:ERROR",
         "-Xep:TypeParameterUnusedInFormals:ERROR",
         "-Xep:URLEqualsHashCode:ERROR",
+        # "-Xep:UndefinedEquals:WARN",
+        "-Xep:UnescapedEntity:ERROR",
+        "-Xep:UnnecessaryAssignment:ERROR",
+        "-Xep:UnnecessaryCheckNotNull:ERROR",
+        # "-Xep:UnnecessaryLambda:WARN",
+        "-Xep:UnnecessaryMethodInvocationMatcher:ERROR",
+        "-Xep:UnnecessaryMethodReference:ERROR",
+        # "-Xep:UnnecessaryParentheses:WARN",
+        "-Xep:UnnecessaryTypeArgument:ERROR",
+        "-Xep:UnrecognisedJavadocTag:ERROR",
+        "-Xep:UnsafeFinalization:ERROR",
+        "-Xep:UnsafeReflectiveConstructionCast:ERROR",
         "-Xep:UnsynchronizedOverridesSynchronized:ERROR",
+        "-Xep:UnusedAnonymousClass:ERROR",
+        "-Xep:UnusedCollectionModifiedInPlace:ERROR",
         "-Xep:UnusedException:ERROR",
+        "-Xep:UnusedMethod:ERROR",
+        "-Xep:UnusedNestedClass:ERROR",
+        "-Xep:UnusedVariable:ERROR",
+        "-Xep:UseBinds:ERROR",
+        "-Xep:UseCorrectAssertInTests:ERROR",
+        "-Xep:VarTypeName:ERROR",
+        "-Xep:VariableNameSameAsType:ERROR",
         "-Xep:WaitNotInLoop:ERROR",
+        "-Xep:WakelockReleasedDangerously:ERROR",
         "-Xep:WildcardImport:ERROR",
+        "-Xep:WithSignatureDiscouraged:ERROR",
+        "-Xep:WrongOneof:ERROR",
+        "-Xep:XorPower:ERROR",
+        "-Xep:ZoneIdOfZ:ERROR",
     ],
     packages = ["error_prone_packages"],
 )
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index eedf0a1..c8d6e4b 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -142,3 +142,36 @@
             "zip -Drq $$ROOT/$@ -g .",
         ]),
     )
+
+def karma_test(name, srcs, data):
+    """Creates a Karma test target.
+
+    It can be used both for the main Gerrit js bundle, but also for plugins. So
+    it should be extremely easy to add Karma test capabilities for new plugins.
+
+    We are sharing one karma.conf.js file. If you want to customize that, then
+    consider using command line arguments that the config file can process, see
+    the `root` argument for an example.
+
+    Args:
+      name: The name of the test rule.
+      srcs: The shell script to invoke, where you can set command line
+        arguments for Karma and its config.
+      data: The bundle of JavaScript files with the tests included.
+    """
+
+    native.sh_test(
+        name = name,
+        size = "enormous",
+        srcs = srcs,
+        args = [
+            "$(location //polygerrit-ui:karma_bin)",
+            "$(location //polygerrit-ui:karma.conf.js)",
+        ],
+        data = data + [
+            "//polygerrit-ui:karma_bin",
+            "//polygerrit-ui:karma.conf.js",
+        ],
+        # Should not run sandboxed.
+        tags = ["karma", "local", "manual"],
+    )
diff --git a/tools/js/eslint.bzl b/tools/js/eslint.bzl
index b32e2bc..eb4d37a 100644
--- a/tools/js/eslint.bzl
+++ b/tools/js/eslint.bzl
@@ -16,6 +16,34 @@
 
 load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test")
 
+def plugin_eslint():
+    """ Convenience wrapper macro of eslint() for Gerrit js plugins
+
+    Args:
+        name: name of the rule
+    """
+    eslint(
+        name = "lint",
+        srcs = native.glob(["**/*.ts"]),
+        config = ".eslintrc.js",
+        data = [
+            "tsconfig.json",
+            "//plugins:.eslintrc.js",
+            "//plugins:.prettierrc.js",
+            "//plugins:tsconfig-plugins-base.json",
+        ],
+        extensions = [".ts"],
+        ignore = "//plugins:.eslintignore",
+        plugins = [
+            "@npm//eslint-config-google",
+            "@npm//eslint-plugin-html",
+            "@npm//eslint-plugin-import",
+            "@npm//eslint-plugin-jsdoc",
+            "@npm//eslint-plugin-prettier",
+            "@npm//gts",
+        ],
+    )
+
 def eslint(name, plugins, srcs, config, ignore, extensions = [".js"], data = []):
     """ Macro to define eslint rules for files.
 
@@ -87,7 +115,7 @@
             "*_test_require_patch.js",
             "--ignore-pattern",
             "*_test_loader.js",
-            "./", # Relative to the config file location
+            "./",  # Relative to the config file location
         ],
         # Should not run sandboxed.
         tags = [
diff --git a/tools/node_tools/launchpad.patch b/tools/node_tools/launchpad.patch
new file mode 100644
index 0000000..565494b
--- /dev/null
+++ b/tools/node_tools/launchpad.patch
@@ -0,0 +1,240 @@
+From d430b5d912bebe87529b887f408ee55c82a0e003 Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Fri, 26 Jun 2020 20:16:47 +0200
+Subject: [PATCH 1/7] Update version.js
+
+---
+ lib/local/version.js | 15 ++++++++++++---
+ 1 file changed, 12 insertions(+), 3 deletions(-)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
+index 0110a74..2c02bef 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
+@@ -6,6 +6,15 @@ var plist = require('plist');
+ var utils = require('./utils');
+ var debug = require('debug')('launchpad:local:version');
+ 
++var validPath = function (filename){
++  var filter = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,<>\/?~]/;
++  if (filter.test(filename)){
++    console.log('\nInvalid characters inside the path to the browser\n');
++    return
++  }
++  return filename;
++}
++
+ module.exports = function(browser) {
+   if (!browser || !browser.path) {
+     return Q(null);
+@@ -18,7 +27,7 @@ module.exports = function(browser) {
+ 
+     debug('Retrieving version for windows executable', command);
+     // Can't use Q.nfcall here unfortunately because of non 0 exit code
+-    exec(command, function(error, stdout) {
++    exec(command.split(' ')[0], command.split(' ').slice(1), function(error, stdout) {
+       var regex = /ProductVersion:\s*(.*)/;
+       // ShowVer.exe returns a non zero status code even if it works
+       if (typeof stdout === 'string' && regex.test(stdout)) {
+@@ -47,8 +56,8 @@ module.exports = function(browser) {
+   }
+ 
+   // Try executing <browser> --version (everything else)
+-  return Q.nfcall(exec, browser.path + ' --version').then(function(stdout) {
+-    debug('Ran ' + browser.path + ' --version', stdout);
++  return Q.nfcall(exec, validPath(browser.path) + ' --version').then(function(stdout) {
++    debug('Ran ' + validPath(browser.path) + ' --version', stdout);
+     var version = utils.getStdout(stdout);
+     if (version) {
+       browser.version = version;
+
+From 09ce4fab2fd53cab893ceaa3b4d7f997af9b41d8 Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Fri, 26 Jun 2020 20:18:35 +0200
+Subject: [PATCH 2/7] Update instance.js
+
+---
+ lib/local/instance.js | 11 +++++++++--
+ 1 file changed, 9 insertions(+), 2 deletions(-)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/instance.js b/tools/node_tools/node_modules/launchpad/lib/g/local/instance.js
+index 484a866..b49990f 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/instance.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/instance.js
+@@ -5,8 +5,15 @@ var EventEmitter = require('events').EventEmitter;
+ var debug = require('debug')('launchpad:local:instance');
+ var rimraf = require('rimraf');
+ 
++var safe = function (str) {
++   // Avoid quotes makes impossible escape the `multi command` scenario
++   return str.replace(/['"]+/g, '');
++}
++
+ var getProcessId = function (name, callback) {
+ 
++  name = safe(name);
++
+   var commands = {
+     darwin: "ps -clx | grep '" + name + "$' | awk '{print $2}' | head -1",
+     linux: "ps -ax | grep '" + name + "$' | awk '{print $2}' | head -1",
+@@ -90,11 +97,11 @@ Instance.prototype.stop = function (callback) {
+     } catch (error) {}
+   } else {
+     if (this.options.command.indexOf('open') === 0) {
+-      command = 'osascript -e \'tell application "' + self.options.process + '" to quit\'';
++      command = 'osascript -e \'tell application "' + safe(self.options.process) + '" to quit\'';
+       debug('Executing shutdown AppleScript', command);
+       exec(command);
+     } else if (process.platform === 'win32') {
+-      command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd));
++      command = 'taskkill /IM "' + safe(this.options.imageName || path.basename(this.cmd)) + '"';
+       debug('Executing shutdown taskkil', command);
+       exec(command).once('exit', function(data) {
+         self.emit('stop', data);
+
+From d3993fce090ed6ef378c1f0594eff18d125dad1e Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Fri, 26 Jun 2020 20:19:17 +0200
+Subject: [PATCH 3/7] Update version.js
+
+---
+ lib/local/version.js | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
+index 2c02bef..5eac082 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
+@@ -6,6 +6,7 @@ var plist = require('plist');
+ var utils = require('./utils');
+ var debug = require('debug')('launchpad:local:version');
+ 
++// Validate paths supplied by the user in order to avoid "arbitrary command execution"
+ var validPath = function (filename){
+   var filter = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,<>\/?~]/;
+   if (filter.test(filename)){
+
+From abf3dbcc79e6b338338594ab2dbef834550e8f65 Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Mon, 29 Jun 2020 13:32:50 +0200
+Subject: [PATCH 4/7] Update instance.js
+
+---
+ lib/local/instance.js | 10 +++++++---
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/instance.js b/tools/node_tools/node_modules/launchpad/lib/g/local/instance.js
+index b49990f..9375d1f 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/instance.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/instance.js
+@@ -1,6 +1,7 @@
+ var path = require('path');
+ var spawn = require("child_process").spawn;
+ var exec = require("child_process").exec;
++var execFile = require("child_process").execFile;
+ var EventEmitter = require('events').EventEmitter;
+ var debug = require('debug')('launchpad:local:instance');
+ var rimraf = require('rimraf');
+@@ -99,11 +100,14 @@ Instance.prototype.stop = function (callback) {
+     if (this.options.command.indexOf('open') === 0) {
+       command = 'osascript -e \'tell application "' + safe(self.options.process) + '" to quit\'';
+       debug('Executing shutdown AppleScript', command);
+-      exec(command);
++      command = command.split(' ');
++      execFile(command[0], command.slice(1));
+     } else if (process.platform === 'win32') {
+-      command = 'taskkill /IM "' + safe(this.options.imageName || path.basename(this.cmd)) + '"';
++      //Adding `"` wasn't safe/functional on Win systems
++      command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd); 
+       debug('Executing shutdown taskkil', command);
+-      exec(command).once('exit', function(data) {
++      command = command.split(' ');
++      execFile(command[0], command.slice(1)).once('exit', function(data) {
+         self.emit('stop', data);
+       });
+     } else {
+
+From 68518b274c9351f799d41ce85f23499ca4a785e9 Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Tue, 30 Jun 2020 00:01:31 +0200
+Subject: [PATCH 5/7] Update instance.js
+
+---
+ lib/local/instance.js | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/instance.js b/tools/node_tools/node_modules/launchpad/lib/g/local/instance.js
+index 9375d1f..f157dd4 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/instance.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/instance.js
+@@ -104,7 +104,7 @@ Instance.prototype.stop = function (callback) {
+       execFile(command[0], command.slice(1));
+     } else if (process.platform === 'win32') {
+       //Adding `"` wasn't safe/functional on Win systems
+-      command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd); 
++      command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd)); 
+       debug('Executing shutdown taskkil', command);
+       command = command.split(' ');
+       execFile(command[0], command.slice(1)).once('exit', function(data) {
+
+From e711d07d40d39162ea4bdb1ed344c58f92bfa10b Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Fri, 3 Jul 2020 12:30:31 +0200
+Subject: [PATCH 6/7] Update version.js
+
+---
+ lib/local/version.js | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
+index 5eac082..d1403a0 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
+@@ -1,5 +1,6 @@
+ var fs = require('fs');
+ var exec = require('child_process').exec;
++var execFile = require('child_process').execFile;
+ var Q = require('q');
+ var path = require('path');
+ var plist = require('plist');
+@@ -8,7 +9,7 @@ var debug = require('debug')('launchpad:local:version');
+ 
+ // Validate paths supplied by the user in order to avoid "arbitrary command execution"
+ var validPath = function (filename){
+-  var filter = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,<>\/?~]/;
++  var filter = /[`!@#$%^&*()_+\-=\[\]{};':"|,<>?~]/;
+   if (filter.test(filename)){
+     console.log('\nInvalid characters inside the path to the browser\n');
+     return
+@@ -28,7 +29,7 @@ module.exports = function(browser) {
+ 
+     debug('Retrieving version for windows executable', command);
+     // Can't use Q.nfcall here unfortunately because of non 0 exit code
+-    exec(command.split(' ')[0], command.split(' ').slice(1), function(error, stdout) {
++    execFile(command.split(' ')[0], command.split(' ').slice(1), function(error, stdout) {
+       var regex = /ProductVersion:\s*(.*)/;
+       // ShowVer.exe returns a non zero status code even if it works
+       if (typeof stdout === 'string' && regex.test(stdout)) {
+
+From a3ff1804f0aacfb4fa20dad1312427b81280bb3e Mon Sep 17 00:00:00 2001
+From: Michele Romano <33063403+Mik317@users.noreply.github.com>
+Date: Fri, 3 Jul 2020 12:31:31 +0200
+Subject: [PATCH 7/7] Update version.js
+
+---
+ lib/local/version.js | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
+index d1403a0..d937be4 100644
+--- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
++++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
+@@ -9,7 +9,7 @@ var debug = require('debug')('launchpad:local:version');
+ 
+ // Validate paths supplied by the user in order to avoid "arbitrary command execution"
+ var validPath = function (filename){
+-  var filter = /[`!@#$%^&*()_+\-=\[\]{};':"|,<>?~]/;
++  var filter = /[`!@#$%^&*()_+\-=\[\]{};'"|,<>?~]/;
+   if (filter.test(filename)){
+     console.log('\nInvalid characters inside the path to the browser\n');
+     return
diff --git a/tools/node_tools/node_modules_licenses/tsconfig.json b/tools/node_tools/node_modules_licenses/tsconfig.json
index cb7bb60..4a57571 100644
--- a/tools/node_tools/node_modules_licenses/tsconfig.json
+++ b/tools/node_tools/node_modules_licenses/tsconfig.json
@@ -8,8 +8,8 @@
         ]
       }
     ],
-    "target": "es6",
-    "module": "commonjs",
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index ea36e9f..7ee64df 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -19,6 +19,12 @@
     "typescript": "4.3.2"
   },
   "devDependencies": {},
+  "scripts": {
+    "postinstall": "(git apply --reverse --ignore-whitespace launchpad.patch || true) && git apply --ignore-whitespace launchpad.patch"
+  },
   "license": "Apache-2.0",
-  "private": true
+  "private": true,
+  "resolutions": {
+    "lodash": "4.17.21"
+  }
 }
diff --git a/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json b/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
index 34ffb2f..d3c7d1d 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
+++ b/tools/node_tools/polygerrit_app_preprocessor/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es6",
-    "module": "commonjs",
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/tools/node_tools/utils/tsconfig.json b/tools/node_tools/utils/tsconfig.json
index 56ab91b..d6bffa0 100644
--- a/tools/node_tools/utils/tsconfig.json
+++ b/tools/node_tools/utils/tsconfig.json
@@ -1,7 +1,7 @@
 {
   "compilerOptions": {
-    "target": "es6",
-    "module": "commonjs",
+    "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     "allowSyntheticDefaultImports": true,
     "esModuleInterop": true,
     "strict": true,
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index d9a2c93..6525c41 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -2,255 +2,262 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
-  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.5.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
+  integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==
   dependencies:
-    "@babel/highlight" "^7.8.3"
+    "@babel/highlight" "^7.14.5"
+
+"@babel/compat-data@^7.14.7", "@babel/compat-data@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176"
+  integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==
 
 "@babel/core@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941"
-  integrity sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8"
+  integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==
   dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.8.3"
-    "@babel/helpers" "^7.8.3"
-    "@babel/parser" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/code-frame" "^7.14.5"
+    "@babel/generator" "^7.15.0"
+    "@babel/helper-compilation-targets" "^7.15.0"
+    "@babel/helper-module-transforms" "^7.15.0"
+    "@babel/helpers" "^7.14.8"
+    "@babel/parser" "^7.15.0"
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.15.0"
+    "@babel/types" "^7.15.0"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
-    gensync "^1.0.0-beta.1"
-    json5 "^2.1.0"
-    lodash "^4.17.13"
-    resolve "^1.3.2"
-    semver "^5.4.1"
+    gensync "^1.0.0-beta.2"
+    json5 "^2.1.2"
+    semver "^6.3.0"
     source-map "^0.5.0"
 
-"@babel/generator@^7.0.0-beta.42", "@babel/generator@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03"
-  integrity sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==
+"@babel/generator@^7.0.0-beta.42", "@babel/generator@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.0.tgz#a7d0c172e0d814974bad5aa77ace543b97917f15"
+  integrity sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.15.0"
     jsesc "^2.5.1"
-    lodash "^4.17.13"
     source-map "^0.5.0"
 
-"@babel/helper-annotate-as-pure@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
-  integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==
+"@babel/helper-annotate-as-pure@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61"
+  integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503"
-  integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz#b939b43f8c37765443a19ae74ad8b15978e0a191"
+  integrity sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==
   dependencies:
-    "@babel/helper-explode-assignable-expression" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/helper-explode-assignable-expression" "^7.14.5"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-call-delegate@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz#de82619898aa605d409c42be6ffb8d7204579692"
-  integrity sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A==
+"@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz#973df8cbd025515f3ff25db0c05efc704fa79818"
+  integrity sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==
   dependencies:
-    "@babel/helper-hoist-variables" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/compat-data" "^7.15.0"
+    "@babel/helper-validator-option" "^7.14.5"
+    browserslist "^4.16.6"
+    semver "^6.3.0"
 
-"@babel/helper-create-regexp-features-plugin@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz#c774268c95ec07ee92476a3862b75cc2839beb79"
-  integrity sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q==
+"@babel/helper-create-regexp-features-plugin@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz#c7d5ac5e9cf621c26057722fb7a8a4c5889358c4"
+  integrity sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==
   dependencies:
-    "@babel/helper-regex" "^7.8.3"
-    regexpu-core "^4.6.0"
+    "@babel/helper-annotate-as-pure" "^7.14.5"
+    regexpu-core "^4.7.1"
 
-"@babel/helper-define-map@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15"
-  integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==
+"@babel/helper-explode-assignable-expression@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz#8aa72e708205c7bb643e45c73b4386cdf2a1f645"
+  integrity sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==
   dependencies:
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/types" "^7.8.3"
-    lodash "^4.17.13"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-explode-assignable-expression@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982"
-  integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==
+"@babel/helper-function-name@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4"
+  integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==
   dependencies:
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/helper-get-function-arity" "^7.14.5"
+    "@babel/template" "^7.14.5"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-function-name@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca"
-  integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==
+"@babel/helper-get-function-arity@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815"
+  integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-get-function-arity@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
-  integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==
+"@babel/helper-hoist-variables@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d"
+  integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-hoist-variables@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134"
-  integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==
+"@babel/helper-member-expression-to-functions@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz#0ddaf5299c8179f27f37327936553e9bba60990b"
+  integrity sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.15.0"
 
-"@babel/helper-member-expression-to-functions@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
-  integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==
+"@babel/helper-module-imports@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3"
+  integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-module-imports@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
-  integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==
+"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz#679275581ea056373eddbe360e1419ef23783b08"
+  integrity sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/helper-module-imports" "^7.14.5"
+    "@babel/helper-replace-supers" "^7.15.0"
+    "@babel/helper-simple-access" "^7.14.8"
+    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/helper-validator-identifier" "^7.14.9"
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.15.0"
+    "@babel/types" "^7.15.0"
 
-"@babel/helper-module-transforms@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz#d305e35d02bee720fbc2c3c3623aa0c316c01590"
-  integrity sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==
+"@babel/helper-optimise-call-expression@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c"
+  integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==
   dependencies:
-    "@babel/helper-module-imports" "^7.8.3"
-    "@babel/helper-simple-access" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.8.3"
-    lodash "^4.17.13"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-optimise-call-expression@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
-  integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==
+"@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
+  integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
+
+"@babel/helper-remap-async-to-generator@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz#51439c913612958f54a987a4ffc9ee587a2045d6"
+  integrity sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==
   dependencies:
-    "@babel/types" "^7.8.3"
+    "@babel/helper-annotate-as-pure" "^7.14.5"
+    "@babel/helper-wrap-function" "^7.14.5"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
-  integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
-
-"@babel/helper-regex@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965"
-  integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==
+"@babel/helper-replace-supers@^7.14.5", "@babel/helper-replace-supers@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz#ace07708f5bf746bf2e6ba99572cce79b5d4e7f4"
+  integrity sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==
   dependencies:
-    lodash "^4.17.13"
+    "@babel/helper-member-expression-to-functions" "^7.15.0"
+    "@babel/helper-optimise-call-expression" "^7.14.5"
+    "@babel/traverse" "^7.15.0"
+    "@babel/types" "^7.15.0"
 
-"@babel/helper-remap-async-to-generator@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86"
-  integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==
+"@babel/helper-simple-access@^7.14.8":
+  version "7.14.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924"
+  integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.8.3"
-    "@babel/helper-wrap-function" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.8"
 
-"@babel/helper-replace-supers@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz#91192d25f6abbcd41da8a989d4492574fb1530bc"
-  integrity sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==
+"@babel/helper-skip-transparent-expression-wrappers@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4"
+  integrity sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==
   dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.8.3"
-    "@babel/helper-optimise-call-expression" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-simple-access@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
-  integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==
+"@babel/helper-split-export-declaration@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a"
+  integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==
   dependencies:
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/types" "^7.14.5"
 
-"@babel/helper-split-export-declaration@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
-  integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==
-  dependencies:
-    "@babel/types" "^7.8.3"
+"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9":
+  version "7.14.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48"
+  integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==
 
-"@babel/helper-wrap-function@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610"
-  integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==
-  dependencies:
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+"@babel/helper-validator-option@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
+  integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==
 
-"@babel/helpers@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.3.tgz#382fbb0382ce7c4ce905945ab9641d688336ce85"
-  integrity sha512-LmU3q9Pah/XyZU89QvBgGt+BCsTPoQa+73RxAQh8fb8qkDyIfeQnmgs+hvzhTCKTzqOyk7JTkS3MS1S8Mq5yrQ==
+"@babel/helper-wrap-function@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz#5919d115bf0fe328b8a5d63bcb610f51601f2bff"
+  integrity sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==
   dependencies:
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/helper-function-name" "^7.14.5"
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.14.5"
+    "@babel/types" "^7.14.5"
 
-"@babel/highlight@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797"
-  integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==
+"@babel/helpers@^7.14.8":
+  version "7.15.3"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.15.3.tgz#c96838b752b95dcd525b4e741ed40bb1dc2a1357"
+  integrity sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g==
   dependencies:
+    "@babel/template" "^7.14.5"
+    "@babel/traverse" "^7.15.0"
+    "@babel/types" "^7.15.0"
+
+"@babel/highlight@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
+  integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.14.5"
     chalk "^2.0.0"
-    esutils "^2.0.2"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081"
-  integrity sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ==
+"@babel/parser@^7.14.5", "@babel/parser@^7.15.0":
+  version "7.15.3"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862"
+  integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==
 
 "@babel/plugin-external-helpers@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-external-helpers/-/plugin-external-helpers-7.8.3.tgz#5a94164d9af393b2820a3cdc407e28ebf237de4b"
-  integrity sha512-mx0WXDDiIl5DwzMtzWGRSPugXi9BxROS05GQrhLNbEamhBiicgn994ibwkyiBH+6png7bm/yA7AUsvHyCXi4Vw==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-external-helpers/-/plugin-external-helpers-7.14.5.tgz#920baa1569a8df5d5710abc342c7b1ac8968ed76"
+  integrity sha512-q/B/hLX+nDGk73Xn529d7Ar4ih17J8pNBbsXafq8oXij0XfFEA/bks+u+6q5q04zO5o/qivjzui6BqzPfYShEg==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-proposal-async-generator-functions@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
-  integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==
+  version "7.14.9"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz#7028dc4fa21dc199bbacf98b39bab1267d0eaf9a"
+  integrity sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/helper-remap-async-to-generator" "^7.8.3"
-    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/helper-remap-async-to-generator" "^7.14.5"
+    "@babel/plugin-syntax-async-generators" "^7.8.4"
 
 "@babel/plugin-proposal-object-rest-spread@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz#eb5ae366118ddca67bed583b53d7554cad9951bb"
-  integrity sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==
+  version "7.14.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz#5920a2b3df7f7901df0205974c0641b13fd9d363"
+  integrity sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/compat-data" "^7.14.7"
+    "@babel/helper-compilation-targets" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+    "@babel/plugin-transform-parameters" "^7.14.5"
 
-"@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.8.0":
+"@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.8.4":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
   integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
@@ -265,13 +272,13 @@
     "@babel/helper-plugin-utils" "^7.8.0"
 
 "@babel/plugin-syntax-import-meta@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.8.3.tgz#230afff79d3ccc215b5944b438e4e266daf3d84d"
-  integrity sha512-vYiGd4wQ9gx0Lngb7+bPCwQXGK/PR6FeTIJ+TIOlq+OfOKG/kCAOO2+IBac3oMM9qV7/fU76hfcqxUaLKZf1hQ==
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
+  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
+"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
   integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
@@ -279,234 +286,244 @@
     "@babel/helper-plugin-utils" "^7.8.0"
 
 "@babel/plugin-transform-arrow-functions@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
-  integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz#f7187d9588a768dd080bf4c9ffe117ea62f7862a"
+  integrity sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-async-to-generator@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086"
-  integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz#72c789084d8f2094acb945633943ef8443d39e67"
+  integrity sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==
   dependencies:
-    "@babel/helper-module-imports" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/helper-remap-async-to-generator" "^7.8.3"
+    "@babel/helper-module-imports" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/helper-remap-async-to-generator" "^7.14.5"
 
 "@babel/plugin-transform-block-scoped-functions@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3"
-  integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz#e48641d999d4bc157a67ef336aeb54bc44fd3ad4"
+  integrity sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-block-scoping@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a"
-  integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==
+  version "7.15.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz#94c81a6e2fc230bcce6ef537ac96a1e4d2b3afaf"
+  integrity sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-    lodash "^4.17.13"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-classes@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz#46fd7a9d2bb9ea89ce88720477979fe0d71b21b8"
-  integrity sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==
+  version "7.14.9"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz#2a391ffb1e5292710b00f2e2c210e1435e7d449f"
+  integrity sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.8.3"
-    "@babel/helper-define-map" "^7.8.3"
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/helper-optimise-call-expression" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/helper-replace-supers" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/helper-annotate-as-pure" "^7.14.5"
+    "@babel/helper-function-name" "^7.14.5"
+    "@babel/helper-optimise-call-expression" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/helper-replace-supers" "^7.14.5"
+    "@babel/helper-split-export-declaration" "^7.14.5"
     globals "^11.1.0"
 
 "@babel/plugin-transform-computed-properties@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b"
-  integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz#1b9d78987420d11223d41195461cc43b974b204f"
+  integrity sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-destructuring@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz#20ddfbd9e4676906b1056ee60af88590cc7aaa0b"
-  integrity sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==
+  version "7.14.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz#0ad58ed37e23e22084d109f185260835e5557576"
+  integrity sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-duplicate-keys@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1"
-  integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz#365a4844881bdf1501e3a9f0270e7f0f91177954"
+  integrity sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-exponentiation-operator@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7"
-  integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz#5154b8dd6a3dfe6d90923d61724bd3deeb90b493"
+  integrity sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==
   dependencies:
-    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-for-of@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.3.tgz#15f17bce2fc95c7d59a24b299e83e81cedc22e18"
-  integrity sha512-ZjXznLNTxhpf4Q5q3x1NsngzGA38t9naWH8Gt+0qYZEJAcvPI9waSStSh56u19Ofjr7QmD0wUsQ8hw8s/p1VnA==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz#dae384613de8f77c196a8869cbf602a44f7fc0eb"
+  integrity sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-function-name@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b"
-  integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz#e81c65ecb900746d7f31802f6bed1f52d915d6f2"
+  integrity sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==
   dependencies:
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-function-name" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-instanceof@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-instanceof/-/plugin-transform-instanceof-7.8.3.tgz#a44d7d71590da36be7429573300618aefd784c3d"
-  integrity sha512-c/jB6Ebe2u17hxo+rce6PDgbkuHyfcJOleqgHYttnvMrCsxVwUnYsMq7GhxXekzUQsv9IImhv6YICKihpen+Ag==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-instanceof/-/plugin-transform-instanceof-7.14.5.tgz#8568277fbcfd7a3e4f3e6c8b7aa8ce4f60cba6e7"
+  integrity sha512-3CIpRzBLk5tEwIzjjD86KR8oMYrp1fl9q7kbdJa6O6Lcmkcee9DXfeO6zRXis//5gWRf63o5oDlNBh0VAlmtgw==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-literals@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1"
-  integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz#41d06c7ff5d4d09e3cf4587bd3ecf3930c730f78"
+  integrity sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-modules-amd@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz#65606d44616b50225e76f5578f33c568a0b876a5"
-  integrity sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz#4fd9ce7e3411cb8b83848480b7041d83004858f7"
+  integrity sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==
   dependencies:
-    "@babel/helper-module-transforms" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
-    babel-plugin-dynamic-import-node "^2.3.0"
+    "@babel/helper-module-transforms" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    babel-plugin-dynamic-import-node "^2.3.3"
 
 "@babel/plugin-transform-object-super@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725"
-  integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz#d0b5faeac9e98597a161a9cf78c527ed934cdc45"
+  integrity sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/helper-replace-supers" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/helper-replace-supers" "^7.14.5"
 
-"@babel/plugin-transform-parameters@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.3.tgz#7890576a13b17325d8b7d44cb37f21dc3bbdda59"
-  integrity sha512-/pqngtGb54JwMBZ6S/D3XYylQDFtGjWrnoCF4gXZOUpFV/ujbxnoNGNvDGu6doFWRPBveE72qTx/RRU44j5I/Q==
+"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz#49662e86a1f3ddccac6363a7dfb1ff0a158afeb3"
+  integrity sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==
   dependencies:
-    "@babel/helper-call-delegate" "^7.8.3"
-    "@babel/helper-get-function-arity" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-regenerator@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz#b31031e8059c07495bf23614c97f3d9698bc6ec8"
-  integrity sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz#9676fd5707ed28f522727c5b3c0aa8544440b04f"
+  integrity sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==
   dependencies:
-    regenerator-transform "^0.14.0"
+    regenerator-transform "^0.14.2"
 
 "@babel/plugin-transform-shorthand-properties@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8"
-  integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz#97f13855f1409338d8cadcbaca670ad79e091a58"
+  integrity sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-spread@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8"
-  integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
+  version "7.14.6"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz#6bd40e57fe7de94aa904851963b5616652f73144"
+  integrity sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5"
 
 "@babel/plugin-transform-sticky-regex@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100"
-  integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz#5b617542675e8b7761294381f3c28c633f40aeb9"
+  integrity sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
-    "@babel/helper-regex" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-template-literals@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80"
-  integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz#a5f2bc233937d8453885dc736bdd8d9ffabf3d93"
+  integrity sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-typeof-symbol@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.3.tgz#5cffb216fb25c8c64ba6bf5f76ce49d3ab079f4d"
-  integrity sha512-3TrkKd4LPqm4jHs6nPtSDI/SV9Cm5PRJkHLUgTcqRQQTMAZ44ZaAdDZJtvWFSaRcvT0a1rTmJ5ZA5tDKjleF3g==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz#39af2739e989a2bd291bf6b53f16981423d457d4"
+  integrity sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-unicode-regex@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad"
-  integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz#4cd09b6c8425dd81255c7ceb3fb1836e7414382e"
+  integrity sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==
   dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-create-regexp-features-plugin" "^7.14.5"
+    "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/template@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
-  integrity sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==
+"@babel/runtime@^7.8.4":
+  version "7.15.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
+  integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
   dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/parser" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    regenerator-runtime "^0.13.4"
 
-"@babel/traverse@^7.0.0", "@babel/traverse@^7.0.0-beta.42", "@babel/traverse@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.3.tgz#a826215b011c9b4f73f3a893afbc05151358bf9a"
-  integrity sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg==
+"@babel/template@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
+  integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==
   dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.8.3"
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/parser" "^7.8.3"
-    "@babel/types" "^7.8.3"
+    "@babel/code-frame" "^7.14.5"
+    "@babel/parser" "^7.14.5"
+    "@babel/types" "^7.14.5"
+
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.0.0-beta.42", "@babel/traverse@^7.14.5", "@babel/traverse@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98"
+  integrity sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==
+  dependencies:
+    "@babel/code-frame" "^7.14.5"
+    "@babel/generator" "^7.15.0"
+    "@babel/helper-function-name" "^7.14.5"
+    "@babel/helper-hoist-variables" "^7.14.5"
+    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/parser" "^7.15.0"
+    "@babel/types" "^7.15.0"
     debug "^4.1.0"
     globals "^11.1.0"
-    lodash "^4.17.13"
 
-"@babel/types@^7.0.0-beta.42", "@babel/types@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
-  integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==
+"@babel/types@^7.0.0-beta.42", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd"
+  integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==
   dependencies:
-    esutils "^2.0.2"
-    lodash "^4.17.13"
+    "@babel/helper-validator-identifier" "^7.14.9"
     to-fast-properties "^2.0.0"
 
 "@bazel/rollup@^3.5.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
-  integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.8.0.tgz#850f56176d73e3b7d99a43c7e33df21ecc6ac161"
+  integrity sha512-u63ubqYtfQhOu8Km3uYdhKa6qiLSlOKYsWwMP1xGkkXzu1hOiUznN1N7q8gCF1BV2DMy1D5IYkv+Xg4a+LEiBA==
 
 "@bazel/typescript@^3.5.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.0.tgz#605493f4f0a5297df8a7fcccb86a1a80ea2090bb"
-  integrity sha512-BtGFp4nYFkQTmnONCzomk7dkmOwaINBL3piq+lykBlcc6UxLe9iCAnZpOyPypB1ReN3k3SRNAa53x6oGScQxMg==
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.8.0.tgz#725d51a1c25e314a1d8cddb8b880ac05ba97acd4"
+  integrity sha512-4C1pLe4V7aidWqcPsWNqXFS7uHAB1nH5SUKG5uWoVv4JT9XhkNSvzzQIycMwXs2tZeCylX4KYNeNvfKrmkyFlw==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
     source-map-support "0.5.9"
     tsutils "2.27.2"
 
+"@dabh/diagnostics@^2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
+  integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==
+  dependencies:
+    colorspace "1.1.x"
+    enabled "2.0.x"
+    kuler "^2.0.0"
+
 "@mrmlnc/readdir-enhanced@^2.2.1":
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -521,50 +538,85 @@
   integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
 
 "@octokit/auth-token@^2.4.0":
+  version "2.4.5"
+  resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
+  integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==
+  dependencies:
+    "@octokit/types" "^6.0.3"
+
+"@octokit/endpoint@^6.0.1":
+  version "6.0.12"
+  resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658"
+  integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==
+  dependencies:
+    "@octokit/types" "^6.0.3"
+    is-plain-object "^5.0.0"
+    universal-user-agent "^6.0.0"
+
+"@octokit/openapi-types@^10.0.0":
+  version "10.0.0"
+  resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-10.0.0.tgz#db4335de99509021f501fc4e026e6ff495fe1e62"
+  integrity sha512-k1iO2zKuEjjRS1EJb4FwSLk+iF6EGp+ZV0OMRViQoWhQ1fZTk9hg1xccZII5uyYoiqcbC73MRBmT45y1vp2PPg==
+
+"@octokit/plugin-paginate-rest@^1.1.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc"
+  integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==
+  dependencies:
+    "@octokit/types" "^2.0.1"
+
+"@octokit/plugin-request-log@^1.0.0":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
+  integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
+
+"@octokit/plugin-rest-endpoint-methods@2.4.0":
   version "2.4.0"
-  resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f"
-  integrity sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==
+  resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e"
+  integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==
   dependencies:
-    "@octokit/types" "^2.0.0"
+    "@octokit/types" "^2.0.1"
+    deprecation "^2.3.1"
 
-"@octokit/endpoint@^5.5.0":
-  version "5.5.1"
-  resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.1.tgz#2eea81e110ca754ff2de11c79154ccab4ae16b3f"
-  integrity sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==
+"@octokit/request-error@^1.0.2":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801"
+  integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==
   dependencies:
     "@octokit/types" "^2.0.0"
-    is-plain-object "^3.0.0"
-    universal-user-agent "^4.0.0"
+    deprecation "^2.0.0"
+    once "^1.4.0"
 
-"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.0.tgz#a64d2a9d7a13555570cd79722de4a4d76371baaa"
-  integrity sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==
+"@octokit/request-error@^2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
+  integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==
   dependencies:
-    "@octokit/types" "^2.0.0"
+    "@octokit/types" "^6.0.3"
     deprecation "^2.0.0"
     once "^1.4.0"
 
 "@octokit/request@^5.2.0":
-  version "5.3.1"
-  resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.1.tgz#3a1ace45e6f88b1be4749c5da963b3a3b4a2f120"
-  integrity sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==
+  version "5.6.1"
+  resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.1.tgz#f97aff075c37ab1d427c49082fefeef0dba2d8ce"
+  integrity sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ==
   dependencies:
-    "@octokit/endpoint" "^5.5.0"
-    "@octokit/request-error" "^1.0.1"
-    "@octokit/types" "^2.0.0"
-    deprecation "^2.0.0"
-    is-plain-object "^3.0.0"
-    node-fetch "^2.3.0"
-    once "^1.4.0"
-    universal-user-agent "^4.0.0"
+    "@octokit/endpoint" "^6.0.1"
+    "@octokit/request-error" "^2.1.0"
+    "@octokit/types" "^6.16.1"
+    is-plain-object "^5.0.0"
+    node-fetch "^2.6.1"
+    universal-user-agent "^6.0.0"
 
 "@octokit/rest@^16.2.0":
-  version "16.38.3"
-  resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.38.3.tgz#d5d200f88962392f71e048e12833ea36f4e0d192"
-  integrity sha512-Ui5W4Gzv0YHe9P3KDZAuU/BkRrT88PCuuATfWBMBf4fux4nB8th8LlyVAVnHKba1s/q4umci+sNHzoFYFujPEg==
+  version "16.43.2"
+  resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.2.tgz#c53426f1e1d1044dee967023e3279c50993dd91b"
+  integrity sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==
   dependencies:
     "@octokit/auth-token" "^2.4.0"
+    "@octokit/plugin-paginate-rest" "^1.1.1"
+    "@octokit/plugin-request-log" "^1.0.0"
+    "@octokit/plugin-rest-endpoint-methods" "2.4.0"
     "@octokit/request" "^5.2.0"
     "@octokit/request-error" "^1.0.2"
     atob-lite "^2.0.0"
@@ -578,13 +630,20 @@
     once "^1.4.0"
     universal-user-agent "^4.0.0"
 
-"@octokit/types@^2.0.0":
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.1.1.tgz#77e80d1b663c5f1f829e5377b728fa3c4fe5a97d"
-  integrity sha512-89LOYH+d/vsbDX785NOfLxTW88GjNd0lWRz1DVPVsZgg9Yett5O+3MOvwo7iHgvUwbFz0mf/yPIjBkUbs4kxoQ==
+"@octokit/types@^2.0.0", "@octokit/types@^2.0.1":
+  version "2.16.2"
+  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2"
+  integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==
   dependencies:
     "@types/node" ">= 8"
 
+"@octokit/types@^6.0.3", "@octokit/types@^6.16.1":
+  version "6.26.0"
+  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.26.0.tgz#b8af298485d064ad9424cb41520541c1bf820346"
+  integrity sha512-RDxZBAFMtqs1ZPnbUu1e7ohPNfoNhTiep4fErY7tZs995BeHu369Vsh5woMIaFbllRWEZBfvTCS4hvDnMPiHrA==
+  dependencies:
+    "@octokit/openapi-types" "^10.0.0"
+
 "@polymer/esm-amd-loader@^1.0.0":
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@polymer/esm-amd-loader/-/esm-amd-loader-1.0.4.tgz#4e77f2f59b29b01e0ad02aa83d33716cddc5f9f9"
@@ -653,24 +712,36 @@
   resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
   integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
 
+"@sindresorhus/is@^4.0.0":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5"
+  integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==
+
+"@szmarczak/http-timer@^4.0.5":
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807"
+  integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==
+  dependencies:
+    defer-to-connect "^2.0.0"
+
 "@types/babel-generator@^6.25.1":
-  version "6.25.3"
-  resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.3.tgz#8f06caa12d0595a0538560abe771966d77d29286"
-  integrity sha512-pGgnuxVddKcYIc+VJkRDop7gxLhqclNKBdlsm/5Vp8d+37pQkkDK7fef8d9YYImRzw9xcojEPc18pUYnbxmjqA==
+  version "6.25.4"
+  resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.4.tgz#74eacdaa4822c4c6923e68c541144a04415ad8a1"
+  integrity sha512-Rnsen+ckop5mbl9d43bempS7i9wdTN1vytiTlmQla/YiNm6kH8kEVABVSXmp1UbnpkUV44nUCPeDQoa+Mu7ALA==
   dependencies:
     "@types/babel-types" "*"
 
 "@types/babel-traverse@^6.25.2", "@types/babel-traverse@^6.25.3":
-  version "6.25.5"
-  resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.5.tgz#6d293cf7523e48b524faa7b86dc3c488191484e5"
-  integrity sha512-WrMbwmu+MWf8FiUMbmVOGkc7bHPzndUafn1CivMaBHthBBoo0VNIcYk1KV71UovYguhsNOwf3UF5oRmkkGOU3w==
+  version "6.25.7"
+  resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.7.tgz#bc75fce23d8394534562a36a32dec94a54d11835"
+  integrity sha512-BeQiEGLnVzypzBdsexEpZAHUx+WucOMXW6srEWDkl4SegBlaCy+iBvRO+4vz6EZ+BNQg22G4MCdDdvZxf+jW5A==
   dependencies:
     "@types/babel-types" "*"
 
 "@types/babel-types@*":
-  version "7.0.7"
-  resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
-  integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==
+  version "7.0.11"
+  resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.11.tgz#263b113fa396fac4373188d73225297fb86f19a9"
+  integrity sha512-pkPtJUUY+Vwv6B1inAz55rQvivClHJxc9aVEPPmaq2cbyeMLCiDpbKpcKyX4LAwpNGi+SHBv0tHv6+0gXv0P2A==
 
 "@types/babel-types@^6.25.1":
   version "6.25.2"
@@ -678,25 +749,35 @@
   integrity sha512-+3bMuktcY4a70a0KZc8aPJlEOArPuAKQYHU5ErjkOqGJdx8xuEEVK6nWogqigBOJ8nKPxRpyCUDTCPmZ3bUxGA==
 
 "@types/babylon@^6.16.2":
-  version "6.16.5"
-  resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4"
-  integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==
+  version "6.16.6"
+  resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.6.tgz#a1e7e01567b26a5ebad321a74d10299189d8d932"
+  integrity sha512-G4yqdVlhr6YhzLXFKy5F7HtRBU8Y23+iWy7UKthMq/OSQnL1hbsoeXESQ2LY8zEDlknipDG3nRGhUC9tkwvy/w==
   dependencies:
     "@types/babel-types" "*"
 
 "@types/bluebird@*":
-  version "3.5.29"
-  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6"
-  integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==
+  version "3.5.36"
+  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.36.tgz#00d9301d4dc35c2f6465a8aec634bb533674c652"
+  integrity sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==
 
 "@types/body-parser@*":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897"
-  integrity sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
+  integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==
   dependencies:
     "@types/connect" "*"
     "@types/node" "*"
 
+"@types/cacheable-request@^6.0.1":
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"
+  integrity sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==
+  dependencies:
+    "@types/http-cache-semantics" "*"
+    "@types/keyv" "*"
+    "@types/node" "*"
+    "@types/responselike" "*"
+
 "@types/chai-subset@^1.3.0":
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
@@ -705,9 +786,9 @@
     "@types/chai" "*"
 
 "@types/chai@*":
-  version "4.2.7"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d"
-  integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==
+  version "4.2.21"
+  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650"
+  integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==
 
 "@types/chalk@^0.4.30":
   version "0.4.31"
@@ -722,22 +803,18 @@
     chalk "*"
 
 "@types/clean-css@*":
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.1.tgz#cb0134241ec5e6ede1b5344bc829668fd9871a8d"
-  integrity sha512-A1HQhQ0hkvqqByJMgg+Wiv9p9XdoYEzuwm11SVo1mX2/4PSdhjcrUlilJQoqLscIheC51t1D5g+EFWCXZ2VTQQ==
+  version "4.2.5"
+  resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.5.tgz#69ce62cc13557c90ca40460133f672dc52ceaf89"
+  integrity sha512-NEzjkGGpbs9S9fgC4abuBvTpVwE3i+Acu9BBod3PUyjDVZcNsGx61b8r2PphR61QGPnn0JHVs5ey6/I4eTrkxw==
   dependencies:
     "@types/node" "*"
+    source-map "^0.6.0"
 
 "@types/clone@^0.1.29", "@types/clone@^0.1.30":
   version "0.1.30"
   resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614"
   integrity sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=
 
-"@types/color-name@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
-  integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
-
 "@types/compression@^0.0.33":
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.33.tgz#95dc733a2339aa846381d7f1377792d2553dc27d"
@@ -746,21 +823,21 @@
     "@types/express" "*"
 
 "@types/connect@*":
-  version "3.4.33"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
-  integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
+  version "3.4.35"
+  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
+  integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
   dependencies:
     "@types/node" "*"
 
 "@types/content-type@^1.1.0":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07"
-  integrity sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.5.tgz#aa02dca40864749a9e2bf0161a6216da57e3ede5"
+  integrity sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==
 
 "@types/cssbeautify@^0.3.1":
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/@types/cssbeautify/-/cssbeautify-0.3.1.tgz#8e0bee8f7decb952250da0caebe05e30591c17ef"
-  integrity sha1-jgvuj33suVIlDaDK6+BeMFkcF+8=
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@types/cssbeautify/-/cssbeautify-0.3.2.tgz#8a76207cd980d3e7b29b4b6dea1f4ed861285615"
+  integrity sha512-b3PXlFAcS4gvGr2pDz0NoZEBo3MMQe8Ozy6+Mvm3XIEcHS4oQstvCnnCofBZD/0tQgxSzkYbW+cD3yD4yaKTxQ==
 
 "@types/del@^3.0.0":
   version "3.0.1"
@@ -780,9 +857,9 @@
   integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==
 
 "@types/estree@*":
-  version "0.0.42"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.42.tgz#8d0c1f480339efedb3e46070e22dd63e0430dd11"
-  integrity sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==
+  version "0.0.50"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
+  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
 
 "@types/events@*":
   version "3.0.0"
@@ -794,21 +871,23 @@
   resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
   integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
 
-"@types/express-serve-static-core@*":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf"
-  integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==
+"@types/express-serve-static-core@^4.17.18":
+  version "4.17.24"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07"
+  integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==
   dependencies:
     "@types/node" "*"
+    "@types/qs" "*"
     "@types/range-parser" "*"
 
 "@types/express@*", "@types/express@^4.0.30", "@types/express@^4.0.36":
-  version "4.17.2"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c"
-  integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==
+  version "4.17.13"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
+  integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
   dependencies:
     "@types/body-parser" "*"
-    "@types/express-serve-static-core" "*"
+    "@types/express-serve-static-core" "^4.17.18"
+    "@types/qs" "*"
     "@types/serve-static" "*"
 
 "@types/fast-levenshtein@0.0.1":
@@ -831,24 +910,23 @@
     form-data "*"
 
 "@types/freeport@^1.0.19":
-  version "1.0.21"
-  resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.21.tgz#73f6543ed67d3ca3fff97b985591598b7092066f"
-  integrity sha1-c/ZUPtZ9PKP/+XuYVZFZi3CSBm8=
+  version "1.0.22"
+  resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.22.tgz#dbe627a20cb30c17c8aaaba09332e1d14cc2281f"
+  integrity sha512-UGg4s5PDPXZXkkrHarU1l6WDbULxN3g7xUEtdbNf9HQhU/JnCj1G1/xZHZmQjC0uWqN1LlB0R0xOlk3k5svgTQ==
 
 "@types/glob-stream@*":
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
-  integrity sha512-RHv6ZQjcTncXo3thYZrsbAVwoy4vSKosSWhuhuQxLOTv74OJuFQxXkmUuZCr3q9uNBEVCvIzmZL/FeRNbHZGUg==
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.1.tgz#c792d8d1514278ff03cad5689aba4c4ab4fbc805"
+  integrity sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==
   dependencies:
     "@types/glob" "*"
     "@types/node" "*"
 
-"@types/glob@*":
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
-  integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
+"@types/glob@*", "@types/glob@^7.1.1":
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672"
+  integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==
   dependencies:
-    "@types/events" "*"
     "@types/minimatch" "*"
     "@types/node" "*"
 
@@ -876,10 +954,15 @@
     "@types/relateurl" "*"
     "@types/uglify-js" "*"
 
+"@types/http-cache-semantics@*":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
+  integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
+
 "@types/inquirer@*":
-  version "6.5.0"
-  resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be"
-  integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac"
+  integrity sha512-HhxyLejTHMfohAuhRun4csWigAMjXTmRyiJTU1Y/I1xmggikFMkOUoMQRlFm+zQcPEGHSs3io/0FAmNZf8EymQ==
   dependencies:
     "@types/through" "*"
     rxjs "^6.4.0"
@@ -897,10 +980,17 @@
   resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-0.2.0.tgz#6f24ee48731d31168ea510610d6dd15e5fc9c6ff"
   integrity sha1-byTuSHMdMRaOpRBhDW3RXl/Jxv8=
 
+"@types/keyv@*":
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.2.tgz#5d97bb65526c20b6e0845f6b0d2ade4f28604ee5"
+  integrity sha512-/FvAK2p4jQOaJ6CGDHJTqZcUtbZe820qIeTg7o0Shg7drB4JHeL+V/dhSaly7NXx6u8eSee+r7coT+yuJEvDLg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/launchpad@^0.6.0":
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
-  integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.1.tgz#9a5f285128598f5e0cb4a5db3e458e1b7cf07f35"
+  integrity sha512-kQ1a7PwzJelwwOIw1SABmW5OsbCRPvdjps0J84MahGsEKzN89StrPyrWCMWfwpONR3ZqSxDeblxS+8WznIBEGw==
 
 "@types/long@^4.0.0":
   version "4.0.1"
@@ -914,15 +1004,20 @@
   dependencies:
     "@types/node" "*"
 
-"@types/mime@*", "@types/mime@^2.0.0":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
-  integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
+"@types/mime@^1":
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
+  integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
 
-"@types/minimatch@*", "@types/minimatch@^3.0.1":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
-  integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+"@types/mime@^2.0.0":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
+  integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
+
+"@types/minimatch@*", "@types/minimatch@^3.0.1", "@types/minimatch@^3.0.3":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
 
 "@types/mz@0.0.29":
   version "0.0.29"
@@ -940,29 +1035,29 @@
     "@types/node" "*"
 
 "@types/node@*", "@types/node@>= 8":
-  version "13.5.0"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.5.0.tgz#4e498dbf355795a611a87ae5ef811a8660d42662"
-  integrity sha512-Onhn+z72D2O2Pb2ql2xukJ55rglumsVo1H6Fmyi8mlU9SvKdBk/pUSUAiBY/d9bAOF7VVWajX3sths/+g6ZiAQ==
+  version "16.7.10"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.10.tgz#7aa732cc47341c12a16b7d562f519c2383b6d4fc"
+  integrity sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==
 
 "@types/node@6.0.*":
   version "6.0.118"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.118.tgz#8014a9b1dee0b72b4d7cd142563f1af21241c3a2"
   integrity sha512-N33cKXGSqhOYaPiT4xUGsYlPPDwFtQM/6QxJxuMXA/7BcySW+lkn2yigWP7vfs4daiL/7NJNU6DMCqg5N4B+xQ==
 
-"@types/node@^10.1.0":
-  version "10.17.42"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.42.tgz#90dd71b26fe4f4e2929df6b07e72ef2e9648a173"
-  integrity sha512-HElxYF7C/MSkuvlaHB2c+82zhXiuO49Cq056Dol8AQuTph7oJtduo2n6J8rFa+YhJyNgQ/Lm20ZaxqD0vxU0+Q==
-
-"@types/node@^10.17.12":
-  version "10.17.24"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944"
-  integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA==
+"@types/node@^10.1.0", "@types/node@^10.17.12":
+  version "10.17.60"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
+  integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
 
 "@types/node@^4.0.30":
-  version "4.9.4"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-4.9.4.tgz#75ef91733afaa856b01e12da6ecf48aa9d5e221f"
-  integrity sha512-nKoiCZ87x6+fs26bNHjy07AQt6f46nFEitGH0P9JmWbY6tEyum6LLfLf7SIsKFh4DnBWsyUM2gYhaQAt+aA0Sw==
+  version "4.9.5"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-4.9.5.tgz#a3785db96b07a4b56466cc99fd624838746f2e25"
+  integrity sha512-+8fpgbXsbATKRF2ayAlYhPl2E9MPdLjrnK/79ZEpyPJ+k7dZwJm9YM8FK+l4rqL//xHk7PgQhGwz6aA2ckxbCQ==
+
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
+  integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
 
 "@types/opn@^3.0.28":
   version "3.0.28"
@@ -979,17 +1074,17 @@
     "@types/parse5-sax-parser" "*"
 
 "@types/parse5-sax-parser@*":
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/@types/parse5-sax-parser/-/parse5-sax-parser-5.0.1.tgz#f1e26e82bb09e48cb0c16ff6d1e88aea1e538fd5"
-  integrity sha512-wBEwg10aACLggnb44CwzAA27M1Jrc/8TR16zA61/rKO5XZoi7JSfLjdpXbshsm7wOlM6hpfvwygh40rzM2RsQQ==
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/@types/parse5-sax-parser/-/parse5-sax-parser-5.0.2.tgz#4cdca0f8bc0ce71b17e27b96e7ca9b5f79e861ff"
+  integrity sha512-EQtGoduLbdMmS4N27g6wcXdCCJ70dWYemfogWuumYg+JmzRqwYvTRAbGOYFortSHtS/qRzRCFwcP3ixy62RsdA==
   dependencies:
     "@types/node" "*"
     "@types/parse5" "*"
 
 "@types/parse5@*":
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.2.tgz#a877a4658f8238c8266faef300ae41c84d72ec8a"
-  integrity sha512-BOl+6KDs4ItndUWUFchy3aEqGdHhw0BC4Uu+qoDonN/f0rbUnJbm71Ulj8Tt9jLFRaAxPLKvdS1bBLfx1qXR9g==
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.1.tgz#f8ae4fbcd2b9ba4ff934698e28778961f9cb22ca"
+  integrity sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==
 
 "@types/parse5@^0.0.31":
   version "0.0.31"
@@ -1006,9 +1101,9 @@
     "@types/node" "*"
 
 "@types/parse5@^4.0.0":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-4.0.0.tgz#26dd73df171a69be517395d294c7af2ae0cd2579"
-  integrity sha512-OaBwNFk6dO8gbdfWut41VYiD5Fmj3Yi24cr/oGCXFXCjT2fteSQx2l3kx/phuQvBte/F54ajN2uDQF5MRwupGw==
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-4.0.1.tgz#ec53c3f948f284be08454f622ba83765efdd06d7"
+  integrity sha512-CgJIkoNLclXUUg5cEo/SybyhxgVuKGqGcw8BPBpkKWX7wGcNfDlO2Ot+5O9u5E6k3NDg6RmJzm5w5N2prDIE8Q==
   dependencies:
     "@types/node" "*"
 
@@ -1018,21 +1113,26 @@
   integrity sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==
 
 "@types/pem@^1.8.1":
-  version "1.9.5"
-  resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.5.tgz#cd5548b5e0acb4b41a9e21067e9fcd8c57089c99"
-  integrity sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ==
+  version "1.9.6"
+  resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.6.tgz#c3686832e935947fdd9d848dec3b8fe830068de7"
+  integrity sha512-IC67SxacM9fxEi/w7hf98dTun83OwUMeLMo1NS2gE0wdM9MHeg73iH/Pp9nB02OUCQ7Zb2UuKE/IpFCmQw9jxw==
   dependencies:
     "@types/node" "*"
 
+"@types/qs@*":
+  version "6.9.7"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
+  integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
+
 "@types/range-parser@*":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
-  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
+  integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
 
 "@types/relateurl@*":
-  version "0.2.28"
-  resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
-  integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y=
+  version "0.2.29"
+  resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.29.tgz#68ccecec3d4ffdafb9c577fe764f912afc050fe6"
+  integrity sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==
 
 "@types/request@2.0.3":
   version "2.0.3"
@@ -1070,6 +1170,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/responselike@*", "@types/responselike@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
+  integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
+  dependencies:
+    "@types/node" "*"
+
 "@types/rimraf@^0.0.28":
   version "0.0.28"
   resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-0.0.28.tgz#5562519bc7963caca8abf7f128cae3b594d41d06"
@@ -1159,9 +1266,9 @@
     "@types/rx-core-binding" "*"
 
 "@types/rx@*":
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/@types/rx/-/rx-4.1.1.tgz#598fc94a56baed975f194574e0f572fd8e627a48"
-  integrity sha1-WY/JSla67ZdfGUV04PVy/Y5iekg=
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/@types/rx/-/rx-4.1.2.tgz#a4061b3d72b03cf11a38d69e2022a17334c54dc0"
+  integrity sha512-1r8ZaT26Nigq7o4UBGl+aXB2UMFUIdLPP/8bLIP0x3d0pZL46ybKKjhWKaJQWIkLl5QCLD0nK3qTOO1QkwdFaA==
   dependencies:
     "@types/rx-core" "*"
     "@types/rx-core-binding" "*"
@@ -1182,17 +1289,17 @@
   integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
 
 "@types/serve-static@*", "@types/serve-static@^1.7.31":
-  version "1.13.3"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
-  integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==
+  version "1.13.10"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
+  integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
   dependencies:
-    "@types/express-serve-static-core" "*"
-    "@types/mime" "*"
+    "@types/mime" "^1"
+    "@types/node" "*"
 
 "@types/spdy@^3.4.1":
-  version "3.4.4"
-  resolved "https://registry.yarnpkg.com/@types/spdy/-/spdy-3.4.4.tgz#3282fd4ad8c4603aa49f7017dd520a08a345b2bc"
-  integrity sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==
+  version "3.4.5"
+  resolved "https://registry.yarnpkg.com/@types/spdy/-/spdy-3.4.5.tgz#194dc132312ddcd31e8053789ae83a7bb32a8aaf"
+  integrity sha512-/33fIRK/aqkKNxg9BSjpzt1ucmvPremgeDywm9z2C2mOlIh5Ljjvgc3UhQHqwXsSLDLHPT9jlsnrjKQ1XiVJzA==
   dependencies:
     "@types/node" "*"
 
@@ -1211,28 +1318,26 @@
     "@types/node" "*"
 
 "@types/ua-parser-js@^0.7.31":
-  version "0.7.33"
-  resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.33.tgz#4a92089511574e12928a7cb6b99a01831acd1dd7"
-  integrity sha512-ngUKcHnytUodUCL7C6EZ+lVXUjTMQb+9p/e1JjV5tN9TVzS98lHozWEFRPY1QcCdwFeMsmVWfZ3DPPT/udCyIw==
+  version "0.7.36"
+  resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
+  integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==
 
 "@types/uglify-js@*":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
-  integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==
+  version "3.13.1"
+  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea"
+  integrity sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==
   dependencies:
     source-map "^0.6.1"
 
 "@types/update-notifier@^1.0.0":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.3.tgz#3c7ee1921af6f16149cdcaef356baf57d7a0b806"
-  integrity sha512-BLStNhP2DFF7funARwTcoD6tetRte8NK3Sc59mn7GNALCN975jOlKX3dGvsFxXr/HwQMxxCuRn9IWB3WQ7odHQ==
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.4.tgz#ce73d597bd399d5df4544fe136a79c2b9fe41958"
+  integrity sha512-smyU9GTDitojg87woCcLNCdPnUfNx4LHRBWf+aWmHsAgE1kaCDhhcu84W+dFymAKL1yKDsq2JFWKkR2K6WjJfw==
 
 "@types/uuid@^3.4.3":
-  version "3.4.6"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016"
-  integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==
-  dependencies:
-    "@types/node" "*"
+  version "3.4.10"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.10.tgz#637d3c8431f112edf6728ac9bdfadfe029540f48"
+  integrity sha512-BgeaZuElf7DEYZhWYDTc/XcLZXdVgFkVSTa13BqKvbnmUrxr3TJFKofUxCtDO9UQOdhnV+HPOESdHiHKZOJV1A==
 
 "@types/vinyl-fs@0.0.28":
   version "0.0.28"
@@ -1244,18 +1349,18 @@
     "@types/vinyl" "*"
 
 "@types/vinyl-fs@^2.4.8":
-  version "2.4.11"
-  resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz#b98119b8bb2494141eaf649b09fbfeb311161206"
-  integrity sha512-2OzQSfIr9CqqWMGqmcERE6Hnd2KY3eBVtFaulVo3sJghplUcaeMdL9ZjEiljcQQeHjheWY9RlNmumjIAvsBNaA==
+  version "2.4.12"
+  resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.12.tgz#7b4673d9b4d5a874c8652d10f0f0265479014c8e"
+  integrity sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==
   dependencies:
     "@types/glob-stream" "*"
     "@types/node" "*"
     "@types/vinyl" "*"
 
 "@types/vinyl@*", "@types/vinyl@^2.0.0":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a"
-  integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.5.tgz#52d3b850a4ed494aaad51e96708834c500c8d5cd"
+  integrity sha512-1m6uReH8R/RuLVQGvTT/4LlWq67jZEUxp+FBHt0hYv2BT7TUwFbKI0wa7JZVEU/XtlcnX1QcTuZ36es4rGj7jg==
   dependencies:
     "@types/expect" "^1.20.4"
     "@types/node" "*"
@@ -1285,6 +1390,14 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
   integrity sha512-eLH04VBMpuZGzBIhOnUjECcQPEPcmfhWEijW9u1B5I+2PPYdWf3vWUExdDxu4Y3GljRSTCOlWnGtS9tpzmXMyQ==
 
+JSONStream@^1.2.1, JSONStream@^1.3.5:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
+  integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
+  dependencies:
+    jsonparse "^1.2.0"
+    through ">=2.2.7 <3"
+
 accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -1311,25 +1424,32 @@
   integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
 
 acorn@^5.5.0:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
+  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
 
 acorn@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
-  integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
+  version "7.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
+  integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
 adm-zip@~0.4.3:
-  version "0.4.13"
-  resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a"
-  integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==
+  version "0.4.16"
+  resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365"
+  integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==
 
 after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
   integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
 
+agent-base@6:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+  dependencies:
+    debug "4"
+
 agent-base@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
@@ -1337,10 +1457,10 @@
   dependencies:
     es6-promisify "^5.0.0"
 
-ajv@^6.5.5:
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9"
-  integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==
+ajv@^6.12.3:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
   dependencies:
     fast-deep-equal "^3.1.1"
     fast-json-stable-stringify "^2.0.0"
@@ -1373,10 +1493,12 @@
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
   integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
 
-ansi-escapes@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
-  integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+ansi-escapes@^4.2.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+  integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+  dependencies:
+    type-fest "^0.21.3"
 
 ansi-regex@^2.0.0:
   version "2.1.1"
@@ -1388,10 +1510,10 @@
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
   integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
 
-ansi-regex@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
-  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
+ansi-regex@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+  integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
 
 ansi-styles@^2.2.1:
   version "2.2.1"
@@ -1406,11 +1528,10 @@
     color-convert "^1.9.0"
 
 ansi-styles@^4.1.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
-  integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
   dependencies:
-    "@types/color-name" "^1.1.1"
     color-convert "^2.0.1"
 
 ansi-styles@~1.0.0:
@@ -1501,7 +1622,7 @@
   dependencies:
     typical "^2.6.1"
 
-array-back@^3.0.1:
+array-back@^3.0.1, array-back@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
   integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
@@ -1511,6 +1632,11 @@
   resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
   integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=
 
+array-differ@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b"
+  integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==
+
 array-find-index@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -1521,13 +1647,18 @@
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
   integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
 
-array-union@^1.0.1:
+array-union@^1.0.1, array-union@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
   integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
   dependencies:
     array-uniq "^1.0.1"
 
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
 array-uniq@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@@ -1553,6 +1684,11 @@
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
 
+arrify@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
+  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
+
 asn1@~0.2.3:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -1575,18 +1711,23 @@
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
-async-limiter@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
-  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+async@0.9.x:
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
+  integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
 
-async@^2.0.0, async@^2.0.1, async@^2.1.2, async@^2.4.1, async@^2.6.0, async@^2.6.1, async@^2.6.2, async@^2.6.3:
+async@^2.0.0, async@^2.0.1, async@^2.1.2, async@^2.4.1, async@^2.6.0, async@^2.6.2, async@^2.6.3:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
   integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
   dependencies:
     lodash "^4.17.14"
 
+async@^3.1.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
+  integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
+
 async@~0.2.9:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -1613,9 +1754,16 @@
   integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
 
 aws4@^1.8.0:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
-  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
+  integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
+
+axios@^0.21.1:
+  version "0.21.1"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
+  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
+  dependencies:
+    follow-redirects "^1.10.0"
 
 babel-code-frame@^6.26.0:
   version "6.26.0"
@@ -1682,10 +1830,10 @@
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-dynamic-import-node@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
-  integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
+babel-plugin-dynamic-import-node@^2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
+  integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==
   dependencies:
     object.assign "^4.1.0"
 
@@ -1902,24 +2050,24 @@
   integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
 
 balanced-match@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
-  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base64-arraybuffer@0.1.5:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
-  integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
+base64-arraybuffer@0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
+  integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
 
 base64-js@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
   integrity sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=
 
-base64-js@^1.0.2:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
-  integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
 
 base64id@2.0.0:
   version "2.0.0"
@@ -1947,16 +2095,9 @@
     tweetnacl "^0.14.3"
 
 before-after-hook@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
-  integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
-
-better-assert@~1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
-  integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
-  dependencies:
-    callsite "1.0.0"
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
+  integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
 
 binary-extensions@^1.0.0:
   version "1.13.1"
@@ -1964,9 +2105,9 @@
   integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
 
 binaryextensions@^2.1.2:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.2.0.tgz#e7c6ba82d4f5f5758c26078fe8eea28881233311"
-  integrity sha512-bHhs98rj/7i/RZpCSJ3uk55pLXOItjIrh2sRQZSM6OoktScX+LxJzvlU+FELp9j3TdcddTmmYArLSGptCTwjuw==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
+  integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
 
 bindings@^1.5.0:
   version "1.5.0"
@@ -1976,27 +2117,21 @@
     file-uri-to-path "1.0.0"
 
 bl@^1.0.0:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
-  integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7"
+  integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==
   dependencies:
     readable-stream "^2.3.5"
     safe-buffer "^5.1.1"
 
-bl@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493"
-  integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==
+bl@^4.0.3:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
   dependencies:
-    readable-stream "^2.3.5"
-    safe-buffer "^5.1.1"
-
-bl@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
-  integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==
-  dependencies:
-    readable-stream "^3.0.1"
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
 
 blob@0.0.5:
   version "0.0.5"
@@ -2020,25 +2155,28 @@
     type-is "~1.6.17"
 
 bower-config@^1.4.0, bower-config@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.1.tgz#85fd9df367c2b8dbbd0caa4c5f2bad40cd84c2cc"
-  integrity sha1-hf2d82fCuNu9DKpMXyutQM2Ewsw=
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.3.tgz#3454fecdc5f08e7aa9cc6d556e492be0669689ae"
+  integrity sha512-MVyyUk3d1S7d2cl6YISViwJBc2VXCkxF5AUFykvN0PQj5FsUiMNSgAYTso18oRFfyZ6XEtjrgg9MAaufHbOwNw==
   dependencies:
     graceful-fs "^4.1.3"
+    minimist "^0.2.1"
     mout "^1.0.0"
-    optimist "^0.6.1"
     osenv "^0.1.3"
     untildify "^2.1.0"
+    wordwrap "^0.0.3"
 
 bower-json@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/bower-json/-/bower-json-0.8.1.tgz#96c14723241ae6466a9c52e16caa32623a883843"
-  integrity sha1-lsFHIyQa5kZqnFLhbKoyYjqIOEM=
+  version "0.8.4"
+  resolved "https://registry.yarnpkg.com/bower-json/-/bower-json-0.8.4.tgz#9c3b375870dcd9581350c1f403f6383dbf6a18b1"
+  integrity sha512-mMKghvq9ivbuzSsY5nrOLnDtZIJMUCpysqbGaGW3mj88JAcuSi8ZAzIt34vNZjohy0aR9VXLwgPTZGnBX2Vpjg==
   dependencies:
-    deep-extend "^0.4.0"
-    ext-name "^3.0.0"
+    deep-extend "^0.5.1"
+    ends-with "^0.2.0"
+    ext-list "^2.0.0"
     graceful-fs "^4.1.3"
     intersect "^1.0.1"
+    sort-keys-length "^1.0.0"
 
 bower-logger@^0.2.2:
   version "0.2.2"
@@ -2046,9 +2184,9 @@
   integrity sha1-Ob4H6Xmy/I4DqUY0IF7ZQiNz04E=
 
 bower@^1.8.8:
-  version "1.8.8"
-  resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.8.tgz#82544be34a33aeae7efb8bdf9905247b2cffa985"
-  integrity sha512-1SrJnXnkP9soITHptSO+ahx3QKp3cVzn8poI6ujqc5SeOkg5iqM1pK9H+DSc2OQ8SnO0jC/NG4Ur/UIwy7574A==
+  version "1.8.12"
+  resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.12.tgz#44cfca2a5e04b8d9a066621e24c8b179d8ac321e"
+  integrity sha512-u1xy9SrwwoPlgjuHNjhV+YUPVdqyBj2ALBxuzeIUKXaPI2i2xypGgxqXkuHcITGdi5yBj5JuXgyMvgiWiS1S3Q==
 
 boxen@^0.6.0:
   version "0.6.0"
@@ -2126,10 +2264,21 @@
   dependencies:
     pako "~0.2.0"
 
+browserslist@^4.16.6:
+  version "4.16.8"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
+  integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==
+  dependencies:
+    caniuse-lite "^1.0.30001251"
+    colorette "^1.3.0"
+    electron-to-chromium "^1.3.811"
+    escalade "^3.1.1"
+    node-releases "^1.1.75"
+
 browserstack@^1.2.0:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.3.tgz#93ab48799a12ef99dbd074dd595410ddb196a7ac"
-  integrity sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg==
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3"
+  integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==
   dependencies:
     https-proxy-agent "^2.2.1"
 
@@ -2162,22 +2311,22 @@
   integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
 
 buffer-from@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
-  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
-buffer@^5.1.0:
-  version "5.4.3"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115"
-  integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==
+buffer@^5.1.0, buffer@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
   dependencies:
-    base64-js "^1.0.2"
-    ieee754 "^1.1.4"
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
 
 builtin-modules@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
-  integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
+  integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==
 
 busboy@^0.2.11:
   version "0.2.14"
@@ -2212,16 +2361,37 @@
     union-value "^1.0.0"
     unset-value "^1.0.0"
 
+cacheable-lookup@^5.0.3:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
+  integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
+
+cacheable-request@^7.0.1:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27"
+  integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==
+  dependencies:
+    clone-response "^1.0.2"
+    get-stream "^5.1.0"
+    http-cache-semantics "^4.0.0"
+    keyv "^4.0.0"
+    lowercase-keys "^2.0.0"
+    normalize-url "^6.0.1"
+    responselike "^2.0.0"
+
+call-bind@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.2"
+
 call-me-maybe@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
   integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
 
-callsite@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
-  integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
-
 camel-case@3.0.x:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
@@ -2255,6 +2425,11 @@
   dependencies:
     "@types/node" "^4.0.30"
 
+caniuse-lite@^1.0.30001251:
+  version "1.0.30001252"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz#cb16e4e3dafe948fc4a9bb3307aea054b912019a"
+  integrity sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==
+
 capture-stack-trace@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
@@ -2265,10 +2440,10 @@
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
-chalk@*:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
-  integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
+chalk@*, chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
   dependencies:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
@@ -2307,7 +2482,7 @@
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
-charenc@~0.0.1:
+charenc@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
   integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
@@ -2329,9 +2504,9 @@
     fsevents "^1.0.0"
 
 chownr@^1.0.1:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
-  integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
 
 ci-info@^1.5.0:
   version "1.6.0"
@@ -2349,9 +2524,9 @@
     static-extend "^0.1.1"
 
 clean-css@4.2.x:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.2.tgz#8519abda724b3e759bc79d196369906925d81a3f"
-  integrity sha512-yKycArwReQXbOD/3pmsPmt6p7oUBww8MisDabL2pCUWkbVONvCJoBdCjgY4ZVQmKX5juz/JB9oDcP6XzGUpjwQ==
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
+  integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
   dependencies:
     source-map "~0.6.0"
 
@@ -2372,30 +2547,51 @@
   dependencies:
     restore-cursor "^1.0.1"
 
-cli-cursor@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
-  integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+cli-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+  integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
   dependencies:
-    restore-cursor "^2.0.0"
+    restore-cursor "^3.1.0"
 
 cli-table@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
-  integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM=
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc"
+  integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==
   dependencies:
     colors "1.0.3"
 
 cli-width@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
-  integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
+  integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
+
+cli-width@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
+  integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
 
 clone-buffer@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
   integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
 
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+clone-response@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
+  integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
+  dependencies:
+    mimic-response "^1.0.0"
+
 clone-stats@^0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
@@ -2463,9 +2659,9 @@
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 color-string@^1.5.2:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
-  integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
+  integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
@@ -2478,10 +2674,10 @@
     color-convert "^1.9.1"
     color-string "^1.5.2"
 
-colornames@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96"
-  integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
+colorette@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
+  integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==
 
 colors@1.0.3:
   version "1.0.3"
@@ -2519,11 +2715,11 @@
     typical "^2.6.0"
 
 command-line-args@^5.0.2:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.1.1.tgz#88e793e5bb3ceb30754a86863f0401ac92fd369a"
-  integrity sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.0.tgz#087b02748272169741f1fd7c785b295df079b9be"
+  integrity sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==
   dependencies:
-    array-back "^3.0.1"
+    array-back "^3.1.0"
     find-replace "^3.0.0"
     lodash.camelcase "^4.3.0"
     typical "^4.0.0"
@@ -2561,7 +2757,7 @@
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
   integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
 
-commander@^2.19.0, commander@^2.20.0:
+commander@^2.20.0, commander@^2.20.3:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -2586,7 +2782,7 @@
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
   integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
 
-component-emitter@^1.2.1:
+component-emitter@^1.2.1, component-emitter@~1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
   integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@@ -2657,11 +2853,11 @@
     xdg-basedir "^2.0.0"
 
 configstore@^3.0.0:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
-  integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.5.tgz#e9af331fadc14dabd544d3e7e76dc446a09a530f"
+  integrity sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==
   dependencies:
-    dot-prop "^4.1.0"
+    dot-prop "^4.2.1"
     graceful-fs "^4.1.2"
     make-dir "^1.0.0"
     unique-string "^1.0.0"
@@ -2681,9 +2877,9 @@
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
 convert-source-map@^1.1.1, convert-source-map@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
-  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
+  integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
   dependencies:
     safe-buffer "~5.1.1"
 
@@ -2692,31 +2888,36 @@
   resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
   integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
 
-cookie@0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
-  integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
-
 cookie@0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
   integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
 
+cookie@~0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+  integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
 copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
 core-js@^2.4.0, core-js@^2.4.1:
-  version "2.6.11"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
-  integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+  version "2.6.12"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
-core-util-is@1.0.2, core-util-is@~1.0.0:
+core-util-is@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
+core-util-is@~1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
 cors@^2.8.4:
   version "2.8.5"
   resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
@@ -2776,7 +2977,16 @@
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-crypt@~0.0.1:
+cross-spawn@^7.0.0, cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+crypt@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
   integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
@@ -2814,7 +3024,7 @@
   dependencies:
     array-find-index "^1.0.1"
 
-dargs@^6.0.0:
+dargs@^6.0.0, dargs@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/dargs/-/dargs-6.1.0.tgz#1f3b9b56393ecf8caa7cbfd6c31496ffcfb9b272"
   integrity sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==
@@ -2838,17 +3048,17 @@
   dependencies:
     ms "2.0.0"
 
-debug@^3.0.0, debug@^3.1.0:
-  version "3.2.6"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
-  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
   dependencies:
-    ms "^2.1.1"
+    ms "2.1.2"
 
-debug@^4.1.0, debug@^4.1.1, debug@~4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
-  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+debug@^3.1.0:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
   dependencies:
     ms "^2.1.1"
 
@@ -2859,6 +3069,13 @@
   dependencies:
     ms "2.0.0"
 
+debug@~4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+  dependencies:
+    ms "^2.1.1"
+
 decamelize@^1.1.2:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -2876,17 +3093,34 @@
   dependencies:
     mimic-response "^1.0.0"
 
-deep-extend@^0.4.0, deep-extend@~0.4.1:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
-  integrity sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=
+decompress-response@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
+  dependencies:
+    mimic-response "^3.1.0"
+
+deep-extend@^0.5.1:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
+  integrity sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==
 
 deep-extend@^0.6.0, deep-extend@~0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
   integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
 
-define-properties@^1.1.2:
+deep-extend@~0.4.1:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+  integrity sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=
+
+defer-to-connect@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
+  integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==
+
+define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -2937,7 +3171,7 @@
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
   integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
 
-deprecation@^2.0.0:
+deprecation@^2.0.0, deprecation@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
   integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
@@ -2972,18 +3206,9 @@
     repeating "^2.0.0"
 
 detect-node@^2.0.3:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
-  integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
-
-diagnostics@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a"
-  integrity sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==
-  dependencies:
-    colorspace "1.1.x"
-    enabled "1.0.x"
-    kuler "1.0.x"
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
+  integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
 
 dicer@0.2.5:
   version "0.2.5"
@@ -3003,6 +3228,11 @@
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
   integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
 
+diff@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
+  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+
 dir-glob@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
@@ -3011,6 +3241,13 @@
     arrify "^1.0.1"
     path-type "^3.0.0"
 
+dir-glob@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
+  integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
+  dependencies:
+    path-type "^3.0.0"
+
 doctrine@^2.0.2:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -3052,13 +3289,22 @@
   dependencies:
     is-obj "^1.0.0"
 
-dot-prop@^4.1.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
-  integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
+dot-prop@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4"
+  integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==
   dependencies:
     is-obj "^1.0.0"
 
+download-stats@^0.3.4:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/download-stats/-/download-stats-0.3.4.tgz#67ea0c32f14acd9f639da704eef509684ba2dae7"
+  integrity sha512-ic2BigbyUWx7/CBbsfGjf71zUNZB4edBGC3oRliSzsoNmvyVx3Ycfp1w3vp2Y78Ee0eIIkjIEO5KzW0zThDGaA==
+  dependencies:
+    JSONStream "^1.2.1"
+    lazy-cache "^2.0.1"
+    moment "^2.15.1"
+
 duplexer2@^0.1.2, duplexer2@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -3090,9 +3336,9 @@
     safer-buffer "^2.1.0"
 
 editions@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.0.tgz#47f2d5309340bce93ab5eb6ad755b9e90ff825e4"
-  integrity sha512-jeXYwHPKbitU1l14dWlsl5Nm+b1Hsm7VX73BsrQ4RVwEcAQQIPFHTZAbVtuIGxZBrpdT2FXd8lbtrNBrzZxIsA==
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698"
+  integrity sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==
   dependencies:
     errlop "^2.0.0"
     semver "^6.3.0"
@@ -3102,22 +3348,37 @@
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
-ejs@^2.5.9:
+ejs@^2.5.9, ejs@^2.6.1:
   version "2.7.4"
   resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
   integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
 
+ejs@^3.1.5:
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a"
+  integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==
+  dependencies:
+    jake "^10.6.1"
+
+electron-to-chromium@^1.3.811:
+  version "1.3.826"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.826.tgz#dbe356b1546b39d83bcd47e675a9c5f61dadaed2"
+  integrity sha512-bpLc4QU4B8PYmdO4MSu2ZBTMD8lAaEXRS43C09lB31BvYwuk9UxgBRXbY5OJBw7VuMGcg2MZG5FyTaP9u4PQnw==
+
 emitter-component@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
   integrity sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=
 
-enabled@1.0.x:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/enabled/-/enabled-1.0.2.tgz#965f6513d2c2d1c5f4652b64a2e3396467fc2f93"
-  integrity sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=
-  dependencies:
-    env-variable "0.0.x"
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+enabled@2.0.x:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
+  integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
 
 encodeurl@~1.0.2:
   version "1.0.2"
@@ -3136,55 +3397,50 @@
   resolved "https://registry.yarnpkg.com/ends-with/-/ends-with-0.2.0.tgz#2f9da98d57a50cfda4571ce4339000500f4e6b8a"
   integrity sha1-L52pjVelDP2kVxzkM5AAUA9Oa4o=
 
-engine.io-client@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
-  integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
+engine.io-client@~3.5.0:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.2.tgz#0ef473621294004e9ceebe73cef0af9e36f2f5fa"
+  integrity sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==
   dependencies:
-    component-emitter "1.2.1"
+    component-emitter "~1.3.0"
     component-inherit "0.0.3"
-    debug "~4.1.0"
+    debug "~3.1.0"
     engine.io-parser "~2.2.0"
     has-cors "1.1.0"
     indexof "0.0.1"
-    parseqs "0.0.5"
-    parseuri "0.0.5"
-    ws "~6.1.0"
-    xmlhttprequest-ssl "~1.5.4"
+    parseqs "0.0.6"
+    parseuri "0.0.6"
+    ws "~7.4.2"
+    xmlhttprequest-ssl "~1.6.2"
     yeast "0.1.2"
 
 engine.io-parser@~2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed"
-  integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
+  integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
   dependencies:
     after "0.8.2"
     arraybuffer.slice "~0.0.7"
-    base64-arraybuffer "0.1.5"
+    base64-arraybuffer "0.1.4"
     blob "0.0.5"
     has-binary2 "~1.0.2"
 
-engine.io@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
-  integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==
+engine.io@~3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b"
+  integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==
   dependencies:
     accepts "~1.3.4"
     base64id "2.0.0"
-    cookie "0.3.1"
+    cookie "~0.4.1"
     debug "~4.1.0"
     engine.io-parser "~2.2.0"
-    ws "^7.1.2"
-
-env-variable@0.0.x:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88"
-  integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==
+    ws "~7.4.2"
 
 errlop@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.0.0.tgz#52b97d35da1b0795e2647b5d2d3a46d17776f55a"
-  integrity sha512-z00WIrQhtOMUnjdTG0O4f6hMG64EVccVDBy2WwgjcF8S4UB1exGYuc2OFwmdQmsJwLQVEIHWHPCz/omXXgAZHw==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b"
+  integrity sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==
 
 error-ex@^1.2.0, error-ex@^1.3.1:
   version "1.3.2"
@@ -3213,9 +3469,14 @@
     es6-promise "^4.0.3"
 
 es6-promisify@^6.0.0:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.2.tgz#525c23725b8510f5f1f2feb5a1fbad93a93e29b4"
-  integrity sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg==
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621"
+  integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
 
 escape-html@^1.0.3, escape-html@~1.0.3:
   version "1.0.3"
@@ -3251,9 +3512,9 @@
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 
 eventemitter3@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
-  integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
 
 execa@^0.7.0:
   version "0.7.0"
@@ -3281,6 +3542,21 @@
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
+execa@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
+  integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
+  dependencies:
+    cross-spawn "^7.0.0"
+    get-stream "^5.0.0"
+    human-signals "^1.1.1"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.0"
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+    strip-final-newline "^2.0.0"
+
 exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -3370,16 +3646,6 @@
   dependencies:
     mime-db "^1.28.0"
 
-ext-name@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-3.0.0.tgz#07e4418737cb1f513c32c6ea48d8b8c8e0471abb"
-  integrity sha1-B+RBhzfLH1E8MsbqSNi4yOBHGrs=
-  dependencies:
-    ends-with "^0.2.0"
-    ext-list "^2.0.0"
-    meow "^3.1.0"
-    sort-keys-length "^1.0.0"
-
 extend-shallow@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -3450,11 +3716,11 @@
   integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
 
 fast-deep-equal@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
-  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
-fast-glob@^2.0.2:
+fast-glob@^2.0.2, fast-glob@^2.2.6:
   version "2.2.7"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
   integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
@@ -3477,9 +3743,9 @@
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
 fast-safe-stringify@^2.0.4:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
-  integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f"
+  integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==
 
 fd-slicer@~1.1.0:
   version "1.1.0"
@@ -3500,6 +3766,11 @@
   resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd"
   integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==
 
+fecha@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce"
+  integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==
+
 figures@^1.3.5:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
@@ -3508,10 +3779,10 @@
     escape-string-regexp "^1.0.5"
     object-assign "^4.1.0"
 
-figures@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
-  integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
+figures@^3.0.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
   dependencies:
     escape-string-regexp "^1.0.5"
 
@@ -3520,6 +3791,13 @@
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
   integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
 
+filelist@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
+  integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==
+  dependencies:
+    minimatch "^3.0.4"
+
 filename-regex@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
@@ -3633,12 +3911,15 @@
   dependencies:
     readable-stream "^2.0.2"
 
-follow-redirects@^1.0.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb"
-  integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==
-  dependencies:
-    debug "^3.0.0"
+fn.name@1.x.x:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
+  integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
+
+follow-redirects@^1.0.0, follow-redirects@^1.10.0:
+  version "1.14.2"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.2.tgz#cecb825047c00f5e66b142f90fed4f515dec789b"
+  integrity sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==
 
 for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
@@ -3663,9 +3944,9 @@
   integrity sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=
 
 form-data@*:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
-  integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
   dependencies:
     asynckit "^0.4.0"
     combined-stream "^1.0.8"
@@ -3687,10 +3968,10 @@
   dependencies:
     samsam "1.x"
 
-forwarded@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
-  integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+forwarded@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+  integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
 
 fragment-cache@^0.2.1:
   version "0.2.1"
@@ -3725,27 +4006,36 @@
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
 fsevents@^1.0.0:
-  version "1.2.11"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3"
-  integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+  integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
   dependencies:
     bindings "^1.5.0"
     nan "^2.12.1"
 
-fsevents@~2.1.2:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
-  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
 
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
-gensync@^1.0.0-beta.1:
-  version "1.0.0-beta.1"
-  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
-  integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
+gensync@^1.0.0-beta.2:
+  version "1.0.0-beta.2"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-intrinsic@^1.0.2:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+  integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
 
 get-stdin@^4.0.1:
   version "4.0.1"
@@ -3764,6 +4054,13 @@
   dependencies:
     pump "^3.0.0"
 
+get-stream@^5.0.0, get-stream@^5.1.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+  dependencies:
+    pump "^3.0.0"
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -3776,6 +4073,14 @@
   dependencies:
     assert-plus "^1.0.0"
 
+gh-got@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-5.0.0.tgz#ee95be37106fd8748a96f8d1db4baea89e1bfa8a"
+  integrity sha1-7pW+NxBv2HSKlvjR20uuqJ4b+oo=
+  dependencies:
+    got "^6.2.0"
+    is-plain-obj "^1.1.0"
+
 gh-got@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0"
@@ -3784,6 +4089,13 @@
     got "^7.0.0"
     is-plain-obj "^1.1.0"
 
+github-username@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/github-username/-/github-username-3.0.0.tgz#0a772219b3130743429f2456d0bdd3db55dce7b1"
+  integrity sha1-CnciGbMTB0NCnyRW0L3T21Xc57E=
+  dependencies:
+    gh-got "^5.0.0"
+
 github-username@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
@@ -3856,9 +4168,9 @@
     path-is-absolute "^1.0.0"
 
 glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
-  version "7.1.6"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
-  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+  integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
   dependencies:
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
@@ -3958,6 +4270,37 @@
     pify "^3.0.0"
     slash "^1.0.0"
 
+globby@^9.2.0:
+  version "9.2.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
+  integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==
+  dependencies:
+    "@types/glob" "^7.1.1"
+    array-union "^1.0.2"
+    dir-glob "^2.2.2"
+    fast-glob "^2.2.6"
+    glob "^7.1.3"
+    ignore "^4.0.3"
+    pify "^4.0.1"
+    slash "^2.0.0"
+
+got@^11.8.2:
+  version "11.8.2"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
+  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
+  dependencies:
+    "@sindresorhus/is" "^4.0.0"
+    "@szmarczak/http-timer" "^4.0.5"
+    "@types/cacheable-request" "^6.0.1"
+    "@types/responselike" "^1.0.0"
+    cacheable-lookup "^5.0.3"
+    cacheable-request "^7.0.1"
+    decompress-response "^6.0.0"
+    http2-wrapper "^1.0.0-beta.5.2"
+    lowercase-keys "^2.0.0"
+    p-cancelable "^2.0.0"
+    responselike "^2.0.0"
+
 got@^5.0.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
@@ -3979,7 +4322,7 @@
     unzip-response "^1.0.2"
     url-parse-lax "^1.0.0"
 
-got@^6.7.1:
+got@^6.2.0, got@^6.7.1:
   version "6.7.1"
   resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
   integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
@@ -4017,17 +4360,24 @@
     url-to-options "^1.0.1"
 
 graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.2.0:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
-  integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 
-grouped-queue@^0.3.0, grouped-queue@^0.3.3:
+grouped-queue@^0.3.0:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c"
   integrity sha1-wWfSpTGcWg4JZO9qJbfC34mWyFw=
   dependencies:
     lodash "^4.17.2"
 
+grouped-queue@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-1.1.0.tgz#63e3f9ca90af952269d1d40879e41221eacc74cb"
+  integrity sha512-rZOFKfCqLhsu5VqjBjEWiwrYqJR07KxIkH4mLZlNlGDfntbb4FbMyGFP14TlvRPrU9S3Hnn/sgxbC5ZeN0no3Q==
+  dependencies:
+    lodash "^4.17.15"
+
 gulp-if@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/gulp-if/-/gulp-if-2.0.2.tgz#a497b7e7573005041caa2bc8b7dda3c80444d629"
@@ -4056,9 +4406,9 @@
     vinyl "^1.0.0"
 
 gunzip-maybe@^1.3.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.1.tgz#39c72ed89d1b49ba708e18776500488902a52027"
-  integrity sha512-qtutIKMthNJJgeHQS7kZ9FqDq59/Wn0G2HYCRNjpup7yKfVI6/eqwpmroyZGFoCYaG+sW6psNVb4zoLADHpp2g==
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"
+  integrity sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==
   dependencies:
     browserify-zlib "^0.1.4"
     is-deflate "^1.0.0"
@@ -4077,12 +4427,12 @@
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
   integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
 
-har-validator@~5.1.0:
-  version "5.1.3"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
-  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+har-validator@~5.1.0, har-validator@~5.1.3:
+  version "5.1.5"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
+  integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
   dependencies:
-    ajv "^6.5.5"
+    ajv "^6.12.3"
     har-schema "^2.0.0"
 
 has-ansi@^2.0.0:
@@ -4124,10 +4474,10 @@
   resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455"
   integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==
 
-has-symbols@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
-  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+has-symbols@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+  integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
 
 has-to-string-tag-x@^1.2.0:
   version "1.4.1"
@@ -4167,6 +4517,13 @@
     is-number "^3.0.0"
     kind-of "^4.0.0"
 
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
 he@1.2.x:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -4180,9 +4537,9 @@
     parse-passwd "^1.0.0"
 
 hosted-git-info@^2.1.4:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
-  integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
+  version "2.8.9"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 
 hpack.js@^2.1.6:
   version "2.1.6"
@@ -4207,6 +4564,11 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
+http-cache-semantics@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
+  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
+
 http-deceiver@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -4255,9 +4617,9 @@
     micromatch "^2.3.11"
 
 http-proxy@^1.16.2:
-  version "1.18.0"
-  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
-  integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
+  version "1.18.1"
+  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
+  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
   dependencies:
     eventemitter3 "^4.0.0"
     follow-redirects "^1.0.0"
@@ -4272,6 +4634,14 @@
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
+http2-wrapper@^1.0.0-beta.5.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
+  integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==
+  dependencies:
+    quick-lru "^5.1.1"
+    resolve-alpn "^1.0.0"
+
 https-proxy-agent@^2.2.1:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
@@ -4280,13 +4650,18 @@
     agent-base "^4.3.0"
     debug "^3.1.0"
 
-https-proxy-agent@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
-  integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
+https-proxy-agent@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+  integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
   dependencies:
-    agent-base "^4.3.0"
-    debug "^3.1.0"
+    agent-base "6"
+    debug "4"
+
+human-signals@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
 
 iconv-lite@0.4.24, iconv-lite@^0.4.24:
   version "0.4.24"
@@ -4295,16 +4670,21 @@
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.4:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
-  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
 
 ignore@^3.3.5:
   version "3.3.10"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
   integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
 
+ignore@^4.0.3:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
 import-lazy@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@@ -4340,7 +4720,7 @@
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -4351,9 +4731,9 @@
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
 ini@^1.3.4, ini@~1.3.0:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
 inquirer@^1.0.2:
   version "1.2.3"
@@ -4375,29 +4755,29 @@
     strip-ansi "^3.0.0"
     through "^2.3.6"
 
-inquirer@^6.0.0:
-  version "6.5.2"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
-  integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
+inquirer@^7.1.0:
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
+  integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
   dependencies:
-    ansi-escapes "^3.2.0"
-    chalk "^2.4.2"
-    cli-cursor "^2.1.0"
-    cli-width "^2.0.0"
+    ansi-escapes "^4.2.1"
+    chalk "^4.1.0"
+    cli-cursor "^3.1.0"
+    cli-width "^3.0.0"
     external-editor "^3.0.3"
-    figures "^2.0.0"
-    lodash "^4.17.12"
-    mute-stream "0.0.7"
-    run-async "^2.2.0"
-    rxjs "^6.4.0"
-    string-width "^2.1.0"
-    strip-ansi "^5.1.0"
+    figures "^3.0.0"
+    lodash "^4.17.19"
+    mute-stream "0.0.8"
+    run-async "^2.4.0"
+    rxjs "^6.6.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
     through "^2.3.6"
 
 interpret@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
-  integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
 
 intersect@^1.0.1:
   version "1.0.1"
@@ -4411,10 +4791,10 @@
   dependencies:
     loose-envify "^1.0.0"
 
-ipaddr.js@1.9.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
-  integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
+ipaddr.js@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
 is-accessor-descriptor@^0.1.6:
   version "0.1.6"
@@ -4447,7 +4827,7 @@
   dependencies:
     binary-extensions "^1.0.0"
 
-is-buffer@^1.1.5, is-buffer@~1.1.1:
+is-buffer@^1.1.5, is-buffer@~1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@@ -4459,6 +4839,13 @@
   dependencies:
     ci-info "^1.5.0"
 
+is-core-module@^2.2.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
+  integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
+  dependencies:
+    has "^1.0.3"
+
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -4531,11 +4918,9 @@
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
 
 is-finite@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
-  integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
-  dependencies:
-    number-is-nan "^1.0.0"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
+  integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
 
 is-fullwidth-code-point@^1.0.0:
   version "1.0.0"
@@ -4549,6 +4934,11 @@
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
   integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
 
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
 is-glob@^2.0.0, is-glob@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -4618,9 +5008,9 @@
   integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
 
 is-object@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
-  integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf"
+  integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
 
 is-path-cwd@^1.0.0:
   version "1.0.0"
@@ -4653,12 +5043,10 @@
   dependencies:
     isobject "^3.0.1"
 
-is-plain-object@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
-  integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
-  dependencies:
-    isobject "^4.0.0"
+is-plain-object@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
+  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
 
 is-posix-bracket@^0.1.0:
   version "0.1.1"
@@ -4666,20 +5054,15 @@
   integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=
 
 is-potential-custom-element-name@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
-  integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
+  integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
 
 is-primitive@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
   integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
 
-is-promise@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
-  integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
-
 is-redirect@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
@@ -4702,12 +5085,17 @@
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
   integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
 
+is-stream@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
 is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
 
-is-utf8@^0.2.0:
+is-utf8@^0.2.0, is-utf8@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
   integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
@@ -4749,6 +5137,11 @@
   dependencies:
     buffer-alloc "^1.2.0"
 
+isbinaryfile@^4.0.0:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf"
+  integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -4766,17 +5159,12 @@
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
   integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
 
-isobject@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
-  integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
-
 isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
-istextorbinary@^2.2.1:
+istextorbinary@^2.2.1, istextorbinary@^2.5.1:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.6.0.tgz#60776315fb0fa3999add276c02c69557b9ca28ab"
   integrity sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==
@@ -4793,6 +5181,16 @@
     has-to-string-tag-x "^1.2.0"
     is-object "^1.0.1"
 
+jake@^10.6.1:
+  version "10.8.2"
+  resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
+  integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==
+  dependencies:
+    async "0.9.x"
+    chalk "^2.4.2"
+    filelist "^1.0.1"
+    minimatch "^3.0.4"
+
 jest-worker@^24.9.0:
   version "24.9.0"
   resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
@@ -4831,11 +5229,21 @@
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
 
+json-buffer@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
 json-parse-better-errors@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
 
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -4856,17 +5264,22 @@
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
-json5@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"
-  integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==
+json5@^2.1.2:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
+  integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
   dependencies:
-    minimist "^1.2.0"
+    minimist "^1.2.5"
+
+jsonparse@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+  integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
 
 jsonschema@^1.1.0, jsonschema@^1.1.1:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.5.tgz#bab69d97fa28946aec0a56a9cc266d23fe80ae61"
-  integrity sha512-kVTF+08x25PQ0CjuVc0gRM9EUPb0Fe9Ln/utFOgcdxEIOHuU7ooBk/UPTd7t1M91pP35m0MU1T8M5P7vP1bRRw==
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2"
+  integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==
 
 jsprim@^1.2.2:
   version "1.4.1"
@@ -4878,6 +5291,13 @@
     json-schema "0.2.3"
     verror "1.10.0"
 
+keyv@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
+  integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
+  dependencies:
+    json-buffer "3.0.1"
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -4902,12 +5322,10 @@
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
-kuler@1.0.x:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6"
-  integrity sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==
-  dependencies:
-    colornames "^1.1.1"
+kuler@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
+  integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
 
 latest-version@^2.0.0:
   version "2.0.0"
@@ -4937,6 +5355,13 @@
     rimraf "^3.0.0"
     underscore "^1.8.3"
 
+lazy-cache@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264"
+  integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=
+  dependencies:
+    set-getter "^0.1.0"
+
 lazy-req@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac"
@@ -4949,6 +5374,11 @@
   dependencies:
     readable-stream "^2.0.5"
 
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -5018,6 +5448,16 @@
   resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
   integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
 
+lodash.mapvalues@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
+  integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
+
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
 lodash.padend@^4.6.1:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
@@ -5058,15 +5498,10 @@
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^3.0.0, lodash@^3.10.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-  integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
-
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.3.0:
-  version "4.17.15"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
-  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@4.17.21, lodash@^3.0.0, lodash@^3.10.1, lodash@^4.0.0, lodash@^4.11.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.3.0:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
 log-symbols@^1.0.0, log-symbols@^1.0.1:
   version "1.0.2"
@@ -5093,14 +5528,14 @@
     ms "^2.1.1"
     triple-beam "^1.2.0"
 
-logform@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360"
-  integrity sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==
+logform@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
+  integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
   dependencies:
     colors "^1.2.1"
     fast-safe-stringify "^2.0.4"
-    fecha "^2.3.3"
+    fecha "^4.2.0"
     ms "^2.1.1"
     triple-beam "^1.3.0"
 
@@ -5139,6 +5574,11 @@
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
   integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
 
+lowercase-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
+  integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+
 lru-cache@^4.0.1, lru-cache@^4.0.2:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@@ -5147,10 +5587,17 @@
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
 macos-release@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
-  integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2"
+  integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==
 
 magic-string@^0.22.4:
   version "0.22.5"
@@ -5166,6 +5613,13 @@
   dependencies:
     pify "^3.0.0"
 
+make-dir@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
+  integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+  dependencies:
+    semver "^6.0.0"
+
 map-cache@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -5196,13 +5650,13 @@
   integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
 
 md5@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
-  integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
+  integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
   dependencies:
-    charenc "~0.0.1"
-    crypt "~0.0.1"
-    is-buffer "~1.1.1"
+    charenc "0.0.2"
+    crypt "0.0.2"
+    is-buffer "~1.1.6"
 
 media-typer@0.3.0:
   version "0.3.0"
@@ -5226,16 +5680,50 @@
     through2 "^2.0.0"
     vinyl "^2.0.1"
 
-mem-fs@^1.1.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.1.3.tgz#b8ae8d2e3fcb6f5d3f9165c12d4551a065d989cc"
-  integrity sha1-uK6NLj/Lb10/kWXBLUVRoGXZicw=
+mem-fs-editor@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-6.0.0.tgz#d63607cf0a52fe6963fc376c6a7aa52db3edabab"
+  integrity sha512-e0WfJAMm8Gv1mP5fEq/Blzy6Lt1VbLg7gNnZmZak7nhrBTibs+c6nQ4SKs/ZyJYHS1mFgDJeopsLAv7Ow0FMFg==
   dependencies:
-    through2 "^2.0.0"
-    vinyl "^1.1.0"
-    vinyl-file "^2.0.0"
+    commondir "^1.0.1"
+    deep-extend "^0.6.0"
+    ejs "^2.6.1"
+    glob "^7.1.4"
+    globby "^9.2.0"
+    isbinaryfile "^4.0.0"
+    mkdirp "^0.5.0"
+    multimatch "^4.0.0"
+    rimraf "^2.6.3"
+    through2 "^3.0.1"
+    vinyl "^2.2.0"
 
-meow@^3.1.0, meow@^3.7.0:
+mem-fs-editor@^7.0.1:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-7.1.0.tgz#2a16f143228df87bf918874556723a7ee73bfe88"
+  integrity sha512-BH6QEqCXSqGeX48V7zu+e3cMwHU7x640NB8Zk8VNvVZniz+p4FK60pMx/3yfkzo6miI6G3a8pH6z7FeuIzqrzA==
+  dependencies:
+    commondir "^1.0.1"
+    deep-extend "^0.6.0"
+    ejs "^3.1.5"
+    glob "^7.1.4"
+    globby "^9.2.0"
+    isbinaryfile "^4.0.0"
+    mkdirp "^1.0.0"
+    multimatch "^4.0.0"
+    rimraf "^3.0.0"
+    through2 "^3.0.2"
+    vinyl "^2.2.1"
+
+mem-fs@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.2.0.tgz#5f29b2d02a5875cd14cd836c388385892d556cde"
+  integrity sha512-b8g0jWKdl8pM0LqAPdK9i8ERL7nYrzmJfRhxMiWH2uYdfYnb7uXnmwVb0ZGe7xyEl4lj+nLIU3yf4zPUT+XsVQ==
+  dependencies:
+    through2 "^3.0.0"
+    vinyl "^2.0.1"
+    vinyl-file "^3.0.0"
+
+meow@^3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
   integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
@@ -5269,9 +5757,9 @@
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
 merge2@^1.2.3:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
-  integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
 methods@~1.1.2:
   version "1.1.2"
@@ -5316,17 +5804,17 @@
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-mime-db@1.43.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0:
-  version "1.43.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
-  integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
+mime-db@1.49.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0:
+  version "1.49.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
+  integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
 
 mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
-  version "2.1.26"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
-  integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
+  version "2.1.32"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
+  integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
   dependencies:
-    mime-db "1.43.0"
+    mime-db "1.49.0"
 
 mime@1.4.1:
   version "1.4.1"
@@ -5339,20 +5827,25 @@
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
 
 mime@^2.3.1:
-  version "2.4.4"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
-  integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
+  integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
 
-mimic-fn@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
-  integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
 mimic-response@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
+mimic-response@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+
 minimalistic-assert@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@@ -5372,20 +5865,15 @@
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@0.0.8:
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-  integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+minimist@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.1.tgz#827ba4e7593464e7c221e8c5bed930904ee2c455"
+  integrity sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg==
 
-minimist@^1.1.3, minimist@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
-
-minimist@~0.0.1:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-  integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
+minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
 mixin-deep@^1.2.0:
   version "1.3.2"
@@ -5395,12 +5883,22 @@
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@^0.5.0, mkdirp@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
-  integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
+  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
   dependencies:
-    minimist "0.0.8"
+    minimist "^1.2.5"
+
+mkdirp@^1.0.0, mkdirp@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+moment@^2.15.1, moment@^2.24.0:
+  version "2.29.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
 
 mout@^1.0.0:
   version "1.2.2"
@@ -5417,20 +5915,25 @@
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
-ms@^2.1.1:
+ms@2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
 multer@^1.3.0:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a"
-  integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b"
+  integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==
   dependencies:
     append-field "^1.0.0"
     busboy "^0.2.11"
     concat-stream "^1.5.2"
-    mkdirp "^0.5.1"
+    mkdirp "^0.5.4"
     object-assign "^4.1.1"
     on-finished "^2.3.0"
     type-is "^1.6.4"
@@ -5446,6 +5949,17 @@
     arrify "^1.0.0"
     minimatch "^3.0.0"
 
+multimatch@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3"
+  integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==
+  dependencies:
+    "@types/minimatch" "^3.0.3"
+    array-differ "^3.0.0"
+    array-union "^2.1.0"
+    arrify "^2.0.1"
+    minimatch "^3.0.4"
+
 multipipe@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-1.0.2.tgz#cc13efd833c9cda99f224f868461b8e1a3fd939d"
@@ -5459,10 +5973,10 @@
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db"
   integrity sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=
 
-mute-stream@0.0.7:
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-  integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
+mute-stream@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+  integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
 
 mz@^2.4.0, mz@^2.6.0:
   version "2.7.0"
@@ -5474,9 +5988,9 @@
     thenify-all "^1.0.0"
 
 nan@^2.12.1:
-  version "2.14.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
-  integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
+  integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -5517,10 +6031,15 @@
   dependencies:
     lower-case "^1.1.1"
 
-node-fetch@^2.3.0:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
-  integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+node-fetch@^2.6.0, node-fetch@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+
+node-releases@^1.1.75:
+  version "1.1.75"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe"
+  integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==
 
 node-status-codes@^1.0.0:
   version "1.0.0"
@@ -5535,7 +6054,7 @@
     chalk "~0.4.0"
     underscore "~1.6.0"
 
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
   integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -5557,6 +6076,23 @@
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
+normalize-url@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
+npm-api@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/npm-api/-/npm-api-1.0.1.tgz#3def9b51afedca57db14ca0c970d92442d21c9c5"
+  integrity sha512-4sITrrzEbPcr0aNV28QyOmgn6C9yKiF8k92jn4buYAK8wmA5xo1qL3II5/gT1r7wxbXBflSduZ2K3FbtOrtGkA==
+  dependencies:
+    JSONStream "^1.3.5"
+    clone-deep "^4.0.1"
+    download-stats "^0.3.4"
+    moment "^2.24.0"
+    node-fetch "^2.6.0"
+    paged-request "^2.0.1"
+
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -5564,6 +6100,13 @@
   dependencies:
     path-key "^2.0.0"
 
+npm-run-path@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -5579,11 +6122,6 @@
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
 
-object-component@0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
-  integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
-
 object-copy@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@@ -5593,7 +6131,7 @@
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-keys@^1.0.11, object-keys@^1.0.12:
+object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -5606,14 +6144,14 @@
     isobject "^3.0.0"
 
 object.assign@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
-  integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
   dependencies:
-    define-properties "^1.1.2"
-    function-bind "^1.1.1"
-    has-symbols "^1.0.0"
-    object-keys "^1.0.11"
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    has-symbols "^1.0.1"
+    object-keys "^1.1.1"
 
 object.omit@^2.0.0:
   version "2.0.1"
@@ -5659,22 +6197,24 @@
   dependencies:
     wrappy "1"
 
-one-time@0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e"
-  integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=
+one-time@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
+  integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
+  dependencies:
+    fn.name "1.x.x"
 
 onetime@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
   integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
 
-onetime@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
-  integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+onetime@^5.1.0:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
   dependencies:
-    mimic-fn "^1.0.0"
+    mimic-fn "^2.1.0"
 
 opn@^3.0.2:
   version "3.0.3"
@@ -5683,14 +6223,6 @@
   dependencies:
     object-assign "^4.0.1"
 
-optimist@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
-  integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
-  dependencies:
-    minimist "~0.0.1"
-    wordwrap "~0.0.2"
-
 ordered-read-streams@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b"
@@ -5735,15 +6267,20 @@
   resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
   integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==
 
+p-cancelable@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
+  integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
+
 p-finally@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
   integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
 
 p-limit@^2.0.0:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
-  integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
   dependencies:
     p-try "^2.0.0"
 
@@ -5791,6 +6328,13 @@
     registry-url "^3.0.3"
     semver "^5.1.0"
 
+paged-request@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/paged-request/-/paged-request-2.0.2.tgz#4d621a08b8d6bee4440a0a92112354eeece5b5b0"
+  integrity sha512-NWrGqneZImDdcMU/7vMcAOo1bIi5h/pmpJqe7/jdsy85BA/s5MSaU/KlpxwW/IVPmIwBcq2uKPrBWWhEWhtxag==
+  dependencies:
+    axios "^0.21.1"
+
 pako@~0.2.0:
   version "0.2.9"
   resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@@ -5828,6 +6372,16 @@
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
 
+parse-json@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
 parse-passwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
@@ -5863,19 +6417,15 @@
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
   integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
-parseqs@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
-  integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
-  dependencies:
-    better-assert "~1.0.0"
+parseqs@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
+  integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
 
-parseuri@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
-  integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
-  dependencies:
-    better-assert "~1.0.0"
+parseuri@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
+  integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
 
 parseurl@~1.3.3:
   version "1.3.3"
@@ -5919,10 +6469,15 @@
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
   integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
 
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
 path-parse@^1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
-  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
 path-to-regexp@0.1.7:
   version "0.1.7"
@@ -6312,15 +6867,10 @@
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
   integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=
 
-pretty-bytes@^5.1.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"
-  integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==
-
-private@^0.1.6:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
-  integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+pretty-bytes@^5.1.0, pretty-bytes@^5.2.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
+  integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
 
 process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
   version "2.0.1"
@@ -6352,22 +6902,22 @@
     long "^4.0.0"
 
 proxy-addr@~2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
-  integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+  integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
   dependencies:
-    forwarded "~0.1.2"
-    ipaddr.js "1.9.0"
+    forwarded "0.2.0"
+    ipaddr.js "1.9.1"
 
 pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
   integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 
-psl@^1.1.24:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
-  integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
+psl@^1.1.24, psl@^1.1.28:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
 
 pump@^1.0.0:
   version "1.0.3"
@@ -6407,7 +6957,7 @@
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
   integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
 
-punycode@^2.1.0:
+punycode@^2.1.0, punycode@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -6427,6 +6977,11 @@
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
   integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 
+quick-lru@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
+  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
+
 randomatic@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
@@ -6436,6 +6991,13 @@
     kind-of "^6.0.0"
     math-random "^1.0.1"
 
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
 range-parser@~1.2.0, range-parser@~1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -6469,7 +7031,7 @@
     pinkie-promise "^2.0.0"
     readable-stream "^2.0.0"
 
-read-chunk@^3.0.0:
+read-chunk@^3.0.0, read-chunk@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca"
   integrity sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==
@@ -6493,6 +7055,14 @@
     find-up "^3.0.0"
     read-pkg "^3.0.0"
 
+read-pkg-up@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-5.0.0.tgz#b6a6741cb144ed3610554f40162aa07a6db621b8"
+  integrity sha512-XBQjqOBtTzyol2CpsQOw8LHV0XbDZVG7xMMjmXAJomlVY03WOBRmYgDJETlvcg0H63AJvPRwT7GFi5rvOzUOKg==
+  dependencies:
+    find-up "^3.0.0"
+    read-pkg "^5.0.0"
+
 read-pkg@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -6511,6 +7081,16 @@
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
+read-pkg@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
 readable-stream@1.1.x:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -6521,10 +7101,10 @@
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-"readable-stream@2 || 3", readable-stream@^3.0.1, readable-stream@^3.1.1, readable-stream@^3.4.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606"
-  integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==
+"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
   dependencies:
     inherits "^2.0.3"
     string_decoder "^1.1.1"
@@ -6540,7 +7120,7 @@
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
+readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
   integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -6582,29 +7162,34 @@
   resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
   integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=
 
-regenerate-unicode-properties@^8.1.0:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
-  integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==
+regenerate-unicode-properties@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
+  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
   dependencies:
     regenerate "^1.4.0"
 
 regenerate@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
-  integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
+  integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
 
 regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
-regenerator-transform@^0.14.0:
-  version "0.14.1"
-  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb"
-  integrity sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==
+regenerator-runtime@^0.13.4:
+  version "0.13.9"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
+  integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+
+regenerator-transform@^0.14.2:
+  version "0.14.5"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
+  integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==
   dependencies:
-    private "^0.1.6"
+    "@babel/runtime" "^7.8.4"
 
 regex-cache@^0.4.2:
   version "0.4.4"
@@ -6621,17 +7206,17 @@
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexpu-core@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6"
-  integrity sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==
+regexpu-core@^4.7.1:
+  version "4.7.1"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
+  integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
   dependencies:
     regenerate "^1.4.0"
-    regenerate-unicode-properties "^8.1.0"
-    regjsgen "^0.5.0"
-    regjsparser "^0.6.0"
+    regenerate-unicode-properties "^8.2.0"
+    regjsgen "^0.5.1"
+    regjsparser "^0.6.4"
     unicode-match-property-ecmascript "^1.0.4"
-    unicode-match-property-value-ecmascript "^1.1.0"
+    unicode-match-property-value-ecmascript "^1.2.0"
 
 registry-auth-token@^3.0.1:
   version "3.4.0"
@@ -6648,15 +7233,15 @@
   dependencies:
     rc "^1.0.1"
 
-regjsgen@^0.5.0:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
-  integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
+regjsgen@^0.5.1:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
+  integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
 
-regjsparser@^0.6.0:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.2.tgz#fd62c753991467d9d1ffe0a9f67f27a529024b96"
-  integrity sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==
+regjsparser@^0.6.4:
+  version "0.6.9"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6"
+  integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==
   dependencies:
     jsesc "~0.5.0"
 
@@ -6671,9 +7256,9 @@
   integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
 
 repeat-element@^1.1.2:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
-  integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
+  integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
 
 repeat-string@^1.5.2, repeat-string@^1.6.1:
   version "1.6.1"
@@ -6693,11 +7278,11 @@
   integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
 
 replace-ext@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
-  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
+  integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
 
-request@2.88.0, request@^2.72.0, request@^2.85.0:
+request@2.88.0:
   version "2.88.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
   integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
@@ -6723,6 +7308,32 @@
     tunnel-agent "^0.6.0"
     uuid "^3.3.2"
 
+request@^2.72.0, request@^2.85.0:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
 requirejs@^2.3.4:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
@@ -6733,6 +7344,11 @@
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
+resolve-alpn@^1.0.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
+  integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==
+
 resolve-dir@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e"
@@ -6754,13 +7370,21 @@
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.3.2, resolve@^1.5.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5"
-  integrity sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==
+resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.5.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
   dependencies:
+    is-core-module "^2.2.0"
     path-parse "^1.0.6"
 
+responselike@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
+  integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
+  dependencies:
+    lowercase-keys "^2.0.0"
+
 restore-cursor@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@@ -6769,12 +7393,12 @@
     exit-hook "^1.0.0"
     onetime "^1.0.0"
 
-restore-cursor@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
-  integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+restore-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+  integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
   dependencies:
-    onetime "^2.0.0"
+    onetime "^5.1.0"
     signal-exit "^3.0.2"
 
 ret@~0.1.10:
@@ -6782,7 +7406,7 @@
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
-rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -6790,9 +7414,9 @@
     glob "^7.1.3"
 
 rimraf@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b"
-  integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
   dependencies:
     glob "^7.1.3"
 
@@ -6815,14 +7439,14 @@
     rollup-pluginutils "^2.8.1"
 
 rollup-plugin-terser@^5.1.3:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.2.0.tgz#ba758adf769347b7f1eaf9ef35978d2e207dccc7"
-  integrity sha512-jQI+nYhtDBc9HFRBz8iGttQg7li9klmzR62RG2W2nN6hJ/FI2K2ItYQ7kJ7/zn+vs+BP1AEccmVRjRN989I+Nw==
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-5.3.1.tgz#8c650062c22a8426c64268548957463bf981b413"
+  integrity sha512-1pkwkervMJQGFYvM9nscrUoncPwiKR/K+bHdjv6PFgRo3cgPHoRT83y2Aa3GvINj4539S15t/tpFPb775TDs6w==
   dependencies:
     "@babel/code-frame" "^7.5.5"
     jest-worker "^24.9.0"
     rollup-pluginutils "^2.8.2"
-    serialize-javascript "^2.1.2"
+    serialize-javascript "^4.0.0"
     terser "^4.6.2"
 
 rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
@@ -6833,37 +7457,35 @@
     estree-walker "^0.6.1"
 
 rollup@^1.3.0:
-  version "1.30.0"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.30.0.tgz#ae9c893804e8eaa8f8f74b0aaf7e7fb4374a9d01"
-  integrity sha512-ANcmfaSQwpcJtZUTA0ZMNBtFcQ1B4A5FldlNqEK0WdWm9sHSKu93ffa2KV1ux8HA/yKIV/ZARV28m7rNdXJgEw==
+  version "1.32.1"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4"
+  integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==
   dependencies:
     "@types/estree" "*"
     "@types/node" "*"
     acorn "^7.1.0"
 
 rollup@^2.3.4:
-  version "2.35.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.35.1.tgz#e6bc8d10893556a638066f89e8c97f422d03968c"
-  integrity sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA==
+  version "2.56.3"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.56.3.tgz#b63edadd9851b0d618a6d0e6af8201955a77aeff"
+  integrity sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==
   optionalDependencies:
-    fsevents "~2.1.2"
+    fsevents "~2.3.2"
 
-run-async@^2.0.0, run-async@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
-  integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
-  dependencies:
-    is-promise "^2.1.0"
+run-async@^2.0.0, run-async@^2.2.0, run-async@^2.4.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
+  integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
 
 rx@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
   integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=
 
-rxjs@^6.4.0:
-  version "6.5.4"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
-  integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
+rxjs@^6.4.0, rxjs@^6.6.0:
+  version "6.6.7"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
   dependencies:
     tslib "^1.9.0"
 
@@ -6872,10 +7494,10 @@
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
-  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
 safe-regex@^1.1.0:
   version "1.1.0"
@@ -6895,13 +7517,13 @@
   integrity sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==
 
 sauce-connect-launcher@^1.0.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.1.tgz#31137f57b0f7176e1c0525b7fb09c6da746647cf"
-  integrity sha512-vIf9qDol3q2FlYzrKt0dr3kvec6LSjX2WS+/mVnAJIhqh1evSkPKCR2AzcJrnSmx9Xt9PtV0tLY7jYh0wsQi8A==
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.2.tgz#dfc675a258550809a8eaf457eb9162b943ddbaf0"
+  integrity sha512-wf0coUlidJ7rmeClgVVBh6Kw55/yalZCY/Un5RgjSnTXRAeGqagnTsTYpZaqC4dCtrY4myuYpOAZXCdbO7lHfQ==
   dependencies:
     adm-zip "~0.4.3"
     async "^2.1.2"
-    https-proxy-agent "^3.0.0"
+    https-proxy-agent "^5.0.0"
     lodash "^4.16.6"
     rimraf "^2.5.4"
 
@@ -6916,22 +7538,21 @@
   integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
 
 selenium-standalone@^6.7.0:
-  version "6.17.0"
-  resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.17.0.tgz#0f24b691836205ee9bc3d7a6f207ebcb28170cd9"
-  integrity sha512-5PSnDHwMiq+OCiAGlhwQ8BM9xuwFfvBOZ7Tfbw+ifkTnOy0PWbZmI1B9gPGuyGHpbQ/3J3CzIK7BYwrQ7EjtWQ==
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.24.0.tgz#cca7c1c36bfa3429078a8e6a1a4fd373f641a7c8"
+  integrity sha512-Dun2XgNAgCfJNrrSzuv7Z7Wj7QTvBKpqx0VXFz7bW9T9FUe5ytzgzoCEEshwDVMh0Dv6sCgdZg7VDhM/q2yPPQ==
   dependencies:
-    async "^2.6.2"
-    commander "^2.19.0"
-    cross-spawn "^6.0.5"
-    debug "^4.1.1"
-    lodash "^4.17.11"
-    minimist "^1.2.0"
-    mkdirp "^0.5.1"
+    commander "^2.20.3"
+    cross-spawn "^7.0.3"
+    debug "^4.3.1"
+    got "^11.8.2"
+    lodash.mapvalues "^4.6.0"
+    lodash.merge "^4.6.2"
+    minimist "^1.2.5"
+    mkdirp "^1.0.4"
     progress "2.0.3"
-    request "2.88.0"
-    tar-stream "2.0.0"
-    urijs "^1.19.1"
-    which "^1.3.1"
+    tar-stream "2.2.0"
+    which "^2.0.2"
     yauzl "^2.10.0"
 
 semver-diff@^2.0.0:
@@ -6941,7 +7562,7 @@
   dependencies:
     semver "^5.0.3"
 
-"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -6951,11 +7572,18 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
   integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
 
-semver@^6.3.0:
+semver@^6.0.0, semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
+semver@^7.1.3, semver@^7.2.1:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -6994,10 +7622,12 @@
     range-parser "~1.2.0"
     statuses "~1.4.0"
 
-serialize-javascript@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
-  integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
+serialize-javascript@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
+  integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
+  dependencies:
+    randombytes "^2.1.0"
 
 serve-static@1.14.1:
   version "1.14.1"
@@ -7019,6 +7649,13 @@
   resolved "https://registry.yarnpkg.com/serviceworker-cache-polyfill/-/serviceworker-cache-polyfill-4.0.0.tgz#de19ee73bef21ab3c0740a37b33db62464babdeb"
   integrity sha1-3hnuc77yGrPAdAo3sz22JGS6ves=
 
+set-getter@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.1.tgz#a3110e1b461d31a9cfc8c5c9ee2e9737ad447102"
+  integrity sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==
+  dependencies:
+    to-object-path "^0.3.0"
+
 set-value@^2.0.0, set-value@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@@ -7044,6 +7681,13 @@
   resolved "https://registry.yarnpkg.com/shady-css-parser/-/shady-css-parser-0.1.0.tgz#534dc79c8ca5884c5ed92a4e5a13d6d863bca428"
   integrity sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA==
 
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -7051,24 +7695,36 @@
   dependencies:
     shebang-regex "^1.0.0"
 
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
 shebang-regex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
   integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
 
-shelljs@^0.8.0:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097"
-  integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+shelljs@^0.8.0, shelljs@^0.8.4:
+  version "0.8.4"
+  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
+  integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
   dependencies:
     glob "^7.0.0"
     interpret "^1.0.0"
     rechoir "^0.6.2"
 
 signal-exit@^3.0.0, signal-exit@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
-  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
 simple-swizzle@^0.2.2:
   version "0.2.2"
@@ -7101,6 +7757,11 @@
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
   integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
 
+slash@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+  integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
 slide@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
@@ -7141,54 +7802,51 @@
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
   integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
 
-socket.io-client@2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
-  integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
+socket.io-client@2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35"
+  integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==
   dependencies:
     backo2 "1.0.2"
-    base64-arraybuffer "0.1.5"
     component-bind "1.0.0"
-    component-emitter "1.2.1"
-    debug "~4.1.0"
-    engine.io-client "~3.4.0"
+    component-emitter "~1.3.0"
+    debug "~3.1.0"
+    engine.io-client "~3.5.0"
     has-binary2 "~1.0.2"
-    has-cors "1.1.0"
     indexof "0.0.1"
-    object-component "0.0.3"
-    parseqs "0.0.5"
-    parseuri "0.0.5"
+    parseqs "0.0.6"
+    parseuri "0.0.6"
     socket.io-parser "~3.3.0"
     to-array "0.1.4"
 
 socket.io-parser@~3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f"
-  integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.2.tgz#ef872009d0adcf704f2fbe830191a14752ad50b6"
+  integrity sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==
   dependencies:
-    component-emitter "1.2.1"
+    component-emitter "~1.3.0"
     debug "~3.1.0"
     isarray "2.0.1"
 
 socket.io-parser@~3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.0.tgz#370bb4a151df2f77ce3345ff55a7072cc6e9565a"
-  integrity sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a"
+  integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==
   dependencies:
     component-emitter "1.2.1"
     debug "~4.1.0"
     isarray "2.0.1"
 
 socket.io@^2.0.3:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
-  integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2"
+  integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==
   dependencies:
     debug "~4.1.0"
-    engine.io "~3.4.0"
+    engine.io "~3.5.0"
     has-binary2 "~1.0.2"
     socket.io-adapter "~1.1.0"
-    socket.io-client "2.3.0"
+    socket.io-client "2.4.0"
     socket.io-parser "~3.4.0"
 
 sort-keys-length@^1.0.0:
@@ -7225,17 +7883,17 @@
     source-map "^0.6.0"
 
 source-map-support@~0.5.12:
-  version "0.5.16"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
-  integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
   dependencies:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
 source-map-url@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
-  integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
+  integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
 
 source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
   version "0.5.7"
@@ -7256,30 +7914,30 @@
     os-shim "^0.1.2"
 
 spdx-correct@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
-  integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+  integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
   dependencies:
     spdx-expression-parse "^3.0.0"
     spdx-license-ids "^3.0.0"
 
 spdx-exceptions@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
-  integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
 
 spdx-expression-parse@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
-  integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+  integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
   dependencies:
     spdx-exceptions "^2.1.0"
     spdx-license-ids "^3.0.0"
 
 spdx-license-ids@^3.0.0:
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
-  integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
+  version "3.0.10"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
+  integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
 
 spdy-transport@^2.0.18:
   version "2.1.1"
@@ -7395,7 +8053,7 @@
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
-string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+string-width@^2.0.0, string-width@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@@ -7403,6 +8061,15 @@
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
+string-width@^4.1.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
+  integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.0"
+
 string_decoder@^1.1.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -7436,18 +8103,25 @@
   dependencies:
     ansi-regex "^3.0.0"
 
-strip-ansi@^5.1.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
-  integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
+strip-ansi@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+  integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
   dependencies:
-    ansi-regex "^4.1.0"
+    ansi-regex "^5.0.0"
 
 strip-ansi@~0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
   integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=
 
+strip-bom-buf@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz#1cb45aaf57530f4caf86c7f75179d2c9a51dd572"
+  integrity sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=
+  dependencies:
+    is-utf8 "^0.2.1"
+
 strip-bom-stream@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee"
@@ -7481,6 +8155,11 @@
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
   integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
 
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
 strip-indent@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
@@ -7518,9 +8197,9 @@
     has-flag "^3.0.0"
 
 supports-color@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
-  integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
   dependencies:
     has-flag "^4.0.0"
 
@@ -7581,12 +8260,12 @@
     pump "^1.0.0"
     tar-stream "^1.1.2"
 
-tar-stream@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.0.0.tgz#8829bbf83067bc0288a9089db49c56be395b6aea"
-  integrity sha512-n2vtsWshZOVr/SY4KtslPoUlyNh06I2SGgAOCZmquCEjlbV/LjY2CY80rDtdQRHFOYXNlgBDo6Fr3ww2CWPOtA==
+tar-stream@2.2.0, tar-stream@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
   dependencies:
-    bl "^2.2.0"
+    bl "^4.0.3"
     end-of-stream "^1.4.1"
     fs-constants "^1.0.0"
     inherits "^2.0.3"
@@ -7605,17 +8284,6 @@
     to-buffer "^1.1.1"
     xtend "^4.0.0"
 
-tar-stream@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3"
-  integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==
-  dependencies:
-    bl "^3.0.0"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
 temp@^0.8.1, temp@^0.8.3:
   version "0.8.4"
   resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2"
@@ -7641,9 +8309,9 @@
     through2 "^2.0.1"
 
 terser@^4.6.2:
-  version "4.6.3"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.3.tgz#e33aa42461ced5238d352d2df2a67f21921f8d87"
-  integrity sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
+  integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
   dependencies:
     commander "^2.20.0"
     source-map "~0.6.1"
@@ -7685,9 +8353,9 @@
     thenify ">= 3.1.0 < 4"
 
 "thenify@>= 3.1.0 < 4":
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
-  integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
+  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
   dependencies:
     any-promise "^1.0.0"
 
@@ -7723,14 +8391,15 @@
     readable-stream "~2.3.6"
     xtend "~4.0.1"
 
-through2@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
-  integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
+through2@^3.0.0, through2@^3.0.1, through2@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4"
+  integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==
   dependencies:
+    inherits "^2.0.4"
     readable-stream "2 || 3"
 
-through@^2.3.6:
+"through@>=2.2.7 <3", through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@@ -7824,6 +8493,14 @@
     psl "^1.1.24"
     punycode "^1.4.1"
 
+tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
 tr46@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@@ -7846,16 +8523,11 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
-tslib@^1.8.1:
+tslib@^1.8.1, tslib@^1.9.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
-tslib@^1.9.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
-  integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
-
 tsutils@2.27.2:
   version "2.27.2"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7"
@@ -7880,6 +8552,16 @@
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
+type-fest@^0.21.3:
+  version "0.21.3"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+  integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+
 type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -7909,9 +8591,9 @@
   integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
 
 ua-parser-js@^0.7.15:
-  version "0.7.21"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
-  integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
+  version "0.7.28"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
+  integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
 
 uglify-js@3.4.x:
   version "3.4.10"
@@ -7922,9 +8604,9 @@
     source-map "~0.6.1"
 
 underscore@^1.8.3:
-  version "1.9.2"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f"
-  integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
+  integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
 
 underscore@~1.6.0:
   version "1.6.0"
@@ -7944,15 +8626,15 @@
     unicode-canonical-property-names-ecmascript "^1.0.4"
     unicode-property-aliases-ecmascript "^1.0.4"
 
-unicode-match-property-value-ecmascript@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
-  integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
+unicode-match-property-value-ecmascript@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
+  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
 
 unicode-property-aliases-ecmascript@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
-  integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
+  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
 
 union-value@^1.0.0:
   version "1.0.1"
@@ -7980,12 +8662,17 @@
     crypto-random-string "^1.0.0"
 
 universal-user-agent@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.0.tgz#27da2ec87e32769619f68a14996465ea1cb9df16"
-  integrity sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557"
+  integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==
   dependencies:
     os-name "^3.1.0"
 
+universal-user-agent@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
+  integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
+
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -8057,16 +8744,16 @@
   integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
 
 uri-js@^4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
-  integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
   dependencies:
     punycode "^2.1.0"
 
-urijs@^1.16.1, urijs@^1.19.1:
-  version "1.19.2"
-  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a"
-  integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==
+urijs@^1.16.1:
+  version "1.19.7"
+  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.7.tgz#4f594e59113928fea63c00ce688fb395b1168ab9"
+  integrity sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==
 
 urix@^0.1.0:
   version "0.1.0"
@@ -8151,17 +8838,16 @@
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
-vinyl-file@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a"
-  integrity sha1-p+v1/779obfRjRQPyweyI++2dRo=
+vinyl-file@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-3.0.0.tgz#b104d9e4409ffa325faadd520642d0a3b488b365"
+  integrity sha1-sQTZ5ECf+jJfqt1SBkLQo7SIs2U=
   dependencies:
     graceful-fs "^4.1.2"
     pify "^2.3.0"
-    pinkie-promise "^2.0.0"
-    strip-bom "^2.0.0"
+    strip-bom-buf "^1.0.0"
     strip-bom-stream "^2.0.0"
-    vinyl "^1.1.0"
+    vinyl "^2.0.1"
 
 vinyl-fs@^2.4.3, vinyl-fs@^2.4.4:
   version "2.4.4"
@@ -8186,7 +8872,7 @@
     vali-date "^1.0.0"
     vinyl "^1.0.0"
 
-vinyl@^1.0.0, vinyl@^1.1.0, vinyl@^1.1.1, vinyl@^1.2.0:
+vinyl@^1.0.0, vinyl@^1.1.1, vinyl@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
   integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=
@@ -8195,10 +8881,10 @@
     clone-stats "^0.0.1"
     replace-ext "0.0.1"
 
-vinyl@^2.0.1:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
-  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
+vinyl@^2.0.1, vinyl@^2.2.0, vinyl@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974"
+  integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==
   dependencies:
     clone "^2.1.1"
     clone-buffer "^1.0.0"
@@ -8254,9 +8940,9 @@
     uuid "^3.2.1"
 
 wd@^1.2.0:
-  version "1.12.1"
-  resolved "https://registry.yarnpkg.com/wd/-/wd-1.12.1.tgz#067eb3674db00eeb9e506701f9314657c44d5a89"
-  integrity sha512-O99X8OnOgkqfmsPyLIRzG9LmZ+rjmdGFBCyhGpnsSL4MB4xzHoeWmSVcumDiQ5QqPZcwGkszTgeJvjk2VjtiNw==
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/wd/-/wd-1.14.0.tgz#1fe6450b5baef37caa135e7755292c6998ca8a90"
+  integrity sha512-X7ZfGHHYlQ5zYpRlgP16LUsvYti+Al/6fz3T/ClVyivVCpCZQpESTDdz6zbK910O5OIvujO23Ym2DBBo3XsQlA==
   dependencies:
     archiver "^3.0.0"
     async "^2.0.0"
@@ -8315,14 +9001,14 @@
     tr46 "^1.0.1"
     webidl-conversions "^4.0.2"
 
-which@^1.0.8, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.1:
+which@^1.0.8, which@^1.2.12, which@^1.2.14, which@^1.2.9:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
   dependencies:
     isexe "^2.0.0"
 
-which@^2.0.2:
+which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -8344,34 +9030,34 @@
     string-width "^2.1.1"
 
 windows-release@^3.1.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"
-  integrity sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999"
+  integrity sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==
   dependencies:
     execa "^1.0.0"
 
-winston-transport@^4.2.0, winston-transport@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.3.0.tgz#df68c0c202482c448d9b47313c07304c2d7c2c66"
-  integrity sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==
+winston-transport@^4.2.0, winston-transport@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
+  integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
   dependencies:
-    readable-stream "^2.3.6"
+    readable-stream "^2.3.7"
     triple-beam "^1.2.0"
 
 winston@^3.0.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-3.2.1.tgz#63061377976c73584028be2490a1846055f77f07"
-  integrity sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
+  integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
   dependencies:
-    async "^2.6.1"
-    diagnostics "^1.1.1"
-    is-stream "^1.1.0"
-    logform "^2.1.1"
-    one-time "0.0.4"
-    readable-stream "^3.1.1"
+    "@dabh/diagnostics" "^2.0.2"
+    async "^3.1.0"
+    is-stream "^2.0.0"
+    logform "^2.2.0"
+    one-time "^1.0.0"
+    readable-stream "^3.4.0"
     stack-trace "0.0.x"
     triple-beam "^1.3.0"
-    winston-transport "^4.3.0"
+    winston-transport "^4.4.0"
 
 with-open-file@^0.1.6:
   version "0.1.7"
@@ -8382,7 +9068,7 @@
     p-try "^2.1.0"
     pify "^4.0.1"
 
-wordwrap@~0.0.2:
+wordwrap@^0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
   integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
@@ -8428,17 +9114,10 @@
     imurmurhash "^0.1.4"
     signal-exit "^3.0.2"
 
-ws@^7.1.2:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
-  integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
-
-ws@~6.1.0:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
-  integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
-  dependencies:
-    async-limiter "~1.0.0"
+ws@~7.4.2:
+  version "7.4.6"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
+  integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
 
 xdg-basedir@^2.0.0:
   version "2.0.0"
@@ -8462,10 +9141,10 @@
   resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
   integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
 
-xmlhttprequest-ssl@~1.5.4:
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
-  integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
+xmlhttprequest-ssl@~1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
+  integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
 
 "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
   version "4.0.2"
@@ -8477,6 +9156,11 @@
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
 
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
 yauzl@^2.10.0:
   version "2.10.0"
   resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
@@ -8508,26 +9192,30 @@
     text-table "^0.2.0"
     untildify "^2.0.0"
 
-yeoman-environment@^2.0.5:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.7.0.tgz#d1b6679de883ce14a68b869c4b19d55a0d66f477"
-  integrity sha512-YNzSUWgJVSgnm0qgLON4Gb2nTm+kywBiWjK4MbvosjUP2YJJ30lNhEx7ukyzKRPUlsavd5IsuALtF6QaVrq81A==
+yeoman-environment@^2.0.5, yeoman-environment@^2.9.5:
+  version "2.10.3"
+  resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.10.3.tgz#9d8f42b77317414434cc0e51fb006a4bdd54688e"
+  integrity sha512-pLIhhU9z/G+kjOXmJ2bPFm3nejfbH+f1fjYRSOteEXDBrv1EoJE/e+kuHixSXfCYfTkxjYsvRaDX+1QykLCnpQ==
   dependencies:
     chalk "^2.4.1"
-    cross-spawn "^6.0.5"
     debug "^3.1.0"
     diff "^3.5.0"
     escape-string-regexp "^1.0.2"
+    execa "^4.0.0"
     globby "^8.0.1"
-    grouped-queue "^0.3.3"
-    inquirer "^6.0.0"
+    grouped-queue "^1.1.0"
+    inquirer "^7.1.0"
     is-scoped "^1.0.0"
     lodash "^4.17.10"
     log-symbols "^2.2.0"
     mem-fs "^1.1.0"
+    mem-fs-editor "^6.0.0"
+    npm-api "^1.0.0"
+    semver "^7.1.3"
     strip-ansi "^4.0.0"
     text-table "^0.2.0"
     untildify "^3.0.3"
+    yeoman-generator "^4.8.2"
 
 yeoman-generator@^3.1.1:
   version "3.2.0"
@@ -8560,6 +9248,40 @@
     through2 "^3.0.0"
     yeoman-environment "^2.0.5"
 
+yeoman-generator@^4.8.2:
+  version "4.13.0"
+  resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-4.13.0.tgz#a6caeed8491fceea1f84f53e31795f25888b4672"
+  integrity sha512-f2/5N5IR3M2Ozm+QocvZQudlQITv2DwI6Mcxfy7R7gTTzaKgvUpgo/pQMJ+WQKm0KN0YMWCFOZpj0xFGxevc1w==
+  dependencies:
+    async "^2.6.2"
+    chalk "^2.4.2"
+    cli-table "^0.3.1"
+    cross-spawn "^6.0.5"
+    dargs "^6.1.0"
+    dateformat "^3.0.3"
+    debug "^4.1.1"
+    diff "^4.0.1"
+    error "^7.0.2"
+    find-up "^3.0.0"
+    github-username "^3.0.0"
+    istextorbinary "^2.5.1"
+    lodash "^4.17.11"
+    make-dir "^3.0.0"
+    mem-fs-editor "^7.0.1"
+    minimist "^1.2.5"
+    pretty-bytes "^5.2.0"
+    read-chunk "^3.2.0"
+    read-pkg-up "^5.0.0"
+    rimraf "^2.6.3"
+    run-async "^2.0.0"
+    semver "^7.2.1"
+    shelljs "^0.8.4"
+    text-table "^0.2.0"
+    through2 "^3.0.1"
+  optionalDependencies:
+    grouped-queue "^1.1.0"
+    yeoman-environment "^2.9.5"
+
 zip-stream@^2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b"
diff --git a/tools/polygerrit-updater/tsconfig.json b/tools/polygerrit-updater/tsconfig.json
index 37ff1b2..80f60c1 100644
--- a/tools/polygerrit-updater/tsconfig.json
+++ b/tools/polygerrit-updater/tsconfig.json
@@ -2,8 +2,8 @@
   "compilerOptions": {
     /* Basic Options */
     // "incremental": true,                   /* Enable incremental compilation */
-    "target": "es2015",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
-    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "target": "es2019", 		      /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", 		      /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     // "lib": [],                             /* Specify library files to be included in the compilation. */
     // "allowJs": true,                       /* Allow javascript files to be compiled. */
     // "checkJs": true,                       /* Report errors in .js files. */
diff --git a/twinkie.patch b/twinkie.patch
deleted file mode 100644
index 0a61243..0000000
--- a/twinkie.patch
+++ /dev/null
@@ -1,11 +0,0 @@
---- a/node_modules/twinkie/src/app/index.js
-+++ b/node_modules/twinkie/src/app/index.js
-@@ -250,7 +250,7 @@ twinkie --tsconfig tsconfig.json --outdir output_dir [--files file_list] [--outt
-                 incremental: false,
-                 noEmit: true,
-             },
--            files: [...allProgramFilesNames, generatedFiles],
-+            files: [...allProgramFilesNames, ...generatedFiles],
-         };
-         fs.writeFileSync(cmdLineOptions.outputTsConfig, JSON.stringify(tsconfigContent, null, 2));
-     }
diff --git a/yarn.lock b/yarn.lock
index 32d86fc..17c08c3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,727 +2,127 @@
 # yarn lockfile v1
 
 
-"@babel/code-frame@7.12.11", "@babel/code-frame@^7.0.0":
+"@babel/code-frame@7.12.11":
   version "7.12.11"
-  resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
   integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
   dependencies:
     "@babel/highlight" "^7.10.4"
 
-"@babel/code-frame@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
-  integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
+"@babel/code-frame@^7.0.0":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
+  integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==
   dependencies:
-    "@babel/highlight" "^7.12.13"
+    "@babel/highlight" "^7.14.5"
 
-"@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.8":
-  version "7.13.15"
-  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4"
-  integrity sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==
+"@babel/helper-validator-identifier@^7.14.5":
+  version "7.14.9"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48"
+  integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==
 
-"@babel/core@^7.0.0":
-  version "7.13.15"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.15.tgz#a6d40917df027487b54312202a06812c4f7792d0"
-  integrity sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==
+"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5":
+  version "7.14.5"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
+  integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
   dependencies:
-    "@babel/code-frame" "^7.12.13"
-    "@babel/generator" "^7.13.9"
-    "@babel/helper-compilation-targets" "^7.13.13"
-    "@babel/helper-module-transforms" "^7.13.14"
-    "@babel/helpers" "^7.13.10"
-    "@babel/parser" "^7.13.15"
-    "@babel/template" "^7.12.13"
-    "@babel/traverse" "^7.13.15"
-    "@babel/types" "^7.13.14"
-    convert-source-map "^1.7.0"
-    debug "^4.1.0"
-    gensync "^1.0.0-beta.2"
-    json5 "^2.1.2"
-    semver "^6.3.0"
-    source-map "^0.5.0"
-
-"@babel/generator@^7.0.0-beta.42", "@babel/generator@^7.13.9":
-  version "7.13.9"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
-  integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
-  dependencies:
-    "@babel/types" "^7.13.0"
-    jsesc "^2.5.1"
-    source-map "^0.5.0"
-
-"@babel/helper-annotate-as-pure@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab"
-  integrity sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==
-  dependencies:
-    "@babel/types" "^7.12.13"
-
-"@babel/helper-builder-binary-assignment-operator-visitor@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.12.13.tgz#6bc20361c88b0a74d05137a65cac8d3cbf6f61fc"
-  integrity sha512-CZOv9tGphhDRlVjVkAgm8Nhklm9RzSmWpX2my+t7Ua/KT616pEzXsQCjinzvkRvHWJ9itO4f296efroX23XCMA==
-  dependencies:
-    "@babel/helper-explode-assignable-expression" "^7.12.13"
-    "@babel/types" "^7.12.13"
-
-"@babel/helper-compilation-targets@^7.13.13", "@babel/helper-compilation-targets@^7.13.8":
-  version "7.13.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5"
-  integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==
-  dependencies:
-    "@babel/compat-data" "^7.13.12"
-    "@babel/helper-validator-option" "^7.12.17"
-    browserslist "^4.14.5"
-    semver "^6.3.0"
-
-"@babel/helper-create-regexp-features-plugin@^7.12.13":
-  version "7.12.17"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz#a2ac87e9e319269ac655b8d4415e94d38d663cb7"
-  integrity sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.12.13"
-    regexpu-core "^4.7.1"
-
-"@babel/helper-explode-assignable-expression@^7.12.13":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz#17b5c59ff473d9f956f40ef570cf3a76ca12657f"
-  integrity sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA==
-  dependencies:
-    "@babel/types" "^7.13.0"
-
-"@babel/helper-function-name@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a"
-  integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==
-  dependencies:
-    "@babel/helper-get-function-arity" "^7.12.13"
-    "@babel/template" "^7.12.13"
-    "@babel/types" "^7.12.13"
-
-"@babel/helper-get-function-arity@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583"
-  integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==
-  dependencies:
-    "@babel/types" "^7.12.13"
-
-"@babel/helper-member-expression-to-functions@^7.13.12":
-  version "7.13.12"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72"
-  integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==
-  dependencies:
-    "@babel/types" "^7.13.12"
-
-"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12":
-  version "7.13.12"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
-  integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
-  dependencies:
-    "@babel/types" "^7.13.12"
-
-"@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.13.14":
-  version "7.13.14"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz#e600652ba48ccb1641775413cb32cfa4e8b495ef"
-  integrity sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==
-  dependencies:
-    "@babel/helper-module-imports" "^7.13.12"
-    "@babel/helper-replace-supers" "^7.13.12"
-    "@babel/helper-simple-access" "^7.13.12"
-    "@babel/helper-split-export-declaration" "^7.12.13"
-    "@babel/helper-validator-identifier" "^7.12.11"
-    "@babel/template" "^7.12.13"
-    "@babel/traverse" "^7.13.13"
-    "@babel/types" "^7.13.14"
-
-"@babel/helper-optimise-call-expression@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea"
-  integrity sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==
-  dependencies:
-    "@babel/types" "^7.12.13"
-
-"@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.8.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af"
-  integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==
-
-"@babel/helper-remap-async-to-generator@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz#376a760d9f7b4b2077a9dd05aa9c3927cadb2209"
-  integrity sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.12.13"
-    "@babel/helper-wrap-function" "^7.13.0"
-    "@babel/types" "^7.13.0"
-
-"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0", "@babel/helper-replace-supers@^7.13.12":
-  version "7.13.12"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804"
-  integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==
-  dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.13.12"
-    "@babel/helper-optimise-call-expression" "^7.12.13"
-    "@babel/traverse" "^7.13.0"
-    "@babel/types" "^7.13.12"
-
-"@babel/helper-simple-access@^7.13.12":
-  version "7.13.12"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6"
-  integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==
-  dependencies:
-    "@babel/types" "^7.13.12"
-
-"@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
-  version "7.12.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf"
-  integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==
-  dependencies:
-    "@babel/types" "^7.12.1"
-
-"@babel/helper-split-export-declaration@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05"
-  integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==
-  dependencies:
-    "@babel/types" "^7.12.13"
-
-"@babel/helper-validator-identifier@^7.12.11":
-  version "7.12.11"
-  resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz"
-  integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
-
-"@babel/helper-validator-option@^7.12.17":
-  version "7.12.17"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831"
-  integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==
-
-"@babel/helper-wrap-function@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz#bdb5c66fda8526ec235ab894ad53a1235c79fcc4"
-  integrity sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA==
-  dependencies:
-    "@babel/helper-function-name" "^7.12.13"
-    "@babel/template" "^7.12.13"
-    "@babel/traverse" "^7.13.0"
-    "@babel/types" "^7.13.0"
-
-"@babel/helpers@^7.13.10":
-  version "7.13.10"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8"
-  integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==
-  dependencies:
-    "@babel/template" "^7.12.13"
-    "@babel/traverse" "^7.13.0"
-    "@babel/types" "^7.13.0"
-
-"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13":
-  version "7.13.10"
-  resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz"
-  integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.12.11"
+    "@babel/helper-validator-identifier" "^7.14.5"
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.12.13", "@babel/parser@^7.13.15":
-  version "7.13.15"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8"
-  integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==
-
-"@babel/plugin-external-helpers@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-external-helpers/-/plugin-external-helpers-7.12.13.tgz#65ef9f4576297250dc601d2aa334769790d9966d"
-  integrity sha512-ClvAsk4RqpE6iacYUjdU9PtvIwC9yAefZENsPfGeG5FckX3jFZLDlWPuyv5gi9/9C2VgwX6H8q1ukBifC0ha+Q==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-proposal-async-generator-functions@^7.0.0":
-  version "7.13.15"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz#80e549df273a3b3050431b148c892491df1bcc5b"
-  integrity sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/helper-remap-async-to-generator" "^7.13.0"
-    "@babel/plugin-syntax-async-generators" "^7.8.4"
-
-"@babel/plugin-proposal-object-rest-spread@^7.0.0":
-  version "7.13.8"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz#5d210a4d727d6ce3b18f9de82cc99a3964eed60a"
-  integrity sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==
-  dependencies:
-    "@babel/compat-data" "^7.13.8"
-    "@babel/helper-compilation-targets" "^7.13.8"
-    "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
-    "@babel/plugin-transform-parameters" "^7.13.0"
-
-"@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.8.4":
-  version "7.8.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
-  integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
-
-"@babel/plugin-syntax-dynamic-import@^7.0.0":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
-  integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
-
-"@babel/plugin-syntax-import-meta@^7.0.0":
-  version "7.10.4"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
-  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.10.4"
-
-"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
-  integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.8.0"
-
-"@babel/plugin-transform-arrow-functions@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz#10a59bebad52d637a027afa692e8d5ceff5e3dae"
-  integrity sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-
-"@babel/plugin-transform-async-to-generator@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz#8e112bf6771b82bf1e974e5e26806c5c99aa516f"
-  integrity sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==
-  dependencies:
-    "@babel/helper-module-imports" "^7.12.13"
-    "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/helper-remap-async-to-generator" "^7.13.0"
-
-"@babel/plugin-transform-block-scoped-functions@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz#a9bf1836f2a39b4eb6cf09967739de29ea4bf4c4"
-  integrity sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-block-scoping@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz#f36e55076d06f41dfd78557ea039c1b581642e61"
-  integrity sha512-Pxwe0iqWJX4fOOM2kEZeUuAxHMWb9nK+9oh5d11bsLoB0xMg+mkDpt0eYuDZB7ETrY9bbcVlKUGTOGWy7BHsMQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-classes@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz#0265155075c42918bf4d3a4053134176ad9b533b"
-  integrity sha512-9BtHCPUARyVH1oXGcSJD3YpsqRLROJx5ZNP6tN5vnk17N0SVf9WCtf8Nuh1CFmgByKKAIMstitKduoCmsaDK5g==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.12.13"
-    "@babel/helper-function-name" "^7.12.13"
-    "@babel/helper-optimise-call-expression" "^7.12.13"
-    "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/helper-replace-supers" "^7.13.0"
-    "@babel/helper-split-export-declaration" "^7.12.13"
-    globals "^11.1.0"
-
-"@babel/plugin-transform-computed-properties@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz#845c6e8b9bb55376b1fa0b92ef0bdc8ea06644ed"
-  integrity sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-
-"@babel/plugin-transform-destructuring@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.0.tgz#c5dce270014d4e1ebb1d806116694c12b7028963"
-  integrity sha512-zym5em7tePoNT9s964c0/KU3JPPnuq7VhIxPRefJ4/s82cD+q1mgKfuGRDMCPL0HTyKz4dISuQlCusfgCJ86HA==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-
-"@babel/plugin-transform-duplicate-keys@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz#6f06b87a8b803fd928e54b81c258f0a0033904de"
-  integrity sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-exponentiation-operator@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz#4d52390b9a273e651e4aba6aee49ef40e80cd0a1"
-  integrity sha512-fbUelkM1apvqez/yYx1/oICVnGo2KM5s63mhGylrmXUxK/IAXSIf87QIxVfZldWf4QsOafY6vV3bX8aMHSvNrA==
-  dependencies:
-    "@babel/helper-builder-binary-assignment-operator-visitor" "^7.12.13"
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-for-of@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz#c799f881a8091ac26b54867a845c3e97d2696062"
-  integrity sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-
-"@babel/plugin-transform-function-name@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz#bb024452f9aaed861d374c8e7a24252ce3a50051"
-  integrity sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ==
-  dependencies:
-    "@babel/helper-function-name" "^7.12.13"
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-instanceof@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-instanceof/-/plugin-transform-instanceof-7.12.13.tgz#5df8ead82ed421b728662c9e0ed943536c872ced"
-  integrity sha512-lYZ6F2xmM797Nk8PzDn2AreAEjKb96S3JkZkaMUlRTIThaYjvo1+aLa7oegnVc42lJY7Hr4yT1M/i6kwRcPlsQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-literals@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz#2ca45bafe4a820197cf315794a4d26560fe4bdb9"
-  integrity sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-modules-amd@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.13.0.tgz#19f511d60e3d8753cc5a6d4e775d3a5184866cc3"
-  integrity sha512-EKy/E2NHhY/6Vw5d1k3rgoobftcNUmp9fGjb9XZwQLtTctsRBOTRO7RHHxfIky1ogMN5BxN7p9uMA3SzPfotMQ==
-  dependencies:
-    "@babel/helper-module-transforms" "^7.13.0"
-    "@babel/helper-plugin-utils" "^7.13.0"
-    babel-plugin-dynamic-import-node "^2.3.3"
-
-"@babel/plugin-transform-object-super@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz#b4416a2d63b8f7be314f3d349bd55a9c1b5171f7"
-  integrity sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-    "@babel/helper-replace-supers" "^7.12.13"
-
-"@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz#8fa7603e3097f9c0b7ca1a4821bc2fb52e9e5007"
-  integrity sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-
-"@babel/plugin-transform-regenerator@^7.0.0":
-  version "7.13.15"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz#e5eb28945bf8b6563e7f818945f966a8d2997f39"
-  integrity sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==
-  dependencies:
-    regenerator-transform "^0.14.2"
-
-"@babel/plugin-transform-shorthand-properties@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz#db755732b70c539d504c6390d9ce90fe64aff7ad"
-  integrity sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-spread@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz#84887710e273c1815ace7ae459f6f42a5d31d5fd"
-  integrity sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
-
-"@babel/plugin-transform-sticky-regex@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz#760ffd936face73f860ae646fb86ee82f3d06d1f"
-  integrity sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-template-literals@^7.0.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz#a36049127977ad94438dee7443598d1cefdf409d"
-  integrity sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.13.0"
-
-"@babel/plugin-transform-typeof-symbol@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz#785dd67a1f2ea579d9c2be722de8c84cb85f5a7f"
-  integrity sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ==
-  dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/plugin-transform-unicode-regex@^7.0.0":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz#b52521685804e155b1202e83fc188d34bb70f5ac"
-  integrity sha512-mDRzSNY7/zopwisPZ5kM9XKCfhchqIYwAKRERtEnhYscZB79VRekuRSoYbN0+KVe3y8+q1h6A4svXtP7N+UoCA==
-  dependencies:
-    "@babel/helper-create-regexp-features-plugin" "^7.12.13"
-    "@babel/helper-plugin-utils" "^7.12.13"
-
-"@babel/runtime@^7.8.4":
-  version "7.13.10"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
-  integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
-"@babel/template@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
-  integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==
-  dependencies:
-    "@babel/code-frame" "^7.12.13"
-    "@babel/parser" "^7.12.13"
-    "@babel/types" "^7.12.13"
-
-"@babel/traverse@^7.0.0", "@babel/traverse@^7.0.0-beta.42", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.13.15":
-  version "7.13.15"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.15.tgz#c38bf7679334ddd4028e8e1f7b3aa5019f0dada7"
-  integrity sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==
-  dependencies:
-    "@babel/code-frame" "^7.12.13"
-    "@babel/generator" "^7.13.9"
-    "@babel/helper-function-name" "^7.12.13"
-    "@babel/helper-split-export-declaration" "^7.12.13"
-    "@babel/parser" "^7.13.15"
-    "@babel/types" "^7.13.14"
-    debug "^4.1.0"
-    globals "^11.1.0"
-
-"@babel/types@^7.0.0-beta.42", "@babel/types@^7.12.1", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.14":
-  version "7.13.14"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d"
-  integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.12.11"
-    lodash "^4.17.19"
-    to-fast-properties "^2.0.0"
-
 "@bazel/rollup@^3.5.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a"
-  integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow==
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.8.0.tgz#850f56176d73e3b7d99a43c7e33df21ecc6ac161"
+  integrity sha512-u63ubqYtfQhOu8Km3uYdhKa6qiLSlOKYsWwMP1xGkkXzu1hOiUznN1N7q8gCF1BV2DMy1D5IYkv+Xg4a+LEiBA==
 
 "@bazel/terser@^3.5.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b"
-  integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA==
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.8.0.tgz#96d337b62b2ba18e2fe00984cca950cda899d165"
+  integrity sha512-c7cGIltFUI7prRocMDZ3qZVERnew81SFheuI5B9RQ3qeqTlJmlV8B8GI9FPG+7Ut69bmwn8es6UyPaH0iBnsQw==
 
 "@bazel/typescript@^3.5.0":
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.5.0.tgz#605493f4f0a5297df8a7fcccb86a1a80ea2090bb"
-  integrity sha512-BtGFp4nYFkQTmnONCzomk7dkmOwaINBL3piq+lykBlcc6UxLe9iCAnZpOyPypB1ReN3k3SRNAa53x6oGScQxMg==
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.8.0.tgz#725d51a1c25e314a1d8cddb8b880ac05ba97acd4"
+  integrity sha512-4C1pLe4V7aidWqcPsWNqXFS7uHAB1nH5SUKG5uWoVv4JT9XhkNSvzzQIycMwXs2tZeCylX4KYNeNvfKrmkyFlw==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
     source-map-support "0.5.9"
     tsutils "2.27.2"
 
-"@dabh/diagnostics@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
-  integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==
-  dependencies:
-    colorspace "1.1.x"
-    enabled "2.0.x"
-    kuler "^2.0.0"
-
-"@eslint/eslintrc@^0.4.0":
-  version "0.4.0"
-  resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz"
-  integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog==
+"@eslint/eslintrc@^0.4.3":
+  version "0.4.3"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
+  integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
   dependencies:
     ajv "^6.12.4"
     debug "^4.1.1"
     espree "^7.3.0"
-    globals "^12.1.0"
+    globals "^13.9.0"
     ignore "^4.0.6"
     import-fresh "^3.2.1"
     js-yaml "^3.13.1"
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
-"@mrmlnc/readdir-enhanced@^2.2.1":
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
-  integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+"@humanwhocodes/config-array@^0.5.0":
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
+  integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
   dependencies:
-    call-me-maybe "^1.0.1"
-    glob-to-regexp "^0.3.0"
+    "@humanwhocodes/object-schema" "^1.2.0"
+    debug "^4.1.1"
+    minimatch "^3.0.4"
 
-"@nodelib/fs.scandir@2.1.4":
-  version "2.1.4"
-  resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz"
-  integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==
+"@humanwhocodes/object-schema@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
+  integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
+
+"@nodelib/fs.scandir@2.1.5":
+  version "2.1.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
   dependencies:
-    "@nodelib/fs.stat" "2.0.4"
+    "@nodelib/fs.stat" "2.0.5"
     run-parallel "^1.1.9"
 
-"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2":
-  version "2.0.4"
-  resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz"
-  integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==
-
-"@nodelib/fs.stat@^1.1.2":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
-  integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
 
 "@nodelib/fs.walk@^1.2.3":
-  version "1.2.6"
-  resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz"
-  integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
   dependencies:
-    "@nodelib/fs.scandir" "2.1.4"
+    "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@octokit/auth-token@^2.4.0":
-  version "2.4.5"
-  resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
-  integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==
-  dependencies:
-    "@octokit/types" "^6.0.3"
-
-"@octokit/endpoint@^6.0.1":
-  version "6.0.11"
-  resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1"
-  integrity sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==
-  dependencies:
-    "@octokit/types" "^6.0.3"
-    is-plain-object "^5.0.0"
-    universal-user-agent "^6.0.0"
-
-"@octokit/openapi-types@^6.0.0":
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-6.0.0.tgz#7da8d7d5a72d3282c1a3ff9f951c8133a707480d"
-  integrity sha512-CnDdK7ivHkBtJYzWzZm7gEkanA7gKH6a09Eguz7flHw//GacPJLmkHA3f3N++MJmlxD1Fl+mB7B32EEpSCwztQ==
-
-"@octokit/plugin-paginate-rest@^1.1.1":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc"
-  integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q==
-  dependencies:
-    "@octokit/types" "^2.0.1"
-
-"@octokit/plugin-request-log@^1.0.0":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz#70a62be213e1edc04bb8897ee48c311482f9700d"
-  integrity sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ==
-
-"@octokit/plugin-rest-endpoint-methods@2.4.0":
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e"
-  integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ==
-  dependencies:
-    "@octokit/types" "^2.0.1"
-    deprecation "^2.3.1"
-
-"@octokit/request-error@^1.0.2":
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801"
-  integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==
-  dependencies:
-    "@octokit/types" "^2.0.0"
-    deprecation "^2.0.0"
-    once "^1.4.0"
-
-"@octokit/request-error@^2.0.0":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.5.tgz#72cc91edc870281ad583a42619256b380c600143"
-  integrity sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==
-  dependencies:
-    "@octokit/types" "^6.0.3"
-    deprecation "^2.0.0"
-    once "^1.4.0"
-
-"@octokit/request@^5.2.0":
-  version "5.4.15"
-  resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.15.tgz#829da413dc7dd3aa5e2cdbb1c7d0ebe1f146a128"
-  integrity sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==
-  dependencies:
-    "@octokit/endpoint" "^6.0.1"
-    "@octokit/request-error" "^2.0.0"
-    "@octokit/types" "^6.7.1"
-    is-plain-object "^5.0.0"
-    node-fetch "^2.6.1"
-    universal-user-agent "^6.0.0"
-
-"@octokit/rest@^16.2.0":
-  version "16.43.2"
-  resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.2.tgz#c53426f1e1d1044dee967023e3279c50993dd91b"
-  integrity sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ==
-  dependencies:
-    "@octokit/auth-token" "^2.4.0"
-    "@octokit/plugin-paginate-rest" "^1.1.1"
-    "@octokit/plugin-request-log" "^1.0.0"
-    "@octokit/plugin-rest-endpoint-methods" "2.4.0"
-    "@octokit/request" "^5.2.0"
-    "@octokit/request-error" "^1.0.2"
-    atob-lite "^2.0.0"
-    before-after-hook "^2.0.0"
-    btoa-lite "^1.0.0"
-    deprecation "^2.0.0"
-    lodash.get "^4.4.2"
-    lodash.set "^4.3.2"
-    lodash.uniq "^4.5.0"
-    octokit-pagination-methods "^1.1.0"
-    once "^1.4.0"
-    universal-user-agent "^4.0.0"
-
-"@octokit/types@^2.0.0", "@octokit/types@^2.0.1":
-  version "2.16.2"
-  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2"
-  integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==
-  dependencies:
-    "@types/node" ">= 8"
-
-"@octokit/types@^6.0.3", "@octokit/types@^6.7.1":
-  version "6.13.0"
-  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.13.0.tgz#779e5b7566c8dde68f2f6273861dd2f0409480d0"
-  integrity sha512-W2J9qlVIU11jMwKHUp5/rbVUeErqelCsO5vW5PKNb7wAXQVUz87Rc+imjlEvpvbH8yUb+KHmv8NEjVZdsdpyxA==
-  dependencies:
-    "@octokit/openapi-types" "^6.0.0"
-
-"@polymer/esm-amd-loader@^1.0.0":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@polymer/esm-amd-loader/-/esm-amd-loader-1.0.4.tgz#4e77f2f59b29b01e0ad02aa83d33716cddc5f9f9"
-  integrity sha512-h+hqYkL+tQV/y2ESD5gFXMl5z4cC+XY1jTlBeGSBaTcj3VbB5OBEScbvRXm63NcEbBneQQYbHfBAXAkF9i9wIA==
-
-"@polymer/sinonjs@^1.14.1":
-  version "1.17.1"
-  resolved "https://registry.yarnpkg.com/@polymer/sinonjs/-/sinonjs-1.17.1.tgz#e47d3785b7d0e8c29feb97f7e924b0fc597e2e9b"
-  integrity sha512-/U8F/cOTrbF2iVVYgINYmvKbtbexs+89Q3v8AaHADRYabTg7aOZGOb0RyWpOI+sUJt04kj63U4FwMhzW5r4wZA==
-
-"@polymer/test-fixture@^0.0.3":
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-0.0.3.tgz#4443752697d4d9293bbc412ea0b5e4d341f149d9"
-  integrity sha1-REN1JpfU2Sk7vEEuoLXk00HxSdk=
-
 "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
   version "1.1.2"
-  resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
   integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
 
 "@protobufjs/base64@^1.1.2":
   version "1.1.2"
-  resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
   integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
 
 "@protobufjs/codegen@^2.0.4":
   version "2.0.4"
-  resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
   integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
 
 "@protobufjs/eventemitter@^1.1.0":
   version "1.1.0"
-  resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
   integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
 
 "@protobufjs/fetch@^1.1.0":
   version "1.1.0"
-  resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
   integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
   dependencies:
     "@protobufjs/aspromise" "^1.1.1"
@@ -730,314 +130,41 @@
 
 "@protobufjs/float@^1.0.2":
   version "1.0.2"
-  resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
   integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
 
 "@protobufjs/inquire@^1.1.0":
   version "1.1.0"
-  resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
   integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
 
 "@protobufjs/path@^1.1.2":
   version "1.1.2"
-  resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
   integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
 
 "@protobufjs/pool@^1.1.0":
   version "1.1.0"
-  resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
   integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
 
 "@protobufjs/utf8@^1.1.0":
   version "1.1.0"
-  resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
   integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
 
 "@sindresorhus/is@^0.14.0":
   version "0.14.0"
-  resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz"
+  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
   integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
 
-"@sindresorhus/is@^4.0.0":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4"
-  integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==
-
 "@szmarczak/http-timer@^1.1.2":
   version "1.1.2"
-  resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
   integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
   dependencies:
     defer-to-connect "^1.0.1"
 
-"@szmarczak/http-timer@^4.0.5":
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152"
-  integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==
-  dependencies:
-    defer-to-connect "^2.0.0"
-
-"@types/babel-generator@^6.25.1":
-  version "6.25.3"
-  resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.3.tgz#8f06caa12d0595a0538560abe771966d77d29286"
-  integrity sha512-pGgnuxVddKcYIc+VJkRDop7gxLhqclNKBdlsm/5Vp8d+37pQkkDK7fef8d9YYImRzw9xcojEPc18pUYnbxmjqA==
-  dependencies:
-    "@types/babel-types" "*"
-
-"@types/babel-traverse@^6.25.2", "@types/babel-traverse@^6.25.3":
-  version "6.25.5"
-  resolved "https://registry.yarnpkg.com/@types/babel-traverse/-/babel-traverse-6.25.5.tgz#6d293cf7523e48b524faa7b86dc3c488191484e5"
-  integrity sha512-WrMbwmu+MWf8FiUMbmVOGkc7bHPzndUafn1CivMaBHthBBoo0VNIcYk1KV71UovYguhsNOwf3UF5oRmkkGOU3w==
-  dependencies:
-    "@types/babel-types" "*"
-
-"@types/babel-types@*":
-  version "7.0.9"
-  resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.9.tgz#01d7b86949f455402a94c788883fe4ba574cad41"
-  integrity sha512-qZLoYeXSTgQuK1h7QQS16hqLGdmqtRmN8w/rl3Au/l5x/zkHx+a4VHrHyBsi1I1vtK2oBHxSzKIu0R5p6spdOA==
-
-"@types/babel-types@^6.25.1":
-  version "6.25.2"
-  resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-6.25.2.tgz#5c57f45973e4f13742dbc5273dd84cffe7373a9e"
-  integrity sha512-+3bMuktcY4a70a0KZc8aPJlEOArPuAKQYHU5ErjkOqGJdx8xuEEVK6nWogqigBOJ8nKPxRpyCUDTCPmZ3bUxGA==
-
-"@types/babylon@^6.16.2":
-  version "6.16.5"
-  resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4"
-  integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==
-  dependencies:
-    "@types/babel-types" "*"
-
-"@types/bluebird@*":
-  version "3.5.33"
-  resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.33.tgz#d79c020f283bd50bd76101d7d300313c107325fc"
-  integrity sha512-ndEo1xvnYeHxm7I/5sF6tBvnsA4Tdi3zj1keRKRs12SP+2ye2A27NDJ1B6PqkfMbGAcT+mqQVqbZRIrhfOp5PQ==
-
-"@types/body-parser@*":
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
-  integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
-  dependencies:
-    "@types/connect" "*"
-    "@types/node" "*"
-
-"@types/cacheable-request@^6.0.1":
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
-  integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==
-  dependencies:
-    "@types/http-cache-semantics" "*"
-    "@types/keyv" "*"
-    "@types/node" "*"
-    "@types/responselike" "*"
-
-"@types/chai-subset@^1.3.0":
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
-  integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==
-  dependencies:
-    "@types/chai" "*"
-
-"@types/chai@*":
-  version "4.2.16"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.16.tgz#f09cc36e18d28274f942e7201147cce34d97e8c8"
-  integrity sha512-vI5iOAsez9+roLS3M3+Xx7w+WRuDtSmF8bQkrbcIJ2sC1PcDgVoA0WGpa+bIrJ+y8zqY2oi//fUctkxtIcXJCw==
-
-"@types/chalk@^0.4.30":
-  version "0.4.31"
-  resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9"
-  integrity sha1-ox10JBprHtu5c8822XooloNKUfk=
-
-"@types/chalk@^2.2.0":
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba"
-  integrity sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==
-  dependencies:
-    chalk "*"
-
-"@types/clean-css@*":
-  version "4.2.4"
-  resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.4.tgz#4fe4705c384e6ec9ee8454bc3d49089f38dc038a"
-  integrity sha512-x8xEbfTtcv5uyQDrBXKg9Beo5QhTPqO4vM0uq4iU27/nhyRRWNEMKHjxvAb0WDvp2Mnt4Sw0jKmIi5yQF/k2Ag==
-  dependencies:
-    "@types/node" "*"
-    source-map "^0.6.0"
-
-"@types/clone@^0.1.30":
-  version "0.1.30"
-  resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614"
-  integrity sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=
-
-"@types/compression@^0.0.33":
-  version "0.0.33"
-  resolved "https://registry.yarnpkg.com/@types/compression/-/compression-0.0.33.tgz#95dc733a2339aa846381d7f1377792d2553dc27d"
-  integrity sha1-ldxzOiM5qoRjgdfxN3eS0lU9wn0=
-  dependencies:
-    "@types/express" "*"
-
-"@types/connect@*":
-  version "3.4.34"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
-  integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/content-type@^1.1.0":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.3.tgz#3688bd77fc12f935548eef102a4e34c512b03a07"
-  integrity sha512-pv8VcFrZ3fN93L4rTNIbbUzdkzjEyVMp5mPVjsFfOYTDOZMZiZ8P1dhu+kEv3faYyKzZgLlSvnyQNFg+p/v5ug==
-
-"@types/cssbeautify@^0.3.1":
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/@types/cssbeautify/-/cssbeautify-0.3.1.tgz#8e0bee8f7decb952250da0caebe05e30591c17ef"
-  integrity sha1-jgvuj33suVIlDaDK6+BeMFkcF+8=
-
-"@types/del@^3.0.0":
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/@types/del/-/del-3.0.1.tgz#4712da8c119873cbbf533ad8dbf1baac5940ac5d"
-  integrity sha512-y6qRq6raBuu965clKgx6FHuiPu3oHdtmzMPXi8Uahsjdq1L6DL5fS/aY5/s71YwM7k6K1QIWvem5vNwlnNGIkQ==
-  dependencies:
-    "@types/glob" "*"
-
-"@types/doctrine@^0.0.1":
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.1.tgz#b999f2d9f7b43cabe0a1a2f39bc203bc7dcada9d"
-  integrity sha1-uZny2fe0PKvgoaLzm8IDvH3K2p0=
-
-"@types/escape-html@0.0.20":
-  version "0.0.20"
-  resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a"
-  integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw==
-
-"@types/estree@*":
-  version "0.0.47"
-  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.47.tgz#d7a51db20f0650efec24cd04994f523d93172ed4"
-  integrity sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==
-
-"@types/events@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
-  integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
-
-"@types/expect@^1.20.4":
-  version "1.20.4"
-  resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5"
-  integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==
-
-"@types/express-serve-static-core@^4.17.18":
-  version "4.17.19"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d"
-  integrity sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==
-  dependencies:
-    "@types/node" "*"
-    "@types/qs" "*"
-    "@types/range-parser" "*"
-
-"@types/express@*", "@types/express@^4.0.30", "@types/express@^4.0.36":
-  version "4.17.11"
-  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545"
-  integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==
-  dependencies:
-    "@types/body-parser" "*"
-    "@types/express-serve-static-core" "^4.17.18"
-    "@types/qs" "*"
-    "@types/serve-static" "*"
-
-"@types/fast-levenshtein@0.0.1":
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/@types/fast-levenshtein/-/fast-levenshtein-0.0.1.tgz#3a3615cf173645c8fca58d051e4e32824e4bd286"
-  integrity sha1-OjYVzxc2Rcj8pY0FHk4ygk5L0oY=
-
-"@types/findup-sync@^0.3.29":
-  version "0.3.30"
-  resolved "https://registry.yarnpkg.com/@types/findup-sync/-/findup-sync-0.3.30.tgz#8ab7bdbd6ba7cbf4f33b6596fde6fff1129c738d"
-  integrity sha512-Dpt1x3rhz6t8BMTS4vziTVos8VLkF4RngIxMBCSE6w0STmnVEEaoe3w+BG5xHyZXshye9lyZE99lpBDoLGY8eA==
-  dependencies:
-    "@types/minimatch" "*"
-
-"@types/form-data@*":
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.5.0.tgz#5025f7433016f923348434c40006d9a797c1b0e8"
-  integrity sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==
-  dependencies:
-    form-data "*"
-
-"@types/freeport@^1.0.19":
-  version "1.0.21"
-  resolved "https://registry.yarnpkg.com/@types/freeport/-/freeport-1.0.21.tgz#73f6543ed67d3ca3fff97b985591598b7092066f"
-  integrity sha1-c/ZUPtZ9PKP/+XuYVZFZi3CSBm8=
-
-"@types/glob-stream@*":
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.0.tgz#7ede8a33e59140534f8d8adfb8ac9edfb31897bc"
-  integrity sha512-RHv6ZQjcTncXo3thYZrsbAVwoy4vSKosSWhuhuQxLOTv74OJuFQxXkmUuZCr3q9uNBEVCvIzmZL/FeRNbHZGUg==
-  dependencies:
-    "@types/glob" "*"
-    "@types/node" "*"
-
-"@types/glob@*", "@types/glob@^7.1.1":
-  version "7.1.3"
-  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
-  integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
-  dependencies:
-    "@types/minimatch" "*"
-    "@types/node" "*"
-
-"@types/globby@^6.1.0":
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/@types/globby/-/globby-6.1.0.tgz#7c25b975512a89effea2a656ca8cf6db7fb29d11"
-  integrity sha512-j3XSDNoK4LO5T+ZviQD6PqfEjm07QFEacOTbJR3hnLWuWX0ZMLJl9oRPgj1PyrfGbXhfHFkksC9QZ9HFltJyrw==
-  dependencies:
-    "@types/glob" "*"
-
-"@types/gulp-if@0.0.33":
-  version "0.0.33"
-  resolved "https://registry.yarnpkg.com/@types/gulp-if/-/gulp-if-0.0.33.tgz#edece22b7925d9a6db5f9c8c0d7882aa776fb678"
-  integrity sha512-J5lzff21X7r1x/4hSzn02GgIUEyjCqYIXZ9GgGBLhbsD3RiBdqwnkFWgF16/0jO5rcVZ52Zp+6MQMQdvIsWuKg==
-  dependencies:
-    "@types/node" "*"
-    "@types/vinyl" "*"
-
-"@types/html-minifier@^3.5.1":
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/@types/html-minifier/-/html-minifier-3.5.3.tgz#5276845138db2cebc54c789e0aaf87621a21e84f"
-  integrity sha512-j1P/4PcWVVCPEy5lofcHnQ6BtXz9tHGiFPWzqm7TtGuWZEfCHEP446HlkSNc9fQgNJaJZ6ewPtp2aaFla/Uerg==
-  dependencies:
-    "@types/clean-css" "*"
-    "@types/relateurl" "*"
-    "@types/uglify-js" "*"
-
-"@types/http-cache-semantics@*":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a"
-  integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==
-
-"@types/inquirer@*":
-  version "7.3.1"
-  resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d"
-  integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g==
-  dependencies:
-    "@types/through" "*"
-    rxjs "^6.4.0"
-
-"@types/inquirer@0.0.32":
-  version "0.0.32"
-  resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-0.0.32.tgz#a4a08e83741c500a7c3c8e7776014f7f8a65870d"
-  integrity sha1-pKCOg3QcUAp8PI53dgFPf4plhw0=
-  dependencies:
-    "@types/rx" "*"
-    "@types/through" "*"
-
-"@types/is-windows@^0.2.0":
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-0.2.0.tgz#6f24ee48731d31168ea510610d6dd15e5fc9c6ff"
-  integrity sha1-byTuSHMdMRaOpRBhDW3RXl/Jxv8=
-
-"@types/json-schema@^7.0.3":
-  version "7.0.7"
-  resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz"
-  integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
-
 "@types/json-schema@^7.0.7":
   version "7.0.9"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@@ -1045,587 +172,121 @@
 
 "@types/json5@^0.0.29":
   version "0.0.29"
-  resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
+  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
-"@types/keyv@*":
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7"
-  integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==
-  dependencies:
-    "@types/node" "*"
-
-"@types/launchpad@^0.6.0":
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
-  integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
-
 "@types/long@^4.0.0":
   version "4.0.1"
-  resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
   integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
 
-"@types/merge-stream@^1.0.28":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1"
-  integrity sha512-7faLmaE99g/yX0Y9pF1neh2IUqOf/fXMOWCVzsXjqI1EJ91lrgXmaBKf6bRWM164lLyiHxHt6t/ZO/cIzq61XA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/mime@^1":
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
-  integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
-
-"@types/mime@^2.0.0":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
-  integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
-
-"@types/minimatch@*", "@types/minimatch@^3.0.1", "@types/minimatch@^3.0.3":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
-  integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
-
 "@types/minimatch@3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
 "@types/minimist@^1.2.0":
-  version "1.2.1"
-  resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz"
-  integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
+  integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
 
-"@types/mz@0.0.29":
-  version "0.0.29"
-  resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.29.tgz#bc24728c649973f1c7851e9033f9ce525668c27b"
-  integrity sha1-vCRyjGSZc/HHhR6QM/nOUlZowns=
-  dependencies:
-    "@types/bluebird" "*"
-    "@types/node" "*"
-
-"@types/mz@0.0.31", "@types/mz@^0.0.31":
-  version "0.0.31"
-  resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.31.tgz#a4d80c082fefe71e40a7c0f07d1e6555bbbc7b52"
-  integrity sha1-pNgMCC/v5x5Ap8DwfR5lVbu8e1I=
-  dependencies:
-    "@types/node" "*"
-
-"@types/node@*", "@types/node@>= 8":
-  version "14.14.37"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
-  integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
+"@types/node@*":
+  version "16.9.6"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.6.tgz#040a64d7faf9e5d9e940357125f0963012e66f04"
+  integrity sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==
 
 "@types/node@^10.1.0":
-  version "10.17.56"
-  resolved "https://registry.npmjs.org/@types/node/-/node-10.17.56.tgz"
-  integrity sha512-LuAa6t1t0Bfw4CuSR0UITsm1hP17YL+u82kfHGrHUWdhlBtH7sa7jGY5z7glGaIj/WDYDkRtgGd+KCjCzxBW1w==
-
-"@types/node@^4.0.30":
-  version "4.9.5"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-4.9.5.tgz#a3785db96b07a4b56466cc99fd624838746f2e25"
-  integrity sha512-+8fpgbXsbATKRF2ayAlYhPl2E9MPdLjrnK/79ZEpyPJ+k7dZwJm9YM8FK+l4rqL//xHk7PgQhGwz6aA2ckxbCQ==
+  version "10.17.60"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
+  integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
 
 "@types/normalize-package-data@^2.4.0":
-  version "2.4.0"
-  resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
-  integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
+  integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
 
-"@types/opn@^3.0.28":
-  version "3.0.28"
-  resolved "https://registry.yarnpkg.com/@types/opn/-/opn-3.0.28.tgz#097d0d1c9b5749573a5d96df132387bb6d02118a"
-  integrity sha1-CX0NHJtXSVc6XZbfEyOHu20CEYo=
+"@typescript-eslint/eslint-plugin@^4.2.0", "@typescript-eslint/eslint-plugin@^4.29.0":
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.30.0.tgz#4a0c1ae96b953f4e67435e20248d812bfa55e4fb"
+  integrity sha512-NgAnqk55RQ/SD+tZFD9aPwNSeHmDHHe5rtUyhIq0ZeCWZEvo4DK9rYz7v9HDuQZFvn320Ot+AikaCKMFKLlD0g==
   dependencies:
-    "@types/node" "*"
-
-"@types/parse5@^2.2.34":
-  version "2.2.34"
-  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-2.2.34.tgz#e3870a10e82735a720f62d71dcd183ba78ef3a9d"
-  integrity sha1-44cKEOgnNacg9i1x3NGDunjvOp0=
-  dependencies:
-    "@types/node" "*"
-
-"@types/path-is-inside@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.0.tgz#02d6ff38975d684bdec96204494baf9f29f0e17f"
-  integrity sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==
-
-"@types/pem@^1.8.1":
-  version "1.9.5"
-  resolved "https://registry.yarnpkg.com/@types/pem/-/pem-1.9.5.tgz#cd5548b5e0acb4b41a9e21067e9fcd8c57089c99"
-  integrity sha512-C0txxEw8B7DCoD85Ko7SEvzUogNd5VDJ5/YBG8XUcacsOGqxr5Oo4g3OUAfdEDUbhXanwUoVh/ZkMFw77FGPQQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/qs@*":
-  version "6.9.6"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
-  integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==
-
-"@types/range-parser@*":
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
-  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
-
-"@types/relateurl@*":
-  version "0.2.28"
-  resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
-  integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y=
-
-"@types/request@2.0.3":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@types/request/-/request-2.0.3.tgz#bdf0fba9488c822f77e97de3dd8fe357b2fb8c06"
-  integrity sha512-cIvnyFRARxwE4OHpCyYue7H+SxaKFPpeleRCHJicft8QhyTNbVYsMwjvEzEPqG06D2LGHZ+sN5lXc8+bTu6D8A==
-  dependencies:
-    "@types/form-data" "*"
-    "@types/node" "*"
-
-"@types/resolve@0.0.4":
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.4.tgz#9b586d65a947dea88c4bc24da0b905fe9520a0d5"
-  integrity sha1-m1htZalH3qiMS8JNoLkF/pUgoNU=
-  dependencies:
-    "@types/node" "*"
-
-"@types/resolve@0.0.6":
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.6.tgz#0bd2f236c2e1cebb98b79885df57edd71a8d770e"
-  integrity sha512-g+Rg8uMWY76oYTyaL+m7ZcblqF/oj7pE6uEUyACluJx4zcop1Lk14qQiocdEkEVMDFm6DmKpxJhsER+ZuTwG3g==
-  dependencies:
-    "@types/node" "*"
-
-"@types/resolve@0.0.7":
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.7.tgz#b299c13be8d712b1b502fb14a084252acef84f4d"
-  integrity sha512-GPewdjkb0Q76o459qgp6pBLzJj/bD3oveS2kfLhIkZ9U3t3AFKtl5DlFB6lGTw0iZmcmxoGC8lpLW3NNJKrN9A==
-  dependencies:
-    "@types/node" "*"
-
-"@types/responselike@*", "@types/responselike@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
-  integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/rimraf@^0.0.28":
-  version "0.0.28"
-  resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-0.0.28.tgz#5562519bc7963caca8abf7f128cae3b594d41d06"
-  integrity sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY=
-
-"@types/rx-core-binding@*":
-  version "4.0.4"
-  resolved "https://registry.yarnpkg.com/@types/rx-core-binding/-/rx-core-binding-4.0.4.tgz#d969d32f15a62b89e2862c17b3ee78fe329818d3"
-  integrity sha512-5pkfxnC4w810LqBPUwP5bg7SFR/USwhMSaAeZQQbEHeBp57pjKXRlXmqpMrLJB4y1oglR/c2502853uN0I+DAQ==
-  dependencies:
-    "@types/rx-core" "*"
-
-"@types/rx-core@*":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/@types/rx-core/-/rx-core-4.0.3.tgz#0b3354b1238cedbe2b74f6326f139dbc7a591d60"
-  integrity sha1-CzNUsSOM7b4rdPYybxOdvHpZHWA=
-
-"@types/rx-lite-aggregates@*":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-aggregates/-/rx-lite-aggregates-4.0.3.tgz#6efb2b7f3d5f07183a1cb2bd4b1371d7073384c2"
-  integrity sha512-MAGDAHy8cRatm94FDduhJF+iNS5//jrZ/PIfm+QYw9OCeDgbymFHChM8YVIvN2zArwsRftKgE33QfRWvQk4DPg==
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-async@*":
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-async/-/rx-lite-async-4.0.2.tgz#27fbf0caeff029f41e2d2aae638b05e91ceb600c"
-  integrity sha512-vTEv5o8l6702ZwfAM5aOeVDfUwBSDOs+ARoGmWAKQ6LOInQ8J4/zjM7ov12fuTpktUKdMQjkeCp07Vd73mPkxw==
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-backpressure@*":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-backpressure/-/rx-lite-backpressure-4.0.3.tgz#05abb19bdf87cc740196c355e5d0b37bb50b5d56"
-  integrity sha512-Y6aIeQCtNban5XSAF4B8dffhIKu6aAy/TXFlScHzSxh6ivfQBQw6UjxyEJxIOt3IT49YkS+siuayM2H/Q0cmgA==
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-coincidence@*":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-coincidence/-/rx-lite-coincidence-4.0.3.tgz#80bd69acc4054a15cdc1638e2dc8843498cd85c0"
-  integrity sha512-1VNJqzE9gALUyMGypDXZZXzR0Tt7LC9DdAZQ3Ou/Q0MubNU35agVUNXKGHKpNTba+fr8GdIdkC26bRDqtCQBeQ==
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-experimental@*":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-experimental/-/rx-lite-experimental-4.0.1.tgz#c532f5cbdf3f2c15da16ded8930d1b2984023cbd"
-  integrity sha1-xTL1y98/LBXaFt7Ykw0bKYQCPL0=
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-joinpatterns@*":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-joinpatterns/-/rx-lite-joinpatterns-4.0.1.tgz#f70fe370518a8432f29158cc92ffb56b4e4afc3e"
-  integrity sha1-9w/jcFGKhDLykVjMkv+1a05K/D4=
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-testing@*":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-testing/-/rx-lite-testing-4.0.1.tgz#21b19d11f4dfd6ffef5a9d1648e9c8879bfe21e9"
-  integrity sha1-IbGdEfTf1v/vWp0WSOnIh5v+Iek=
-  dependencies:
-    "@types/rx-lite-virtualtime" "*"
-
-"@types/rx-lite-time@*":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-time/-/rx-lite-time-4.0.3.tgz#0eda65474570237598f3448b845d2696f2dbb1c4"
-  integrity sha512-ukO5sPKDRwCGWRZRqPlaAU0SKVxmWwSjiOrLhoQDoWxZWg6vyB9XLEZViKOzIO6LnTIQBlk4UylYV0rnhJLxQw==
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite-virtualtime@*":
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite-virtualtime/-/rx-lite-virtualtime-4.0.3.tgz#4b30cacd0fe2e53af29f04f7438584c7d3959537"
-  integrity sha512-3uC6sGmjpOKatZSVHI2xB1+dedgml669ZRvqxy+WqmGJDVusOdyxcKfyzjW0P3/GrCiN4nmRkLVMhPwHCc5QLg==
-  dependencies:
-    "@types/rx-lite" "*"
-
-"@types/rx-lite@*":
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/@types/rx-lite/-/rx-lite-4.0.6.tgz#3c02921c4244074234f26b772241bcc20c18c253"
-  integrity sha512-oYiDrFIcor9zDm0VDUca1UbROiMYBxMLMaM6qzz4ADAfOmA9r1dYEcAFH+2fsPI5BCCjPvV9pWC3X3flbrvs7w==
-  dependencies:
-    "@types/rx-core" "*"
-    "@types/rx-core-binding" "*"
-
-"@types/rx@*":
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/@types/rx/-/rx-4.1.2.tgz#a4061b3d72b03cf11a38d69e2022a17334c54dc0"
-  integrity sha512-1r8ZaT26Nigq7o4UBGl+aXB2UMFUIdLPP/8bLIP0x3d0pZL46ybKKjhWKaJQWIkLl5QCLD0nK3qTOO1QkwdFaA==
-  dependencies:
-    "@types/rx-core" "*"
-    "@types/rx-core-binding" "*"
-    "@types/rx-lite" "*"
-    "@types/rx-lite-aggregates" "*"
-    "@types/rx-lite-async" "*"
-    "@types/rx-lite-backpressure" "*"
-    "@types/rx-lite-coincidence" "*"
-    "@types/rx-lite-experimental" "*"
-    "@types/rx-lite-joinpatterns" "*"
-    "@types/rx-lite-testing" "*"
-    "@types/rx-lite-time" "*"
-    "@types/rx-lite-virtualtime" "*"
-
-"@types/semver@^5.3.30":
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
-  integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
-
-"@types/serve-static@*", "@types/serve-static@^1.7.31":
-  version "1.13.9"
-  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e"
-  integrity sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==
-  dependencies:
-    "@types/mime" "^1"
-    "@types/node" "*"
-
-"@types/spdy@^3.4.1":
-  version "3.4.4"
-  resolved "https://registry.yarnpkg.com/@types/spdy/-/spdy-3.4.4.tgz#3282fd4ad8c4603aa49f7017dd520a08a345b2bc"
-  integrity sha512-N9LBlbVRRYq6HgYpPkqQc3a9HJ/iEtVZToW6xlTtJiMhmRJ7jJdV7TaZQJw/Ve/1ePUsQiCTDc4JMuzzag94GA==
-  dependencies:
-    "@types/node" "*"
-
-"@types/temp@^0.8.28":
-  version "0.8.34"
-  resolved "https://registry.yarnpkg.com/@types/temp/-/temp-0.8.34.tgz#03e4b3cb67cbb48c425bbf54b12230fef85540ac"
-  integrity sha512-oLa9c5LHXgS6UimpEVp08De7QvZ+Dfu5bMQuWyMhf92Z26Q10ubEMOWy9OEfUdzW7Y/sDWVHmUaLFtmnX/2j0w==
-  dependencies:
-    "@types/node" "*"
-
-"@types/through@*":
-  version "0.0.30"
-  resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895"
-  integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==
-  dependencies:
-    "@types/node" "*"
-
-"@types/ua-parser-js@^0.7.31":
-  version "0.7.35"
-  resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.35.tgz#cca67a95deb9165e4b1f449471801e6489d3fe93"
-  integrity sha512-PsPx0RLbo2Un8+ff2buzYJnZjzwhD3jQHPOG2PtVIeOhkRDddMcKU8vJtHpzzfLB95dkUi0qAkfLg2l2Fd0yrQ==
-
-"@types/uglify-js@*":
-  version "3.13.0"
-  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
-  integrity sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q==
-  dependencies:
-    source-map "^0.6.1"
-
-"@types/update-notifier@^1.0.0":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-1.0.3.tgz#3c7ee1921af6f16149cdcaef356baf57d7a0b806"
-  integrity sha512-BLStNhP2DFF7funARwTcoD6tetRte8NK3Sc59mn7GNALCN975jOlKX3dGvsFxXr/HwQMxxCuRn9IWB3WQ7odHQ==
-
-"@types/uuid@^3.4.3":
-  version "3.4.9"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.9.tgz#fcf01997bbc9f7c09ae5f91383af076d466594e1"
-  integrity sha512-XDwyIlt/47l2kWLTzw/mtrpLdB+GPSskR2n/PIcPn+VYhVO77rGhRncIR5GPU0KRzXuqkDO+J5qqrG0Y8P6jzQ==
-
-"@types/vinyl-fs@0.0.28":
-  version "0.0.28"
-  resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-0.0.28.tgz#4663017bc802c6570eae4f3409fd5cabf97cbfde"
-  integrity sha1-RmMBe8gCxlcOrk80Cf1cq/l8v94=
-  dependencies:
-    "@types/glob-stream" "*"
-    "@types/node" "*"
-    "@types/vinyl" "*"
-
-"@types/vinyl-fs@^2.4.8":
-  version "2.4.11"
-  resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz#b98119b8bb2494141eaf649b09fbfeb311161206"
-  integrity sha512-2OzQSfIr9CqqWMGqmcERE6Hnd2KY3eBVtFaulVo3sJghplUcaeMdL9ZjEiljcQQeHjheWY9RlNmumjIAvsBNaA==
-  dependencies:
-    "@types/glob-stream" "*"
-    "@types/node" "*"
-    "@types/vinyl" "*"
-
-"@types/vinyl@*", "@types/vinyl@^2.0.0":
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a"
-  integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==
-  dependencies:
-    "@types/expect" "^1.20.4"
-    "@types/node" "*"
-
-"@types/whatwg-url@^6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"
-  integrity sha512-tonhlcbQ2eho09am6RHnHOgvtDfDYINd5rgxD+2YSkKENooVCFsWizJz139MQW/PV8FfClyKrNe9ZbdHrSCxGg==
-  dependencies:
-    "@types/node" "*"
-
-"@types/which@^1.3.1":
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"
-  integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==
-
-"@types/yeoman-generator@^2.0.3":
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/@types/yeoman-generator/-/yeoman-generator-2.0.3.tgz#f4b161ee354078b526e0901a5a5f87d4f8e085f6"
-  integrity sha512-vch2UFd6k7DdfWEv/alRwZIRXQoxZNUDpfLOK24+005dzE1HVnwSWfETF3WxJnWlsOcH87wU4uzldAE/7F/6Lw==
-  dependencies:
-    "@types/events" "*"
-    "@types/inquirer" "*"
-
-"@typescript-eslint/eslint-plugin@^4.2.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz"
-  integrity sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ==
-  dependencies:
-    "@typescript-eslint/experimental-utils" "4.21.0"
-    "@typescript-eslint/scope-manager" "4.21.0"
-    debug "^4.1.1"
-    functional-red-black-tree "^1.0.1"
-    lodash "^4.17.15"
-    regexpp "^3.0.0"
-    semver "^7.3.2"
-    tsutils "^3.17.1"
-
-"@typescript-eslint/eslint-plugin@^4.25.0":
-  version "4.29.2"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d"
-  integrity sha512-x4EMgn4BTfVd9+Z+r+6rmWxoAzBaapt4QFqE+d8L8sUtYZYLDTK6VG/y/SMMWA5t1/BVU5Kf+20rX4PtWzUYZg==
-  dependencies:
-    "@typescript-eslint/experimental-utils" "4.29.2"
-    "@typescript-eslint/scope-manager" "4.29.2"
+    "@typescript-eslint/experimental-utils" "4.30.0"
+    "@typescript-eslint/scope-manager" "4.30.0"
     debug "^4.3.1"
     functional-red-black-tree "^1.0.1"
     regexpp "^3.1.0"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/experimental-utils@4.21.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz"
-  integrity sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==
-  dependencies:
-    "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/scope-manager" "4.21.0"
-    "@typescript-eslint/types" "4.21.0"
-    "@typescript-eslint/typescript-estree" "4.21.0"
-    eslint-scope "^5.0.0"
-    eslint-utils "^2.0.0"
-
-"@typescript-eslint/experimental-utils@4.29.2":
-  version "4.29.2"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.2.tgz#5f67fb5c5757ef2cb3be64817468ba35c9d4e3b7"
-  integrity sha512-P6mn4pqObhftBBPAv4GQtEK7Yos1fz/MlpT7+YjH9fTxZcALbiiPKuSIfYP/j13CeOjfq8/fr9Thr2glM9ub7A==
+"@typescript-eslint/experimental-utils@4.30.0":
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.30.0.tgz#9e49704fef568432ae16fc0d6685c13d67db0fd5"
+  integrity sha512-K8RNIX9GnBsv5v4TjtwkKtqMSzYpjqAQg/oSphtxf3xxdt6T0owqnpojztjjTcatSteH3hLj3t/kklKx87NPqw==
   dependencies:
     "@types/json-schema" "^7.0.7"
-    "@typescript-eslint/scope-manager" "4.29.2"
-    "@typescript-eslint/types" "4.29.2"
-    "@typescript-eslint/typescript-estree" "4.29.2"
+    "@typescript-eslint/scope-manager" "4.30.0"
+    "@typescript-eslint/types" "4.30.0"
+    "@typescript-eslint/typescript-estree" "4.30.0"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
 "@typescript-eslint/parser@^4.2.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.21.0.tgz"
-  integrity sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA==
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.30.0.tgz#6abd720f66bd790f3e0e80c3be77180c8fcb192d"
+  integrity sha512-HJ0XuluSZSxeboLU7Q2VQ6eLlCwXPBOGnA7CqgBnz2Db3JRQYyBDJgQnop6TZ+rsbSx5gEdWhw4rE4mDa1FnZg==
   dependencies:
-    "@typescript-eslint/scope-manager" "4.21.0"
-    "@typescript-eslint/types" "4.21.0"
-    "@typescript-eslint/typescript-estree" "4.21.0"
-    debug "^4.1.1"
+    "@typescript-eslint/scope-manager" "4.30.0"
+    "@typescript-eslint/types" "4.30.0"
+    "@typescript-eslint/typescript-estree" "4.30.0"
+    debug "^4.3.1"
 
-"@typescript-eslint/scope-manager@4.21.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz"
-  integrity sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==
+"@typescript-eslint/scope-manager@4.30.0":
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.30.0.tgz#1a3ffbb385b1a06be85cd5165a22324f069a85ee"
+  integrity sha512-VJ/jAXovxNh7rIXCQbYhkyV2Y3Ac/0cVHP/FruTJSAUUm4Oacmn/nkN5zfWmWFEanN4ggP0vJSHOeajtHq3f8A==
   dependencies:
-    "@typescript-eslint/types" "4.21.0"
-    "@typescript-eslint/visitor-keys" "4.21.0"
+    "@typescript-eslint/types" "4.30.0"
+    "@typescript-eslint/visitor-keys" "4.30.0"
 
-"@typescript-eslint/scope-manager@4.29.2":
-  version "4.29.2"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz#442b0f029d981fa402942715b1718ac7fcd5aa1b"
-  integrity sha512-mfHmvlQxmfkU8D55CkZO2sQOueTxLqGvzV+mG6S/6fIunDiD2ouwsAoiYCZYDDK73QCibYjIZmGhpvKwAB5BOA==
+"@typescript-eslint/types@4.30.0":
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.30.0.tgz#fb9d9b0358426f18687fba82eb0b0f869780204f"
+  integrity sha512-YKldqbNU9K4WpTNwBqtAerQKLLW/X2A/j4yw92e3ZJYLx+BpKLeheyzoPfzIXHfM8BXfoleTdiYwpsvVPvHrDw==
+
+"@typescript-eslint/typescript-estree@4.30.0":
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.30.0.tgz#ae57833da72a753f4846cd3053758c771670c2ac"
+  integrity sha512-6WN7UFYvykr/U0Qgy4kz48iGPWILvYL34xXJxvDQeiRE018B7POspNRVtAZscWntEPZpFCx4hcz/XBT+erenfg==
   dependencies:
-    "@typescript-eslint/types" "4.29.2"
-    "@typescript-eslint/visitor-keys" "4.29.2"
-
-"@typescript-eslint/types@4.21.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz"
-  integrity sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==
-
-"@typescript-eslint/types@4.29.2":
-  version "4.29.2"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd"
-  integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ==
-
-"@typescript-eslint/typescript-estree@4.21.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz"
-  integrity sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==
-  dependencies:
-    "@typescript-eslint/types" "4.21.0"
-    "@typescript-eslint/visitor-keys" "4.21.0"
-    debug "^4.1.1"
-    globby "^11.0.1"
-    is-glob "^4.0.1"
-    semver "^7.3.2"
-    tsutils "^3.17.1"
-
-"@typescript-eslint/typescript-estree@4.29.2":
-  version "4.29.2"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219"
-  integrity sha512-TJ0/hEnYxapYn9SGn3dCnETO0r+MjaxtlWZ2xU+EvytF0g4CqTpZL48SqSNn2hXsPolnewF30pdzR9a5Lj3DNg==
-  dependencies:
-    "@typescript-eslint/types" "4.29.2"
-    "@typescript-eslint/visitor-keys" "4.29.2"
+    "@typescript-eslint/types" "4.30.0"
+    "@typescript-eslint/visitor-keys" "4.30.0"
     debug "^4.3.1"
     globby "^11.0.3"
     is-glob "^4.0.1"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@4.21.0":
-  version "4.21.0"
-  resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz"
-  integrity sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==
+"@typescript-eslint/visitor-keys@4.30.0":
+  version "4.30.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.30.0.tgz#a47c6272fc71b0c627d1691f68eaecf4ad71445e"
+  integrity sha512-pNaaxDt/Ol/+JZwzP7MqWc8PJQTUhZwoee/PVlQ+iYoYhagccvoHnC9e4l+C/krQYYkENxznhVSDwClIbZVxRw==
   dependencies:
-    "@typescript-eslint/types" "4.21.0"
+    "@typescript-eslint/types" "4.30.0"
     eslint-visitor-keys "^2.0.0"
 
-"@typescript-eslint/visitor-keys@4.29.2":
-  version "4.29.2"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df"
-  integrity sha512-bDgJLQ86oWHJoZ1ai4TZdgXzJxsea3Ee9u9wsTAvjChdj2WLcVsgWYAPeY7RQMn16tKrlQaBnpKv7KBfs4EQag==
-  dependencies:
-    "@typescript-eslint/types" "4.29.2"
-    eslint-visitor-keys "^2.0.0"
-
-"@webcomponents/webcomponentsjs@^1.0.7":
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
-  integrity sha512-eLH04VBMpuZGzBIhOnUjECcQPEPcmfhWEijW9u1B5I+2PPYdWf3vWUExdDxu4Y3GljRSTCOlWnGtS9tpzmXMyQ==
-
-JSONStream@^1.2.1, JSONStream@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
-  integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
-  dependencies:
-    jsonparse "^1.2.0"
-    through ">=2.2.7 <3"
-
-accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
-  version "1.3.7"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
-  integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
-  dependencies:
-    mime-types "~2.1.24"
-    negotiator "0.6.2"
-
-accessibility-developer-tools@^2.12.0:
-  version "2.12.0"
-  resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
-  integrity sha1-PaDM6dbsY3OWS4TzXbfPw996tRQ=
-
-acorn-jsx@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
-  integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=
-  dependencies:
-    acorn "^3.0.4"
-
 acorn-jsx@^5.3.1:
-  version "5.3.1"
-  resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz"
-  integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^3.0.4:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-  integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
-
-acorn@^5.5.0:
-  version "5.7.4"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
-  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
-
-acorn@^7.1.0, acorn@^7.4.0:
+acorn@^7.4.0:
   version "7.4.1"
-  resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
-adm-zip@~0.4.3:
-  version "0.4.16"
-  resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365"
-  integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==
-
-after@0.8.2:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
-  integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
-
-agent-base@6:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
-  integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
-  dependencies:
-    debug "4"
-
-agent-base@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
-  integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
-  dependencies:
-    es6-promisify "^5.0.0"
-
-ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
+ajv@^6.10.0, ajv@^6.12.4:
   version "6.12.6"
-  resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
   dependencies:
     fast-deep-equal "^3.1.1"
@@ -1634,208 +295,68 @@
     uri-js "^4.2.2"
 
 ajv@^8.0.1:
-  version "8.0.5"
-  resolved "https://registry.npmjs.org/ajv/-/ajv-8.0.5.tgz"
-  integrity sha512-RkiLa/AeJx7+9OvniQ/qeWu0w74A8DiPPBclQ6ji3ZQkv5KamO+QGpqmi7O4JIw3rHGUXZ6CoP9tsAkn3gyazg==
+  version "8.6.2"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571"
+  integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
     require-from-string "^2.0.2"
     uri-js "^4.2.2"
 
-ansi-align@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-1.1.0.tgz#2f0c1658829739add5ebb15e6b0c6e3423f016ba"
-  integrity sha1-LwwWWIKXOa3V67FeawxuNCPwFro=
-  dependencies:
-    string-width "^1.0.1"
-
-ansi-align@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
-  integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
-  dependencies:
-    string-width "^2.0.0"
-
 ansi-align@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
   integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
   dependencies:
     string-width "^3.0.0"
 
 ansi-colors@^4.1.1:
   version "4.1.1"
-  resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
   integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
 
-ansi-escapes@^1.1.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
-  integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
-
 ansi-escapes@^4.2.1:
   version "4.3.2"
-  resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
   integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
   dependencies:
     type-fest "^0.21.3"
 
-ansi-regex@^2.0.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
-  integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
-
-ansi-regex@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
-  integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
-
 ansi-regex@^4.1.0:
   version "4.1.0"
-  resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
   integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
 
 ansi-regex@^5.0.0:
   version "5.0.0"
-  resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
   integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
 
-ansi-styles@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-  integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
-
 ansi-styles@^3.2.1:
   version "3.2.1"
-  resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
   integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
   dependencies:
     color-convert "^1.9.0"
 
 ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.3.0"
-  resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
   integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
   dependencies:
     color-convert "^2.0.1"
 
-ansi-styles@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
-  integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
-
-any-promise@^1.0.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
-  integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
-
-anymatch@^1.3.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
-  integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==
-  dependencies:
-    micromatch "^2.1.5"
-    normalize-path "^2.0.0"
-
-append-field@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
-  integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
-
-archiver-utils@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2"
-  integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==
-  dependencies:
-    glob "^7.1.4"
-    graceful-fs "^4.2.0"
-    lazystream "^1.0.0"
-    lodash.defaults "^4.2.0"
-    lodash.difference "^4.5.0"
-    lodash.flatten "^4.4.0"
-    lodash.isplainobject "^4.0.6"
-    lodash.union "^4.6.0"
-    normalize-path "^3.0.0"
-    readable-stream "^2.0.0"
-
-archiver@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0"
-  integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==
-  dependencies:
-    archiver-utils "^2.1.0"
-    async "^2.6.3"
-    buffer-crc32 "^0.2.1"
-    glob "^7.1.4"
-    readable-stream "^3.4.0"
-    tar-stream "^2.1.0"
-    zip-stream "^2.1.2"
-
 argparse@^1.0.7:
   version "1.0.10"
-  resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
   integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
   dependencies:
     sprintf-js "~1.0.2"
 
-arr-diff@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
-  integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=
-  dependencies:
-    arr-flatten "^1.0.1"
-
-arr-diff@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
-  integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
-
-arr-flatten@^1.0.1, arr-flatten@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
-  integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
-
-arr-union@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-  integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
-
-array-back@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022"
-  integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==
-  dependencies:
-    typical "^2.6.1"
-
-array-back@^3.0.1:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
-  integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
-
-array-differ@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
-  integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=
-
-array-differ@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b"
-  integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==
-
-array-find-index@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
-  integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
-
-array-flatten@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
-  integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
-
-array-includes@^3.1.1:
+array-includes@^3.1.3:
   version "3.1.3"
-  resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a"
   integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==
   dependencies:
     call-bind "^1.0.2"
@@ -1844,597 +365,43 @@
     get-intrinsic "^1.1.1"
     is-string "^1.0.5"
 
-array-union@^1.0.1, array-union@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
-  integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
-  dependencies:
-    array-uniq "^1.0.1"
-
 array-union@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
   integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
 
-array-uniq@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
-  integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
-
-array-unique@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
-  integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=
-
-array-unique@^0.3.2:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
-  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-
-array.prototype.flat@^1.2.3:
+array.prototype.flat@^1.2.4:
   version "1.2.4"
-  resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
   integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==
   dependencies:
     call-bind "^1.0.0"
     define-properties "^1.1.3"
     es-abstract "^1.18.0-next.1"
 
-arraybuffer.slice@~0.0.7:
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
-  integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
-
-arrify@^1.0.0, arrify@^1.0.1:
+arrify@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
   integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
 
-arrify@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
-  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
-
-asn1@~0.2.3:
-  version "0.2.4"
-  resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
-  integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
-  dependencies:
-    safer-buffer "~2.1.0"
-
-assert-plus@1.0.0, assert-plus@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
-  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
-
-assign-symbols@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
-  integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
-
 astral-regex@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
   integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
 
-async-each@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
-  integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
-
-async@0.9.x:
-  version "0.9.2"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
-  integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
-
-async@^2.0.0, async@^2.0.1, async@^2.1.2, async@^2.4.1, async@^2.6.0, async@^2.6.2, async@^2.6.3:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
-  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
-  dependencies:
-    lodash "^4.17.14"
-
-async@^3.1.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
-  integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
-
-async@~0.2.9:
-  version "0.2.10"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-  integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
-
-asynckit@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
-  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
-
-atob-lite@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696"
-  integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=
-
-atob@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
-  integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
-
-aws-sign2@~0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
-  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
-
-aws4@^1.8.0:
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
-  integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
-
-axios@^0.21.1:
-  version "0.21.1"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
-  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
-  dependencies:
-    follow-redirects "^1.10.0"
-
-babel-code-frame@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
-  integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
-  dependencies:
-    chalk "^1.1.3"
-    esutils "^2.0.2"
-    js-tokens "^3.0.2"
-
-babel-generator@^6.26.1:
-  version "6.26.1"
-  resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
-  integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==
-  dependencies:
-    babel-messages "^6.23.0"
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    detect-indent "^4.0.0"
-    jsesc "^1.3.0"
-    lodash "^4.17.4"
-    source-map "^0.5.7"
-    trim-right "^1.0.1"
-
-babel-helper-evaluate-path@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.5.0.tgz#a62fa9c4e64ff7ea5cea9353174ef023a900a67c"
-  integrity sha512-mUh0UhS607bGh5wUMAQfOpt2JX2ThXMtppHRdRU1kL7ZLRWIXxoV2UIV1r2cAeeNeU1M5SB5/RSUgUxrK8yOkA==
-
-babel-helper-flip-expressions@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.4.3.tgz#3696736a128ac18bc25254b5f40a22ceb3c1d3fd"
-  integrity sha1-NpZzahKKwYvCUlS19AoizrPB0/0=
-
-babel-helper-is-nodes-equiv@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz#34e9b300b1479ddd98ec77ea0bbe9342dfe39684"
-  integrity sha1-NOmzALFHnd2Y7HfqC76TQt/jloQ=
-
-babel-helper-is-void-0@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-is-void-0/-/babel-helper-is-void-0-0.4.3.tgz#7d9c01b4561e7b95dbda0f6eee48f5b60e67313e"
-  integrity sha1-fZwBtFYee5Xb2g9u7kj1tg5nMT4=
-
-babel-helper-mark-eval-scopes@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.4.3.tgz#d244a3bef9844872603ffb46e22ce8acdf551562"
-  integrity sha1-0kSjvvmESHJgP/tG4izorN9VFWI=
-
-babel-helper-remove-or-void@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.4.3.tgz#a4f03b40077a0ffe88e45d07010dee241ff5ae60"
-  integrity sha1-pPA7QAd6D/6I5F0HAQ3uJB/1rmA=
-
-babel-helper-to-multiple-sequence-expressions@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d"
-  integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA==
-
-babel-messages@^6.23.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
-  integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=
-  dependencies:
-    babel-runtime "^6.22.0"
-
-babel-plugin-dynamic-import-node@^2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
-  integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==
-  dependencies:
-    object.assign "^4.1.0"
-
-babel-plugin-minify-builtins@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz#31eb82ed1a0d0efdc31312f93b6e4741ce82c36b"
-  integrity sha512-wpqbN7Ov5hsNwGdzuzvFcjgRlzbIeVv1gMIlICbPj0xkexnfoIDe7q+AZHMkQmAE/F9R5jkrB6TLfTegImlXag==
-
-babel-plugin-minify-constant-folding@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.5.0.tgz#f84bc8dbf6a561e5e350ff95ae216b0ad5515b6e"
-  integrity sha512-Vj97CTn/lE9hR1D+jKUeHfNy+m1baNiJ1wJvoGyOBUx7F7kJqDZxr9nCHjO/Ad+irbR3HzR6jABpSSA29QsrXQ==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-
-babel-plugin-minify-dead-code-elimination@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.5.1.tgz#1a0c68e44be30de4976ca69ffc535e08be13683f"
-  integrity sha512-x8OJOZIrRmQBcSqxBcLbMIK8uPmTvNWPXH2bh5MDCW1latEqYiRMuUkPImKcfpo59pTUB2FT7HfcgtG8ZlR5Qg==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-    babel-helper-mark-eval-scopes "^0.4.3"
-    babel-helper-remove-or-void "^0.4.3"
-    lodash "^4.17.11"
-
-babel-plugin-minify-flip-comparisons@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.4.3.tgz#00ca870cb8f13b45c038b3c1ebc0f227293c965a"
-  integrity sha1-AMqHDLjxO0XAOLPB68DyJyk8llo=
-  dependencies:
-    babel-helper-is-void-0 "^0.4.3"
-
-babel-plugin-minify-guarded-expressions@^0.4.3, babel-plugin-minify-guarded-expressions@^0.4.4:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.4.4.tgz#818960f64cc08aee9d6c75bec6da974c4d621135"
-  integrity sha512-RMv0tM72YuPPfLT9QLr3ix9nwUIq+sHT6z8Iu3sLbqldzC1Dls8DPCywzUIzkTx9Zh1hWX4q/m9BPoPed9GOfA==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-    babel-helper-flip-expressions "^0.4.3"
-
-babel-plugin-minify-infinity@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.4.3.tgz#dfb876a1b08a06576384ef3f92e653ba607b39ca"
-  integrity sha1-37h2obCKBldjhO8/kuZTumB7Oco=
-
-babel-plugin-minify-mangle-names@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.5.0.tgz#bcddb507c91d2c99e138bd6b17a19c3c271e3fd3"
-  integrity sha512-3jdNv6hCAw6fsX1p2wBGPfWuK69sfOjfd3zjUXkbq8McbohWy23tpXfy5RnToYWggvqzuMOwlId1PhyHOfgnGw==
-  dependencies:
-    babel-helper-mark-eval-scopes "^0.4.3"
-
-babel-plugin-minify-numeric-literals@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.4.3.tgz#8e4fd561c79f7801286ff60e8c5fd9deee93c0bc"
-  integrity sha1-jk/VYcefeAEob/YOjF/Z3u6TwLw=
-
-babel-plugin-minify-replace@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.5.0.tgz#d3e2c9946c9096c070efc96761ce288ec5c3f71c"
-  integrity sha512-aXZiaqWDNUbyNNNpWs/8NyST+oU7QTpK7J9zFEFSA0eOmtUNMU3fczlTTTlnCxHmq/jYNFEmkkSG3DDBtW3Y4Q==
-
-babel-plugin-minify-simplify@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.5.1.tgz#f21613c8b95af3450a2ca71502fdbd91793c8d6a"
-  integrity sha512-OSYDSnoCxP2cYDMk9gxNAed6uJDiDz65zgL6h8d3tm8qXIagWGMLWhqysT6DY3Vs7Fgq7YUDcjOomhVUb+xX6A==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-    babel-helper-flip-expressions "^0.4.3"
-    babel-helper-is-nodes-equiv "^0.0.1"
-    babel-helper-to-multiple-sequence-expressions "^0.5.0"
-
-babel-plugin-minify-type-constructors@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.4.3.tgz#1bc6f15b87f7ab1085d42b330b717657a2156500"
-  integrity sha1-G8bxW4f3qxCF1CszC3F2V6IVZQA=
-  dependencies:
-    babel-helper-is-void-0 "^0.4.3"
-
-babel-plugin-transform-inline-consecutive-adds@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1"
-  integrity sha1-Mj1Ho+pjqDp6w8gRro5pQfrysNE=
-
-babel-plugin-transform-member-expression-literals@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.9.4.tgz#37039c9a0c3313a39495faac2ff3a6b5b9d038bf"
-  integrity sha1-NwOcmgwzE6OUlfqsL/OmtbnQOL8=
-
-babel-plugin-transform-merge-sibling-variables@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.9.4.tgz#85b422fc3377b449c9d1cde44087203532401dae"
-  integrity sha1-hbQi/DN3tEnJ0c3kQIcgNTJAHa4=
-
-babel-plugin-transform-minify-booleans@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.9.4.tgz#acbb3e56a3555dd23928e4b582d285162dd2b198"
-  integrity sha1-rLs+VqNVXdI5KOS1gtKFFi3SsZg=
-
-babel-plugin-transform-property-literals@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.9.4.tgz#98c1d21e255736573f93ece54459f6ce24985d39"
-  integrity sha1-mMHSHiVXNlc/k+zlRFn2ziSYXTk=
-  dependencies:
-    esutils "^2.0.2"
-
-babel-plugin-transform-regexp-constructors@^0.4.3:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz#58b7775b63afcf33328fae9a5f88fbd4fb0b4965"
-  integrity sha1-WLd3W2OvzzMyj66aX4j71PsLSWU=
-
-babel-plugin-transform-remove-console@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780"
-  integrity sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A=
-
-babel-plugin-transform-remove-debugger@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.9.4.tgz#42b727631c97978e1eb2d199a7aec84a18339ef2"
-  integrity sha1-QrcnYxyXl44estGZp67IShgznvI=
-
-babel-plugin-transform-remove-undefined@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.5.0.tgz#80208b31225766c630c97fa2d288952056ea22dd"
-  integrity sha512-+M7fJYFaEE/M9CXa0/IRkDbiV3wRELzA1kKQFCJ4ifhrzLKn/9VCCgj9OFmYWwBd8IB48YdgPkHYtbYq+4vtHQ==
-  dependencies:
-    babel-helper-evaluate-path "^0.5.0"
-
-babel-plugin-transform-simplify-comparison-operators@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.9.4.tgz#f62afe096cab0e1f68a2d753fdf283888471ceb9"
-  integrity sha1-9ir+CWyrDh9ootdT/fKDiIRxzrk=
-
-babel-plugin-transform-undefined-to-void@^6.9.4:
-  version "6.9.4"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280"
-  integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA=
-
-babel-preset-minify@^0.5.0:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/babel-preset-minify/-/babel-preset-minify-0.5.1.tgz#25f5d0bce36ec818be80338d0e594106e21eaa9f"
-  integrity sha512-1IajDumYOAPYImkHbrKeiN5AKKP9iOmRoO2IPbIuVp0j2iuCcj0n7P260z38siKMZZ+85d3mJZdtW8IgOv+Tzg==
-  dependencies:
-    babel-plugin-minify-builtins "^0.5.0"
-    babel-plugin-minify-constant-folding "^0.5.0"
-    babel-plugin-minify-dead-code-elimination "^0.5.1"
-    babel-plugin-minify-flip-comparisons "^0.4.3"
-    babel-plugin-minify-guarded-expressions "^0.4.4"
-    babel-plugin-minify-infinity "^0.4.3"
-    babel-plugin-minify-mangle-names "^0.5.0"
-    babel-plugin-minify-numeric-literals "^0.4.3"
-    babel-plugin-minify-replace "^0.5.0"
-    babel-plugin-minify-simplify "^0.5.1"
-    babel-plugin-minify-type-constructors "^0.4.3"
-    babel-plugin-transform-inline-consecutive-adds "^0.4.3"
-    babel-plugin-transform-member-expression-literals "^6.9.4"
-    babel-plugin-transform-merge-sibling-variables "^6.9.4"
-    babel-plugin-transform-minify-booleans "^6.9.4"
-    babel-plugin-transform-property-literals "^6.9.4"
-    babel-plugin-transform-regexp-constructors "^0.4.3"
-    babel-plugin-transform-remove-console "^6.9.4"
-    babel-plugin-transform-remove-debugger "^6.9.4"
-    babel-plugin-transform-remove-undefined "^0.5.0"
-    babel-plugin-transform-simplify-comparison-operators "^6.9.4"
-    babel-plugin-transform-undefined-to-void "^6.9.4"
-    lodash "^4.17.11"
-
-babel-runtime@^6.22.0, babel-runtime@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
-  integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
-  dependencies:
-    core-js "^2.4.0"
-    regenerator-runtime "^0.11.0"
-
-babel-traverse@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
-  integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
-  dependencies:
-    babel-code-frame "^6.26.0"
-    babel-messages "^6.23.0"
-    babel-runtime "^6.26.0"
-    babel-types "^6.26.0"
-    babylon "^6.18.0"
-    debug "^2.6.8"
-    globals "^9.18.0"
-    invariant "^2.2.2"
-    lodash "^4.17.4"
-
-babel-types@^6.26.0:
-  version "6.26.0"
-  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
-  integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
-  dependencies:
-    babel-runtime "^6.26.0"
-    esutils "^2.0.2"
-    lodash "^4.17.4"
-    to-fast-properties "^1.0.3"
-
-babylon@^6.18.0:
-  version "6.18.0"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
-  integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
-
-babylon@^7.0.0-beta.42:
-  version "7.0.0-beta.47"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.47.tgz#6d1fa44f0abec41ab7c780481e62fd9aafbdea80"
-  integrity sha512-+rq2cr4GDhtToEzKFD6KZZMDBXhjFAr9JjPw9pAppZACeEWqNM294j+NdBzkSHYXwzzBmVjZ3nEVJlOhbR2gOQ==
-
-backo2@1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
-  integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
-
 balanced-match@^1.0.0:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
-base64-arraybuffer@0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
-  integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
-
-base64-js@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1"
-  integrity sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=
-
-base64-js@^1.3.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
-  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-base64id@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
-  integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-
-base@^0.11.1:
-  version "0.11.2"
-  resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
-  integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
-  dependencies:
-    cache-base "^1.0.1"
-    class-utils "^0.3.5"
-    component-emitter "^1.2.1"
-    define-property "^1.0.0"
-    isobject "^3.0.1"
-    mixin-deep "^1.2.0"
-    pascalcase "^0.1.1"
-
-bcrypt-pbkdf@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
-  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
-  dependencies:
-    tweetnacl "^0.14.3"
-
-before-after-hook@^2.0.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.1.tgz#73540563558687586b52ed217dad6a802ab1549c"
-  integrity sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw==
-
-binary-extensions@^1.0.0:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
-  integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
-
-binaryextensions@^2.1.2:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
-  integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
-
-bindings@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
-  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
-  dependencies:
-    file-uri-to-path "1.0.0"
-
-bl@^1.0.0:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7"
-  integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==
-  dependencies:
-    readable-stream "^2.3.5"
-    safe-buffer "^5.1.1"
-
-bl@^4.0.3:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
-  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
-  dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
-
-blob@0.0.5:
-  version "0.0.5"
-  resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
-  integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
-
-body-parser@1.19.0, body-parser@^1.17.2:
-  version "1.19.0"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
-  integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
-  dependencies:
-    bytes "3.1.0"
-    content-type "~1.0.4"
-    debug "2.6.9"
-    depd "~1.1.2"
-    http-errors "1.7.2"
-    iconv-lite "0.4.24"
-    on-finished "~2.3.0"
-    qs "6.7.0"
-    raw-body "2.4.0"
-    type-is "~1.6.17"
-
 boolbase@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
   integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
 
-bower-config@^1.4.0, bower-config@^1.4.1:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.3.tgz#3454fecdc5f08e7aa9cc6d556e492be0669689ae"
-  integrity sha512-MVyyUk3d1S7d2cl6YISViwJBc2VXCkxF5AUFykvN0PQj5FsUiMNSgAYTso18oRFfyZ6XEtjrgg9MAaufHbOwNw==
-  dependencies:
-    graceful-fs "^4.1.3"
-    minimist "^0.2.1"
-    mout "^1.0.0"
-    osenv "^0.1.3"
-    untildify "^2.1.0"
-    wordwrap "^0.0.3"
-
-bower-json@^0.8.1:
-  version "0.8.4"
-  resolved "https://registry.yarnpkg.com/bower-json/-/bower-json-0.8.4.tgz#9c3b375870dcd9581350c1f403f6383dbf6a18b1"
-  integrity sha512-mMKghvq9ivbuzSsY5nrOLnDtZIJMUCpysqbGaGW3mj88JAcuSi8ZAzIt34vNZjohy0aR9VXLwgPTZGnBX2Vpjg==
-  dependencies:
-    deep-extend "^0.5.1"
-    ends-with "^0.2.0"
-    ext-list "^2.0.0"
-    graceful-fs "^4.1.3"
-    intersect "^1.0.1"
-    sort-keys-length "^1.0.0"
-
-bower-logger@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/bower-logger/-/bower-logger-0.2.2.tgz#39be07e979b2fc8e03a94634205ed9422373d381"
-  integrity sha1-Ob4H6Xmy/I4DqUY0IF7ZQiNz04E=
-
-bower@^1.8.8:
-  version "1.8.12"
-  resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.12.tgz#44cfca2a5e04b8d9a066621e24c8b179d8ac321e"
-  integrity sha512-u1xy9SrwwoPlgjuHNjhV+YUPVdqyBj2ALBxuzeIUKXaPI2i2xypGgxqXkuHcITGdi5yBj5JuXgyMvgiWiS1S3Q==
-
-boxen@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/boxen/-/boxen-0.6.0.tgz#8364d4248ac34ff0ef1b2f2bf49a6c60ce0d81b6"
-  integrity sha1-g2TUJIrDT/DvGy8r9JpsYM4NgbY=
-  dependencies:
-    ansi-align "^1.1.0"
-    camelcase "^2.1.0"
-    chalk "^1.1.1"
-    cli-boxes "^1.0.0"
-    filled-array "^1.0.0"
-    object-assign "^4.0.1"
-    repeating "^2.0.0"
-    string-width "^1.0.1"
-    widest-line "^1.0.0"
-
-boxen@^1.2.1:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
-  integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
-  dependencies:
-    ansi-align "^2.0.0"
-    camelcase "^4.0.0"
-    chalk "^2.0.1"
-    cli-boxes "^1.0.0"
-    string-width "^2.0.0"
-    term-size "^1.2.0"
-    widest-line "^2.0.0"
-
 boxen@^5.0.0:
   version "5.0.1"
-  resolved "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.0.1.tgz#657528bdd3f59a772b8279b831f27ec2c744664b"
   integrity sha512-49VBlw+PrWEF51aCmy7QIteYPIFZxSpvqBdP/2itCPPlJ49kj9zg/XPRFrdkne2W+CfwXUls8exMvu1RysZpKA==
   dependencies:
     ansi-align "^3.0.0"
@@ -2448,159 +415,27 @@
 
 brace-expansion@^1.0.0, brace-expansion@^1.1.7:
   version "1.1.11"
-  resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
   integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
   dependencies:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
-braces@^1.8.2:
-  version "1.8.5"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
-  integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=
-  dependencies:
-    expand-range "^1.8.1"
-    preserve "^0.2.0"
-    repeat-element "^1.1.2"
-
-braces@^2.3.1:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
-  integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
-  dependencies:
-    arr-flatten "^1.1.0"
-    array-unique "^0.3.2"
-    extend-shallow "^2.0.1"
-    fill-range "^4.0.0"
-    isobject "^3.0.1"
-    repeat-element "^1.1.2"
-    snapdragon "^0.8.1"
-    snapdragon-node "^2.0.1"
-    split-string "^3.0.2"
-    to-regex "^3.0.1"
-
 braces@^3.0.1:
   version "3.0.2"
-  resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
   integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
   dependencies:
     fill-range "^7.0.1"
 
-browser-capabilities@^1.0.0:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/browser-capabilities/-/browser-capabilities-1.1.4.tgz#a6bd657a07a134532ad66c722b8949904478b973"
-  integrity sha512-BezMQhbQklxjRQpZZQ8tnbzEo6AldUwMh8/PeWt5/CTBSwByQRXZEAK2fbnEahQ4poeeaI0suAYRq25A1YGOmw==
-  dependencies:
-    "@types/ua-parser-js" "^0.7.31"
-    ua-parser-js "^0.7.15"
-
-browserify-zlib@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
-  integrity sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=
-  dependencies:
-    pako "~0.2.0"
-
-browserslist@^4.14.5:
-  version "4.16.3"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
-  integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
-  dependencies:
-    caniuse-lite "^1.0.30001181"
-    colorette "^1.2.1"
-    electron-to-chromium "^1.3.649"
-    escalade "^3.1.1"
-    node-releases "^1.1.70"
-
-browserstack@^1.2.0:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3"
-  integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==
-  dependencies:
-    https-proxy-agent "^2.2.1"
-
-btoa-lite@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
-  integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc=
-
-buffer-alloc-unsafe@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
-  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
-
-buffer-alloc@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
-  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
-  dependencies:
-    buffer-alloc-unsafe "^1.1.0"
-    buffer-fill "^1.0.0"
-
-buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
-  version "0.2.13"
-  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
-  integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
-
-buffer-fill@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
-  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
-
 buffer-from@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz"
-  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
-
-buffer@^5.1.0, buffer@^5.5.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
-  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.1.13"
-
-busboy@^0.2.11:
-  version "0.2.14"
-  resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
-  integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
-  dependencies:
-    dicer "0.2.5"
-    readable-stream "1.1.x"
-
-bytes@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
-  integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
-
-bytes@3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
-  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
-
-cache-base@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
-  integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
-  dependencies:
-    collection-visit "^1.0.0"
-    component-emitter "^1.2.1"
-    get-value "^2.0.6"
-    has-value "^1.0.0"
-    isobject "^3.0.1"
-    set-value "^2.0.0"
-    to-object-path "^0.3.0"
-    union-value "^1.0.0"
-    unset-value "^1.0.0"
-
-cacheable-lookup@^5.0.3:
-  version "5.0.4"
-  resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005"
-  integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
 cacheable-request@^6.0.0:
   version "6.1.0"
-  resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
   integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
   dependencies:
     clone-response "^1.0.2"
@@ -2611,151 +446,60 @@
     normalize-url "^4.1.0"
     responselike "^1.0.2"
 
-cacheable-request@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58"
-  integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==
-  dependencies:
-    clone-response "^1.0.2"
-    get-stream "^5.1.0"
-    http-cache-semantics "^4.0.0"
-    keyv "^4.0.0"
-    lowercase-keys "^2.0.0"
-    normalize-url "^4.1.0"
-    responselike "^2.0.0"
-
 call-bind@^1.0.0, call-bind@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
   integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
   dependencies:
     function-bind "^1.1.1"
     get-intrinsic "^1.0.2"
 
-call-me-maybe@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
-  integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
-
 callsites@^3.0.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
-camel-case@3.0.x:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
-  integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
-  dependencies:
-    no-case "^2.2.0"
-    upper-case "^1.1.1"
-
-camelcase-keys@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
-  integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc=
-  dependencies:
-    camelcase "^2.0.0"
-    map-obj "^1.0.0"
-
 camelcase-keys@^6.2.2:
   version "6.2.2"
-  resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz"
+  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
   integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
   dependencies:
     camelcase "^5.3.1"
     map-obj "^4.0.0"
     quick-lru "^4.0.1"
 
-camelcase@^2.0.0, camelcase@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
-  integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
-
-camelcase@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
-  integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
-
 camelcase@^5.3.1:
   version "5.3.1"
-  resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
 camelcase@^6.2.0:
   version "6.2.0"
-  resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
   integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
 
-cancel-token@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/cancel-token/-/cancel-token-0.1.1.tgz#c18197674bb1c84c1d6933ebf15d8d5a5ce79b4f"
-  integrity sha1-wYGXZ0uxyEwdaTPr8V2NWlznm08=
-  dependencies:
-    "@types/node" "^4.0.30"
-
-caniuse-lite@^1.0.30001181:
-  version "1.0.30001208"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9"
-  integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==
-
-capture-stack-trace@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
-  integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
-
-caseless@~0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
-  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
-
-chalk@*, chalk@^4.0.0, chalk@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz"
-  integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
-chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
-  integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
-  dependencies:
-    ansi-styles "^2.2.1"
-    escape-string-regexp "^1.0.2"
-    has-ansi "^2.0.0"
-    strip-ansi "^3.0.0"
-    supports-color "^2.0.0"
-
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@^2.0.0:
   version "2.4.2"
-  resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
   dependencies:
     ansi-styles "^3.2.1"
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@~0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
-  integrity sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=
+chalk@^4.0.0, chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
   dependencies:
-    ansi-styles "~1.0.0"
-    has-color "~0.1.0"
-    strip-ansi "~0.1.0"
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
 
 chardet@^0.7.0:
   version "0.7.0"
-  resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz"
+  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
-charenc@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
-  integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
-
 cheerio@1.0.0-rc.2:
   version "1.0.0-rc.2"
   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
@@ -2768,380 +512,77 @@
     lodash "^4.15.0"
     parse5 "^3.0.1"
 
-chokidar@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
-  integrity sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=
-  dependencies:
-    anymatch "^1.3.0"
-    async-each "^1.0.0"
-    glob-parent "^2.0.0"
-    inherits "^2.0.1"
-    is-binary-path "^1.0.0"
-    is-glob "^2.0.0"
-    path-is-absolute "^1.0.0"
-    readdirp "^2.0.0"
-  optionalDependencies:
-    fsevents "^1.0.0"
-
-chownr@^1.0.1:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
-  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
-
-ci-info@^1.5.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
-  integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
-
 ci-info@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
   integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
 
-class-utils@^0.3.5:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
-  integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
-  dependencies:
-    arr-union "^3.1.0"
-    define-property "^0.2.5"
-    isobject "^3.0.0"
-    static-extend "^0.1.1"
-
-clean-css@4.2.x:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
-  integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
-  dependencies:
-    source-map "~0.6.0"
-
-cleankill@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/cleankill/-/cleankill-2.0.0.tgz#59830dfc8b411d53dc72ad09d45a78ea33161a91"
-  integrity sha1-WYMN/ItBHVPccq0J1Fp46jMWGpE=
-
-cli-boxes@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
-  integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
-
 cli-boxes@^2.2.1:
   version "2.2.1"
-  resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
   integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
 
-cli-cursor@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
-  integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=
-  dependencies:
-    restore-cursor "^1.0.1"
-
 cli-cursor@^3.1.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
   integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-table@^0.3.1:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc"
-  integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ==
-  dependencies:
-    colors "1.0.3"
-
-cli-width@^2.0.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
-  integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
-
 cli-width@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
   integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
 
-clone-buffer@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
-  integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
-
-clone-deep@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
-  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
-  dependencies:
-    is-plain-object "^2.0.4"
-    kind-of "^6.0.2"
-    shallow-clone "^3.0.0"
-
 clone-response@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
   integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
   dependencies:
     mimic-response "^1.0.0"
 
-clone-stats@^0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
-  integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=
-
-clone-stats@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
-  integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=
-
-clone@^1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
-  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
-
-clone@^2.0.0, clone@^2.1.0, clone@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
-  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
-
-cloneable-readable@^1.0.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec"
-  integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==
-  dependencies:
-    inherits "^2.0.1"
-    process-nextick-args "^2.0.0"
-    readable-stream "^2.3.5"
-
-code-point-at@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
-  integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
-
-collection-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
-  integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
-  dependencies:
-    map-visit "^1.0.0"
-    object-visit "^1.0.0"
-
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0:
   version "1.9.3"
-  resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
   dependencies:
     color-name "1.1.3"
 
 color-convert@^2.0.1:
   version "2.0.1"
-  resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
   integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
   dependencies:
     color-name "~1.1.4"
 
 color-name@1.1.3:
   version "1.1.3"
-  resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-color-name@^1.0.0, color-name@~1.1.4:
+color-name@~1.1.4:
   version "1.1.4"
-  resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-color-string@^1.5.2:
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
-  integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
-  dependencies:
-    color-name "^1.0.0"
-    simple-swizzle "^0.2.2"
-
-color@3.0.x:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
-  integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
-  dependencies:
-    color-convert "^1.9.1"
-    color-string "^1.5.2"
-
-colorette@^1.2.1:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
-  integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
-
-colors@1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
-  integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
-
-colors@^1.2.1:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
-  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-
-colorspace@1.1.x:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5"
-  integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
-  dependencies:
-    color "3.0.x"
-    text-hex "1.0.x"
-
-combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
-  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
-  dependencies:
-    delayed-stream "~1.0.0"
-
-command-line-args@^5.0.2:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.1.1.tgz#88e793e5bb3ceb30754a86863f0401ac92fd369a"
-  integrity sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==
-  dependencies:
-    array-back "^3.0.1"
-    find-replace "^3.0.0"
-    lodash.camelcase "^4.3.0"
-    typical "^4.0.0"
-
-command-line-commands@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/command-line-commands/-/command-line-commands-2.0.1.tgz#c58aa13dc78c06038ed67077e57ad09a6f858f46"
-  integrity sha512-m8c2p1DrNd2ruIAggxd/y6DgygQayf6r8RHwchhXryaLF8I6koYjoYroVP+emeROE9DXN5b9sP1Gh+WtvTTdtQ==
-  dependencies:
-    array-back "^2.0.0"
-
-command-line-usage@^5.0.5:
-  version "5.0.5"
-  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-5.0.5.tgz#5f25933ffe6dedd983c635d38a21d7e623fda357"
-  integrity sha512-d8NrGylA5oCXSbGoKz05FkehDAzSmIm4K03S5VDh4d5lZAtTWfc3D1RuETtuQCn8129nYfJfDdF7P/lwcz1BlA==
-  dependencies:
-    array-back "^2.0.0"
-    chalk "^2.4.1"
-    table-layout "^0.4.3"
-    typical "^2.6.1"
-
-commander@2.17.x:
-  version "2.17.1"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
-  integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
-
-commander@^2.20.0, commander@^2.20.3:
+commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@~2.19.0:
-  version "2.19.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
-  integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
-
-comment-parser@1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.2.tgz#e5317d7a2ec22b470dcb54a29b25426c30bf39d8"
-  integrity sha512-AOdq0i8ghZudnYv8RUnHrhTgafUGs61Rdz9jemU5x2lnZwAWyOq7vySo626K59e1fVKH1xSRorJwPVRLSWOoAQ==
-
-commondir@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
-  integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
-
-component-bind@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
-  integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
-
-component-emitter@1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
-  integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
-
-component-emitter@^1.2.1, component-emitter@~1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
-  integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
-
-component-inherit@0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
-  integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
-
-compress-commons@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610"
-  integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==
-  dependencies:
-    buffer-crc32 "^0.2.13"
-    crc32-stream "^3.0.1"
-    normalize-path "^3.0.0"
-    readable-stream "^2.3.6"
-
-compressible@~2.0.16:
-  version "2.0.18"
-  resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
-  integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
-  dependencies:
-    mime-db ">= 1.43.0 < 2"
-
-compression@^1.6.2:
-  version "1.7.4"
-  resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
-  integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
-  dependencies:
-    accepts "~1.3.5"
-    bytes "3.0.0"
-    compressible "~2.0.16"
-    debug "2.6.9"
-    on-headers "~1.0.2"
-    safe-buffer "5.1.2"
-    vary "~1.1.2"
+comment-parser@1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.5.tgz#453627ef8f67dbcec44e79a9bd5baa37f0bce9b2"
+  integrity sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA==
 
 concat-map@0.0.1:
   version "0.0.1"
-  resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
-concat-stream@^1.4.7, concat-stream@^1.5.2:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
-  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
-  dependencies:
-    buffer-from "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^2.2.2"
-    typedarray "^0.0.6"
-
-configstore@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-2.1.0.tgz#737a3a7036e9886102aa6099e47bb33ab1aba1a1"
-  integrity sha1-c3o6cDbpiGECqmCZ5HuzOrGroaE=
-  dependencies:
-    dot-prop "^3.0.0"
-    graceful-fs "^4.1.2"
-    mkdirp "^0.5.0"
-    object-assign "^4.0.1"
-    os-tmpdir "^1.0.0"
-    osenv "^0.1.0"
-    uuid "^2.0.1"
-    write-file-atomic "^1.1.2"
-    xdg-basedir "^2.0.0"
-
-configstore@^3.0.0:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.5.tgz#e9af331fadc14dabd544d3e7e76dc446a09a530f"
-  integrity sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==
-  dependencies:
-    dot-prop "^4.2.1"
-    graceful-fs "^4.1.2"
-    make-dir "^1.0.0"
-    unique-string "^1.0.0"
-    write-file-atomic "^2.0.0"
-    xdg-basedir "^3.0.0"
-
 configstore@^5.0.1:
   version "5.0.1"
-  resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
   integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==
   dependencies:
     dot-prop "^5.2.0"
@@ -3151,132 +592,18 @@
     write-file-atomic "^3.0.0"
     xdg-basedir "^4.0.0"
 
-contains-path@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz"
-  integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
-
-content-disposition@0.5.3:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
-  integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
-  dependencies:
-    safe-buffer "5.1.2"
-
-content-type@^1.0.2, content-type@~1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
-  integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-
-convert-source-map@^1.1.1, convert-source-map@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
-  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
-  dependencies:
-    safe-buffer "~5.1.1"
-
-cookie-signature@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
-  integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
-
-cookie@0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
-  integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
-
-cookie@~0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
-  integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
-
-copy-descriptor@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-  integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-
-core-js@^2.4.0:
-  version "2.6.12"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
-  integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
-
-core-util-is@1.0.2, core-util-is@~1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
-
-cors@^2.8.4:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
-  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
-crc32-stream@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85"
-  integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==
-  dependencies:
-    crc "^3.4.4"
-    readable-stream "^3.4.0"
-
-crc@^3.4.4:
-  version "3.8.0"
-  resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
-  integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
-  dependencies:
-    buffer "^5.1.0"
-
-create-error-class@^3.0.0, create-error-class@^3.0.1:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
-  integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=
-  dependencies:
-    capture-stack-trace "^1.0.0"
-
-cross-spawn@^5.0.1:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
-  integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
-  dependencies:
-    lru-cache "^4.0.1"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
-
-cross-spawn@^6.0.0, cross-spawn@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
-  integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
-  dependencies:
-    nice-try "^1.0.4"
-    path-key "^2.0.1"
-    semver "^5.5.0"
-    shebang-command "^1.2.0"
-    which "^1.2.9"
-
-cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
-  resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
   dependencies:
     path-key "^3.1.0"
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-crypt@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
-  integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
-
-crypto-random-string@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
-  integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
-
 crypto-random-string@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
   integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
 
 css-select@~1.2.0:
@@ -3289,287 +616,82 @@
     domutils "1.5.1"
     nth-check "~1.0.1"
 
-css-slam@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/css-slam/-/css-slam-2.1.2.tgz#3d35b1922cb3e0002a45c89ab189492508c493e5"
-  integrity sha512-cObrY+mhFEmepWpua6EpMrgRNTQ0eeym+kvR0lukI6hDEzK7F8himEDS4cJ9+fPHCoArTzVrrR0d+oAUbTR1NA==
-  dependencies:
-    command-line-args "^5.0.2"
-    command-line-usage "^5.0.5"
-    dom5 "^3.0.0"
-    parse5 "^4.0.0"
-    shady-css-parser "^0.1.0"
-
-css-what@2.1, css-what@^2.1.0:
+css-what@2.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
   integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
 
-cssbeautify@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/cssbeautify/-/cssbeautify-0.3.1.tgz#12dd1f734035c2e6faca67dcbdcef74e42811397"
-  integrity sha1-Et0fc0A1wub6ymfcvc73TkKBE5c=
-
-currently-unhandled@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
-  integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
-  dependencies:
-    array-find-index "^1.0.1"
-
-dargs@^6.0.0, dargs@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/dargs/-/dargs-6.1.0.tgz#1f3b9b56393ecf8caa7cbfd6c31496ffcfb9b272"
-  integrity sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==
-
-dashdash@^1.12.0:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
-  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
-  dependencies:
-    assert-plus "^1.0.0"
-
-dateformat@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
-  integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
-
-debug@2.6.9, debug@^2.0.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+debug@^2.6.9:
   version "2.6.9"
-  resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
   dependencies:
     ms "2.0.0"
 
-debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
-  version "4.3.1"
-  resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz"
-  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
-  dependencies:
-    ms "2.1.2"
-
-debug@^3.1.0:
+debug@^3.2.7:
   version "3.2.7"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
   integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
   dependencies:
     ms "^2.1.1"
 
-debug@~3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
-  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+debug@^4.0.1, debug@^4.1.1, debug@^4.3.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
   dependencies:
-    ms "2.0.0"
-
-debug@~4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
-  integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
-  dependencies:
-    ms "^2.1.1"
+    ms "2.1.2"
 
 decamelize-keys@^1.1.0:
   version "1.1.0"
-  resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
   integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
   dependencies:
     decamelize "^1.1.0"
     map-obj "^1.0.0"
 
-decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0:
+decamelize@^1.1.0, decamelize@^1.2.0:
   version "1.2.0"
-  resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
-decode-uri-component@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
-
-decompress-response@^3.2.0, decompress-response@^3.3.0:
+decompress-response@^3.3.0:
   version "3.3.0"
-  resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
   integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
   dependencies:
     mimic-response "^1.0.0"
 
-decompress-response@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
-  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
-  dependencies:
-    mimic-response "^3.1.0"
-
-deep-extend@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
-  integrity sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==
-
-deep-extend@^0.6.0, deep-extend@~0.6.0:
+deep-extend@^0.6.0:
   version "0.6.0"
-  resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
   integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
 
 deep-is@^0.1.3:
   version "0.1.3"
-  resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
 defer-to-connect@^1.0.1:
   version "1.1.3"
-  resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
   integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
 
-defer-to-connect@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
-  integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==
-
 define-properties@^1.1.3:
   version "1.1.3"
-  resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
   integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
   dependencies:
     object-keys "^1.0.12"
 
-define-property@^0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
-  integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
-  dependencies:
-    is-descriptor "^0.1.0"
-
-define-property@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
-  integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
-  dependencies:
-    is-descriptor "^1.0.0"
-
-define-property@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
-  integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
-  dependencies:
-    is-descriptor "^1.0.2"
-    isobject "^3.0.1"
-
-del@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
-  integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=
-  dependencies:
-    globby "^6.1.0"
-    is-path-cwd "^1.0.0"
-    is-path-in-cwd "^1.0.0"
-    p-map "^1.1.1"
-    pify "^3.0.0"
-    rimraf "^2.2.8"
-
-delayed-stream@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
-  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
-
-depd@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
-  integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
-
-deprecation@^2.0.0, deprecation@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
-  integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
-
-destroy@~1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
-  integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
-
-detect-conflict@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e"
-  integrity sha1-CIZXpmqWHAUBnbfEIwiDsca0F24=
-
-detect-file@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63"
-  integrity sha1-STXe39lIhkjgBrASlWbpOGcR6mM=
-  dependencies:
-    fs-exists-sync "^0.1.0"
-
-detect-file@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
-  integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
-
-detect-indent@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
-  integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg=
-  dependencies:
-    repeating "^2.0.0"
-
-detect-node@^2.0.3:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.5.tgz#9d270aa7eaa5af0b72c4c9d9b814e7f4ce738b79"
-  integrity sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==
-
-dicer@0.2.5:
-  version "0.2.5"
-  resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
-  integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
-  dependencies:
-    readable-stream "1.1.x"
-    streamsearch "0.1.2"
-
-diff@^2.1.2:
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99"
-  integrity sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k=
-
-diff@^3.1.0, diff@^3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
-  integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
-
-diff@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
-  integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
-
-dir-glob@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
-  integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==
-  dependencies:
-    arrify "^1.0.1"
-    path-type "^3.0.0"
-
-dir-glob@^2.2.2:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
-  integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
-  dependencies:
-    path-type "^3.0.0"
-
 dir-glob@^3.0.1:
   version "3.0.1"
-  resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
   integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
   dependencies:
     path-type "^4.0.0"
 
-doctrine@1.5.0:
-  version "1.5.0"
-  resolved "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz"
-  integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
-  dependencies:
-    esutils "^2.0.2"
-    isarray "^1.0.0"
-
-doctrine@^2.0.2:
+doctrine@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
   integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
@@ -3578,7 +700,7 @@
 
 doctrine@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
   integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
   dependencies:
     esutils "^2.0.2"
@@ -3592,12 +714,12 @@
     entities "^2.0.0"
 
 dom-serializer@^1.0.1:
-  version "1.3.1"
-  resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz"
-  integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
+  integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
   dependencies:
     domelementtype "^2.0.1"
-    domhandler "^4.0.0"
+    domhandler "^4.2.0"
     entities "^2.0.0"
 
 dom-serializer@~0.1.0:
@@ -3608,22 +730,6 @@
     domelementtype "^1.3.0"
     entities "^1.1.1"
 
-dom-urls@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/dom-urls/-/dom-urls-1.1.0.tgz#001ddf81628cd1e706125c7176f53ccec55d918e"
-  integrity sha1-AB3fgWKM0ecGElxxdvU8zsVdkY4=
-  dependencies:
-    urijs "^1.16.1"
-
-dom5@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/dom5/-/dom5-3.0.1.tgz#cdfc7331f376e284bf379e6ea054afc136702944"
-  integrity sha512-JPFiouQIr16VQ4dX6i0+Hpbg3H2bMKPmZ+WZgBOSSvOPx9QHwwY8sPzeM2baUtViESYto6wC2nuZOMC/6gulcA==
-  dependencies:
-    "@types/parse5" "^2.2.34"
-    clone "^2.1.0"
-    parse5 "^4.0.0"
-
 domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
@@ -3631,7 +737,7 @@
 
 domelementtype@^2.0.1, domelementtype@^2.2.0:
   version "2.2.0"
-  resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
   integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
 
 domhandler@^2.3.0:
@@ -3641,10 +747,10 @@
   dependencies:
     domelementtype "1"
 
-domhandler@^4.0.0, domhandler@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz"
-  integrity sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==
+domhandler@^4.0.0, domhandler@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
+  integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
   dependencies:
     domelementtype "^2.2.0"
 
@@ -3665,184 +771,46 @@
     domelementtype "1"
 
 domutils@^2.5.2:
-  version "2.5.2"
-  resolved "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz"
-  integrity sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ==
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
   dependencies:
     dom-serializer "^1.0.1"
     domelementtype "^2.2.0"
-    domhandler "^4.1.0"
-
-dot-prop@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
-  integrity sha1-G3CK8JSknJoOfbyteQq6U52sEXc=
-  dependencies:
-    is-obj "^1.0.0"
-
-dot-prop@^4.2.1:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4"
-  integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==
-  dependencies:
-    is-obj "^1.0.0"
+    domhandler "^4.2.0"
 
 dot-prop@^5.2.0:
   version "5.3.0"
-  resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
   integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
   dependencies:
     is-obj "^2.0.0"
 
-download-stats@^0.3.4:
-  version "0.3.4"
-  resolved "https://registry.yarnpkg.com/download-stats/-/download-stats-0.3.4.tgz#67ea0c32f14acd9f639da704eef509684ba2dae7"
-  integrity sha512-ic2BigbyUWx7/CBbsfGjf71zUNZB4edBGC3oRliSzsoNmvyVx3Ycfp1w3vp2Y78Ee0eIIkjIEO5KzW0zThDGaA==
-  dependencies:
-    JSONStream "^1.2.1"
-    lazy-cache "^2.0.1"
-    moment "^2.15.1"
-
-duplexer2@^0.1.2, duplexer2@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
-  integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
-  dependencies:
-    readable-stream "^2.0.2"
-
 duplexer3@^0.1.4:
   version "0.1.4"
-  resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz"
+  resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
   integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
 
-duplexify@^3.2.0, duplexify@^3.5.0, duplexify@^3.6.0:
-  version "3.7.1"
-  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
-  integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
-  dependencies:
-    end-of-stream "^1.0.0"
-    inherits "^2.0.1"
-    readable-stream "^2.0.0"
-    stream-shift "^1.0.0"
-
-ecc-jsbn@~0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
-  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
-  dependencies:
-    jsbn "~0.1.0"
-    safer-buffer "^2.1.0"
-
-editions@^2.2.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698"
-  integrity sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==
-  dependencies:
-    errlop "^2.0.0"
-    semver "^6.3.0"
-
-ee-first@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-  integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-
-ejs@^2.5.9, ejs@^2.6.1:
-  version "2.7.4"
-  resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
-  integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
-
-ejs@^3.1.5:
-  version "3.1.6"
-  resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a"
-  integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==
-  dependencies:
-    jake "^10.6.1"
-
-electron-to-chromium@^1.3.649:
-  version "1.3.712"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.712.tgz#ae467ffe5f95961c6d41ceefe858fc36eb53b38f"
-  integrity sha512-3kRVibBeCM4vsgoHHGKHmPocLqtFAGTrebXxxtgKs87hNUzXrX2NuS3jnBys7IozCnw7viQlozxKkmty2KNfrw==
-
-emitter-component@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
-  integrity sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=
-
 emoji-regex@^7.0.1:
   version "7.0.3"
-  resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
   integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
 
 emoji-regex@^8.0.0:
   version "8.0.0"
-  resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
-enabled@2.0.x:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
-  integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
-
-encodeurl@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-  integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-
-end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+end-of-stream@^1.1.0:
   version "1.4.4"
-  resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
   dependencies:
     once "^1.4.0"
 
-ends-with@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/ends-with/-/ends-with-0.2.0.tgz#2f9da98d57a50cfda4571ce4339000500f4e6b8a"
-  integrity sha1-L52pjVelDP2kVxzkM5AAUA9Oa4o=
-
-engine.io-client@~3.5.0:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.1.tgz#b500458a39c0cd197a921e0e759721a746d0bdb9"
-  integrity sha512-oVu9kBkGbcggulyVF0kz6BV3ganqUeqXvD79WOFKa+11oK692w1NyFkuEj4xrkFRpZhn92QOqTk4RQq5LiBXbQ==
-  dependencies:
-    component-emitter "~1.3.0"
-    component-inherit "0.0.3"
-    debug "~3.1.0"
-    engine.io-parser "~2.2.0"
-    has-cors "1.1.0"
-    indexof "0.0.1"
-    parseqs "0.0.6"
-    parseuri "0.0.6"
-    ws "~7.4.2"
-    xmlhttprequest-ssl "~1.5.4"
-    yeast "0.1.2"
-
-engine.io-parser@~2.2.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
-  integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
-  dependencies:
-    after "0.8.2"
-    arraybuffer.slice "~0.0.7"
-    base64-arraybuffer "0.1.4"
-    blob "0.0.5"
-    has-binary2 "~1.0.2"
-
-engine.io@~3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b"
-  integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==
-  dependencies:
-    accepts "~1.3.4"
-    base64id "2.0.0"
-    cookie "~0.4.1"
-    debug "~4.1.0"
-    engine.io-parser "~2.2.0"
-    ws "~7.4.2"
-
 enquirer@^2.3.5:
   version "2.3.6"
-  resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
   integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
   dependencies:
     ansi-colors "^4.1.1"
@@ -3854,32 +822,20 @@
 
 entities@^2.0.0:
   version "2.2.0"
-  resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
   integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
 
-errlop@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b"
-  integrity sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==
-
-error-ex@^1.2.0, error-ex@^1.3.1:
+error-ex@^1.3.1:
   version "1.3.2"
-  resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz"
+  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
   integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
   dependencies:
     is-arrayish "^0.2.1"
 
-error@^7.0.2:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894"
-  integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==
-  dependencies:
-    string-template "~0.2.1"
-
-es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2:
-  version "1.18.0"
-  resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz"
-  integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
+es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
+  version "1.18.5"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19"
+  integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
@@ -3887,92 +843,71 @@
     get-intrinsic "^1.1.1"
     has "^1.0.3"
     has-symbols "^1.0.2"
+    internal-slot "^1.0.3"
     is-callable "^1.2.3"
     is-negative-zero "^2.0.1"
-    is-regex "^1.1.2"
-    is-string "^1.0.5"
-    object-inspect "^1.9.0"
+    is-regex "^1.1.3"
+    is-string "^1.0.6"
+    object-inspect "^1.11.0"
     object-keys "^1.1.1"
     object.assign "^4.1.2"
     string.prototype.trimend "^1.0.4"
     string.prototype.trimstart "^1.0.4"
-    unbox-primitive "^1.0.0"
+    unbox-primitive "^1.0.1"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
-  resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
   integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
   dependencies:
     is-callable "^1.1.4"
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
-es6-promise@^4.0.3, es6-promise@^4.0.5:
-  version "4.2.8"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
-  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
-
-es6-promisify@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
-  integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
-  dependencies:
-    es6-promise "^4.0.3"
-
-es6-promisify@^6.0.0:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621"
-  integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg==
-
-escalade@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
-  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
 escape-goat@^2.0.0:
   version "2.1.1"
-  resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
   integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
 
-escape-html@^1.0.3, escape-html@~1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-  integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
-
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.5:
   version "1.0.5"
-  resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
 eslint-config-google@^0.14.0:
   version "0.14.0"
-  resolved "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a"
   integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==
 
 eslint-config-prettier@^7.0.0:
   version "7.2.0"
-  resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9"
   integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==
 
-eslint-import-resolver-node@^0.3.4:
-  version "0.3.4"
-  resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz"
-  integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
+eslint-import-resolver-node@^0.3.6:
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
+  integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==
   dependencies:
-    debug "^2.6.9"
-    resolve "^1.13.1"
+    debug "^3.2.7"
+    resolve "^1.20.0"
 
-eslint-module-utils@^2.6.0:
-  version "2.6.0"
-  resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz"
-  integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
+eslint-module-utils@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534"
+  integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==
   dependencies:
-    debug "^2.6.9"
+    debug "^3.2.7"
     pkg-dir "^2.0.0"
 
 eslint-plugin-es@^3.0.0:
   version "3.0.1"
-  resolved "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
   integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
   dependencies:
     eslint-utils "^2.0.0"
@@ -3986,40 +921,51 @@
     htmlparser2 "^6.0.1"
 
 eslint-plugin-import@^2.22.1:
-  version "2.22.1"
-  resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz"
-  integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==
+  version "2.24.2"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da"
+  integrity sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==
   dependencies:
-    array-includes "^3.1.1"
-    array.prototype.flat "^1.2.3"
-    contains-path "^0.1.0"
+    array-includes "^3.1.3"
+    array.prototype.flat "^1.2.4"
     debug "^2.6.9"
-    doctrine "1.5.0"
-    eslint-import-resolver-node "^0.3.4"
-    eslint-module-utils "^2.6.0"
+    doctrine "^2.1.0"
+    eslint-import-resolver-node "^0.3.6"
+    eslint-module-utils "^2.6.2"
+    find-up "^2.0.0"
     has "^1.0.3"
+    is-core-module "^2.6.0"
     minimatch "^3.0.4"
-    object.values "^1.1.1"
-    read-pkg-up "^2.0.0"
-    resolve "^1.17.0"
-    tsconfig-paths "^3.9.0"
+    object.values "^1.1.4"
+    pkg-up "^2.0.0"
+    read-pkg-up "^3.0.0"
+    resolve "^1.20.0"
+    tsconfig-paths "^3.11.0"
 
 eslint-plugin-jsdoc@^32.3.0:
-  version "32.3.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-32.3.0.tgz#7c9fa5da8c72bd6ad7d97bbf8dee8bc29bec3f9e"
-  integrity sha512-zyx7kajDK+tqS1bHuY5sapkad8P8KT0vdd/lE55j47VPG2MeenSYuIY/M/Pvmzq5g0+3JB+P3BJGUXmHxtuKPQ==
+  version "32.3.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-32.3.4.tgz#6888f3b2dbb9f73fb551458c639a4e8c84fe9ddc"
+  integrity sha512-xSWfsYvffXnN0OkwLnB7MoDDDDjqcp46W7YlY1j7JyfAQBQ+WnGCfLov3gVNZjUGtK9Otj8mEhTZTqJu4QtIGA==
   dependencies:
-    comment-parser "1.1.2"
+    comment-parser "1.1.5"
     debug "^4.3.1"
     jsdoctypeparser "^9.0.0"
-    lodash "^4.17.20"
+    lodash "^4.17.21"
     regextras "^0.7.1"
-    semver "^7.3.4"
+    semver "^7.3.5"
     spdx-expression-parse "^3.0.1"
 
+eslint-plugin-lit@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-lit/-/eslint-plugin-lit-1.5.1.tgz#e5b86fee4aeb6023ad4bb90b3d9e462ca8eff755"
+  integrity sha512-pYB0QM11uyOk5L55QfGhBmWi8a56PkNsnx+zVpY4bxz9YVquEo4BeRnFmf9AwFyT89rhGud9QruFhM2xJ4piwg==
+  dependencies:
+    parse5 "^6.0.1"
+    parse5-htmlparser2-tree-adapter "^6.0.1"
+    requireindex "^1.2.0"
+
 eslint-plugin-node@^11.1.0:
   version "11.1.0"
-  resolved "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
   integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
   dependencies:
     eslint-plugin-es "^3.0.0"
@@ -4029,23 +975,21 @@
     resolve "^1.10.1"
     semver "^6.1.0"
 
-eslint-plugin-prettier@^3.1.4:
-  version "3.3.1"
-  resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz"
-  integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==
+eslint-plugin-prettier@^3.1.4, eslint-plugin-prettier@^3.4.0:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5"
+  integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==
   dependencies:
     prettier-linter-helpers "^1.0.0"
 
-eslint-plugin-prettier@^3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz#cdbad3bf1dbd2b177e9825737fe63b476a08f0c7"
-  integrity sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==
-  dependencies:
-    prettier-linter-helpers "^1.0.0"
+eslint-plugin-regex@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-regex/-/eslint-plugin-regex-1.8.0.tgz#4bd111cf5235fb76a4a7f77d7ffcb7b3777b8a77"
+  integrity sha512-rmzVvpoxHKgvcYDo9d1X9RMFOtyOV3A6taD3KWE6gIID2dHoc8RPd0YAjDSJ0LG35wnehQBfsNB+F7q4eYqXqw==
 
-eslint-scope@^5.0.0, eslint-scope@^5.1.1:
+eslint-scope@^5.1.1:
   version "5.1.1"
-  resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
   integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
   dependencies:
     esrecurse "^4.3.0"
@@ -4053,7 +997,7 @@
 
 eslint-utils@^2.0.0, eslint-utils@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
   integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
   dependencies:
     eslint-visitor-keys "^1.1.0"
@@ -4067,36 +1011,39 @@
 
 eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
   version "1.3.0"
-  resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
   integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
 
 eslint-visitor-keys@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz"
-  integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
+  integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
 
-eslint@^7.10.0:
-  version "7.23.0"
-  resolved "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz"
-  integrity sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==
+eslint@^7.10.0, eslint@^7.24.0:
+  version "7.32.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
+  integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
   dependencies:
     "@babel/code-frame" "7.12.11"
-    "@eslint/eslintrc" "^0.4.0"
+    "@eslint/eslintrc" "^0.4.3"
+    "@humanwhocodes/config-array" "^0.5.0"
     ajv "^6.10.0"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.0.1"
     doctrine "^3.0.0"
     enquirer "^2.3.5"
+    escape-string-regexp "^4.0.0"
     eslint-scope "^5.1.1"
     eslint-utils "^2.1.0"
     eslint-visitor-keys "^2.0.0"
     espree "^7.3.1"
     esquery "^1.4.0"
     esutils "^2.0.2"
+    fast-deep-equal "^3.1.3"
     file-entry-cache "^6.0.1"
     functional-red-black-tree "^1.0.1"
-    glob-parent "^5.0.0"
+    glob-parent "^5.1.2"
     globals "^13.6.0"
     ignore "^4.0.6"
     import-fresh "^3.0.0"
@@ -4105,7 +1052,7 @@
     js-yaml "^3.13.1"
     json-stable-stringify-without-jsonify "^1.0.1"
     levn "^0.4.1"
-    lodash "^4.17.21"
+    lodash.merge "^4.6.2"
     minimatch "^3.0.4"
     natural-compare "^1.4.0"
     optionator "^0.9.1"
@@ -4114,64 +1061,13 @@
     semver "^7.2.1"
     strip-ansi "^6.0.0"
     strip-json-comments "^3.1.0"
-    table "^6.0.4"
+    table "^6.0.9"
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
-eslint@^7.24.0:
-  version "7.24.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.24.0.tgz#2e44fa62d93892bfdb100521f17345ba54b8513a"
-  integrity sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==
-  dependencies:
-    "@babel/code-frame" "7.12.11"
-    "@eslint/eslintrc" "^0.4.0"
-    ajv "^6.10.0"
-    chalk "^4.0.0"
-    cross-spawn "^7.0.2"
-    debug "^4.0.1"
-    doctrine "^3.0.0"
-    enquirer "^2.3.5"
-    eslint-scope "^5.1.1"
-    eslint-utils "^2.1.0"
-    eslint-visitor-keys "^2.0.0"
-    espree "^7.3.1"
-    esquery "^1.4.0"
-    esutils "^2.0.2"
-    file-entry-cache "^6.0.1"
-    functional-red-black-tree "^1.0.1"
-    glob-parent "^5.0.0"
-    globals "^13.6.0"
-    ignore "^4.0.6"
-    import-fresh "^3.0.0"
-    imurmurhash "^0.1.4"
-    is-glob "^4.0.0"
-    js-yaml "^3.13.1"
-    json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.4.1"
-    lodash "^4.17.21"
-    minimatch "^3.0.4"
-    natural-compare "^1.4.0"
-    optionator "^0.9.1"
-    progress "^2.0.0"
-    regexpp "^3.1.0"
-    semver "^7.2.1"
-    strip-ansi "^6.0.0"
-    strip-json-comments "^3.1.0"
-    table "^6.0.4"
-    text-table "^0.2.0"
-    v8-compile-cache "^2.0.3"
-
-espree@^3.5.2:
-  version "3.5.4"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
-  integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==
-  dependencies:
-    acorn "^5.5.0"
-    acorn-jsx "^3.0.0"
-
 espree@^7.3.0, espree@^7.3.1:
   version "7.3.1"
-  resolved "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
   integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
   dependencies:
     acorn "^7.4.0"
@@ -4180,93 +1076,42 @@
 
 esprima@^4.0.0:
   version "4.0.1"
-  resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
 esquery@^1.4.0:
   version "1.4.0"
-  resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
   integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
   dependencies:
     estraverse "^5.1.0"
 
 esrecurse@^4.3.0:
   version "4.3.0"
-  resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
   integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
   dependencies:
     estraverse "^5.2.0"
 
 estraverse@^4.1.1:
   version "4.3.0"
-  resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
   integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
 
 estraverse@^5.1.0, estraverse@^5.2.0:
   version "5.2.0"
-  resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
   integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
 
 esutils@^2.0.2:
   version "2.0.3"
-  resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-etag@~1.8.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
-  integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
-
-eventemitter3@^4.0.0:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
-  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
-execa@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
-  integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
-  dependencies:
-    cross-spawn "^5.0.1"
-    get-stream "^3.0.0"
-    is-stream "^1.1.0"
-    npm-run-path "^2.0.0"
-    p-finally "^1.0.0"
-    signal-exit "^3.0.0"
-    strip-eof "^1.0.0"
-
-execa@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
-  integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
-  dependencies:
-    cross-spawn "^6.0.0"
-    get-stream "^4.0.0"
-    is-stream "^1.1.0"
-    npm-run-path "^2.0.0"
-    p-finally "^1.0.0"
-    signal-exit "^3.0.0"
-    strip-eof "^1.0.0"
-
-execa@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
-  integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
-  dependencies:
-    cross-spawn "^7.0.0"
-    get-stream "^5.0.0"
-    human-signals "^1.1.1"
-    is-stream "^2.0.0"
-    merge-stream "^2.0.0"
-    npm-run-path "^4.0.0"
-    onetime "^5.1.0"
-    signal-exit "^3.0.2"
-    strip-final-newline "^2.0.0"
-
 execa@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz"
-  integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
   dependencies:
     cross-spawn "^7.0.3"
     get-stream "^6.0.0"
@@ -4278,688 +1123,161 @@
     signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
-exit-hook@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
-  integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
-
-expand-brackets@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
-  integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=
-  dependencies:
-    is-posix-bracket "^0.1.0"
-
-expand-brackets@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
-  integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
-  dependencies:
-    debug "^2.3.3"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    posix-character-classes "^0.1.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-expand-range@^1.8.1:
-  version "1.8.2"
-  resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
-  integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=
-  dependencies:
-    fill-range "^2.1.0"
-
-expand-tilde@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449"
-  integrity sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=
-  dependencies:
-    os-homedir "^1.0.1"
-
-expand-tilde@^2.0.0, expand-tilde@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
-  integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
-  dependencies:
-    homedir-polyfill "^1.0.1"
-
-express@^4.15.3, express@^4.8.5:
-  version "4.17.1"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
-  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
-  dependencies:
-    accepts "~1.3.7"
-    array-flatten "1.1.1"
-    body-parser "1.19.0"
-    content-disposition "0.5.3"
-    content-type "~1.0.4"
-    cookie "0.4.0"
-    cookie-signature "1.0.6"
-    debug "2.6.9"
-    depd "~1.1.2"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    finalhandler "~1.1.2"
-    fresh "0.5.2"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "~2.3.0"
-    parseurl "~1.3.3"
-    path-to-regexp "0.1.7"
-    proxy-addr "~2.0.5"
-    qs "6.7.0"
-    range-parser "~1.2.1"
-    safe-buffer "5.1.2"
-    send "0.17.1"
-    serve-static "1.14.1"
-    setprototypeof "1.1.1"
-    statuses "~1.5.0"
-    type-is "~1.6.18"
-    utils-merge "1.0.1"
-    vary "~1.1.2"
-
-ext-list@^2.0.0:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
-  integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==
-  dependencies:
-    mime-db "^1.28.0"
-
-extend-shallow@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
-  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
-  dependencies:
-    is-extendable "^0.1.0"
-
-extend-shallow@^3.0.0, extend-shallow@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
-  integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
-  dependencies:
-    assign-symbols "^1.0.0"
-    is-extendable "^1.0.1"
-
-extend@^3.0.0, extend@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
-  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
-
-external-editor@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b"
-  integrity sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=
-  dependencies:
-    extend "^3.0.0"
-    spawn-sync "^1.0.15"
-    tmp "^0.0.29"
-
 external-editor@^3.0.3:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
   integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
   dependencies:
     chardet "^0.7.0"
     iconv-lite "^0.4.24"
     tmp "^0.0.33"
 
-extglob@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
-  integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=
-  dependencies:
-    is-extglob "^1.0.0"
-
-extglob@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
-  integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
-  dependencies:
-    array-unique "^0.3.2"
-    define-property "^1.0.0"
-    expand-brackets "^2.1.4"
-    extend-shallow "^2.0.1"
-    fragment-cache "^0.2.1"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-extsprintf@1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
-  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
-
-extsprintf@^1.2.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
-  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
-
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
-  resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
 fast-diff@^1.1.2:
   version "1.2.0"
-  resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
   integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
 
-fast-glob@^2.0.2, fast-glob@^2.2.6:
-  version "2.2.7"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
-  integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
-  dependencies:
-    "@mrmlnc/readdir-enhanced" "^2.2.1"
-    "@nodelib/fs.stat" "^1.1.2"
-    glob-parent "^3.1.0"
-    is-glob "^4.0.0"
-    merge2 "^1.2.3"
-    micromatch "^3.1.10"
-
 fast-glob@^3.1.1:
-  version "3.2.5"
-  resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz"
-  integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
+  integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
   dependencies:
     "@nodelib/fs.stat" "^2.0.2"
     "@nodelib/fs.walk" "^1.2.3"
-    glob-parent "^5.1.0"
+    glob-parent "^5.1.2"
     merge2 "^1.3.0"
-    micromatch "^4.0.2"
-    picomatch "^2.2.1"
+    micromatch "^4.0.4"
 
 fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
 fast-levenshtein@^2.0.6:
   version "2.0.6"
-  resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
+  resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
-fast-safe-stringify@^2.0.4:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
-  integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
-
 fastq@^1.6.0:
-  version "1.11.0"
-  resolved "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz"
-  integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.12.0.tgz#ed7b6ab5d62393fb2cc591c853652a5c318bf794"
+  integrity sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==
   dependencies:
     reusify "^1.0.4"
 
-fd-slicer@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
-  integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
-  dependencies:
-    pend "~1.2.0"
-
-fecha@^2.3.3:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd"
-  integrity sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==
-
-fecha@^4.2.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce"
-  integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==
-
-figures@^1.3.5:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
-  integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=
-  dependencies:
-    escape-string-regexp "^1.0.5"
-    object-assign "^4.1.0"
-
 figures@^3.0.0:
   version "3.2.0"
-  resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
   integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
   dependencies:
     escape-string-regexp "^1.0.5"
 
 file-entry-cache@^6.0.1:
   version "6.0.1"
-  resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
   integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
   dependencies:
     flat-cache "^3.0.4"
 
-file-uri-to-path@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
-  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
-
-filelist@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
-  integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==
-  dependencies:
-    minimatch "^3.0.4"
-
-filename-regex@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
-  integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=
-
-fill-range@^2.1.0:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
-  integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==
-  dependencies:
-    is-number "^2.1.0"
-    isobject "^2.0.0"
-    randomatic "^3.0.0"
-    repeat-element "^1.1.2"
-    repeat-string "^1.5.2"
-
-fill-range@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
-  integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-    to-regex-range "^2.1.0"
-
 fill-range@^7.0.1:
   version "7.0.1"
-  resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
   integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
   dependencies:
     to-regex-range "^5.0.1"
 
-filled-array@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/filled-array/-/filled-array-1.1.0.tgz#c3c4f6c663b923459a9aa29912d2d031f1507f84"
-  integrity sha1-w8T2xmO5I0WamqKZEtLQMfFQf4Q=
-
-finalhandler@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
-  integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    parseurl "~1.3.3"
-    statuses "~1.5.0"
-    unpipe "~1.0.0"
-
-find-port@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/find-port/-/find-port-1.0.1.tgz#db084a6cbf99564d99869ae79fbdecf66e8a185c"
-  integrity sha1-2whKbL+ZVk2Zhprnn73s9m6KGFw=
-  dependencies:
-    async "~0.2.9"
-
-find-replace@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
-  integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==
-  dependencies:
-    array-back "^3.0.1"
-
-find-up@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
-  integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
-  dependencies:
-    path-exists "^2.0.0"
-    pinkie-promise "^2.0.0"
-
 find-up@^2.0.0, find-up@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
   integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
   dependencies:
     locate-path "^2.0.0"
 
-find-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
-  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
-  dependencies:
-    locate-path "^3.0.0"
-
 find-up@^4.1.0:
   version "4.1.0"
-  resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
   integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
   dependencies:
     locate-path "^5.0.0"
     path-exists "^4.0.0"
 
-findup-sync@^0.4.2:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12"
-  integrity sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=
-  dependencies:
-    detect-file "^0.1.0"
-    is-glob "^2.0.1"
-    micromatch "^2.3.7"
-    resolve-dir "^0.1.0"
-
-findup-sync@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
-  integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=
-  dependencies:
-    detect-file "^1.0.0"
-    is-glob "^3.1.0"
-    micromatch "^3.0.4"
-    resolve-dir "^1.0.1"
-
-first-chunk-stream@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e"
-  integrity sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=
-
-first-chunk-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
-  integrity sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=
-  dependencies:
-    readable-stream "^2.0.2"
-
 flat-cache@^3.0.4:
   version "3.0.4"
-  resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
   integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
   dependencies:
     flatted "^3.1.0"
     rimraf "^3.0.2"
 
 flatted@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz"
-  integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
-
-fn.name@1.x.x:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
-  integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
-
-follow-redirects@^1.0.0, follow-redirects@^1.10.0:
-  version "1.13.3"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
-  integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
-
-for-in@^1.0.1, for-in@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
-  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
-
-for-own@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
-  integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
-  dependencies:
-    for-in "^1.0.1"
-
-forever-agent@~0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
-  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
-
-fork-stream@^0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/fork-stream/-/fork-stream-0.0.4.tgz#db849fce77f6708a5f8f386ae533a0907b54ae70"
-  integrity sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=
-
-form-data@*:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
-  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
-  dependencies:
-    asynckit "^0.4.0"
-    combined-stream "^1.0.8"
-    mime-types "^2.1.12"
-
-form-data@~2.3.2:
-  version "2.3.3"
-  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
-  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
-  dependencies:
-    asynckit "^0.4.0"
-    combined-stream "^1.0.6"
-    mime-types "^2.1.12"
-
-formatio@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
-  integrity sha1-87IWfZBoxGmKjVH092CjmlTYGOs=
-  dependencies:
-    samsam "1.x"
-
-forwarded@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
-  integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
-
-fragment-cache@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
-  integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
-  dependencies:
-    map-cache "^0.2.2"
-
-freeport@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/freeport/-/freeport-1.0.5.tgz#255e8ab84170c33ba85d990e821ae5f4a1a9bc5d"
-  integrity sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=
-
-fresh@0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-  integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
-
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
-fs-exists-sync@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
-  integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
+  integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
 
 fs.realpath@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
-fsevents@^1.0.0:
-  version "1.2.13"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
-  integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
-  dependencies:
-    bindings "^1.5.0"
-    nan "^2.12.1"
-
-fsevents@~2.3.1:
+fsevents@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
 
 function-bind@^1.1.1:
   version "1.1.1"
-  resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
 functional-red-black-tree@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
-gensync@^1.0.0-beta.2:
-  version "1.0.0-beta.2"
-  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
-  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
-
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
   version "1.1.1"
-  resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
   integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
   dependencies:
     function-bind "^1.1.1"
     has "^1.0.3"
     has-symbols "^1.0.1"
 
-get-stdin@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
-  integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
-
-get-stream@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
-  integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
-
-get-stream@^4.0.0, get-stream@^4.1.0:
+get-stream@^4.1.0:
   version "4.1.0"
-  resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
   integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
   dependencies:
     pump "^3.0.0"
 
-get-stream@^5.0.0, get-stream@^5.1.0:
+get-stream@^5.1.0:
   version "5.2.0"
-  resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
   integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
   dependencies:
     pump "^3.0.0"
 
 get-stream@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz"
-  integrity sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+  integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
 
-get-value@^2.0.3, get-value@^2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
-  integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
-
-getpass@^0.1.1:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
-  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
-  dependencies:
-    assert-plus "^1.0.0"
-
-gh-got@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-5.0.0.tgz#ee95be37106fd8748a96f8d1db4baea89e1bfa8a"
-  integrity sha1-7pW+NxBv2HSKlvjR20uuqJ4b+oo=
-  dependencies:
-    got "^6.2.0"
-    is-plain-obj "^1.1.0"
-
-gh-got@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0"
-  integrity sha512-F/mS+fsWQMo1zfgG9MD8KWvTWPPzzhuVwY++fhQ5Ggd+0P+CAMHtzMZhNxG+TqGfHDChJKsbh6otfMGqO2AKBw==
-  dependencies:
-    got "^7.0.0"
-    is-plain-obj "^1.1.0"
-
-github-username@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/github-username/-/github-username-3.0.0.tgz#0a772219b3130743429f2456d0bdd3db55dce7b1"
-  integrity sha1-CnciGbMTB0NCnyRW0L3T21Xc57E=
-  dependencies:
-    gh-got "^5.0.0"
-
-github-username@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
-  integrity sha1-y+KABBiDIG2kISrp5LXxacML9Bc=
-  dependencies:
-    gh-got "^6.0.0"
-
-glob-base@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
-  integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=
-  dependencies:
-    glob-parent "^2.0.0"
-    is-glob "^2.0.0"
-
-glob-parent@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
-  integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=
-  dependencies:
-    is-glob "^2.0.0"
-
-glob-parent@^3.0.0, glob-parent@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
-  integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
-  dependencies:
-    is-glob "^3.1.0"
-    path-dirname "^1.0.0"
-
-glob-parent@^5.0.0, glob-parent@^5.1.0:
+glob-parent@^5.1.2:
   version "5.1.2"
-  resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
   dependencies:
     is-glob "^4.0.1"
 
-glob-stream@^5.3.2:
-  version "5.3.5"
-  resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22"
-  integrity sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=
-  dependencies:
-    extend "^3.0.0"
-    glob "^5.0.3"
-    glob-parent "^3.0.0"
-    micromatch "^2.3.7"
-    ordered-read-streams "^0.3.0"
-    through2 "^0.6.0"
-    to-absolute-glob "^0.1.1"
-    unique-stream "^2.0.2"
-
-glob-to-regexp@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
-  integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
-
-glob@^5.0.3:
-  version "5.0.15"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
-  integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
-  dependencies:
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "2 || 3"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^6.0.1:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
-  integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=
-  dependencies:
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "2 || 3"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
-  version "7.1.6"
-  resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
-  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+glob@^7.1.3:
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+  integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
   dependencies:
     fs.realpath "^1.0.0"
     inflight "^1.0.4"
@@ -4968,94 +1286,20 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-global-dirs@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
-  integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
-  dependencies:
-    ini "^1.3.4"
-
 global-dirs@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
   integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==
   dependencies:
     ini "2.0.0"
 
-global-modules@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d"
-  integrity sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=
-  dependencies:
-    global-prefix "^0.1.4"
-    is-windows "^0.2.0"
-
-global-modules@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
-  integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
-  dependencies:
-    global-prefix "^1.0.1"
-    is-windows "^1.0.1"
-    resolve-dir "^1.0.0"
-
-global-prefix@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f"
-  integrity sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=
-  dependencies:
-    homedir-polyfill "^1.0.0"
-    ini "^1.3.4"
-    is-windows "^0.2.0"
-    which "^1.2.12"
-
-global-prefix@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
-  integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
-  dependencies:
-    expand-tilde "^2.0.2"
-    homedir-polyfill "^1.0.1"
-    ini "^1.3.4"
-    is-windows "^1.0.1"
-    which "^1.2.14"
-
-globals@^11.1.0:
-  version "11.12.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
-  integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
-
-globals@^12.1.0:
-  version "12.4.0"
-  resolved "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz"
-  integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
-  dependencies:
-    type-fest "^0.8.1"
-
-globals@^13.6.0:
-  version "13.8.0"
-  resolved "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz"
-  integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==
+globals@^13.6.0, globals@^13.9.0:
+  version "13.11.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7"
+  integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==
   dependencies:
     type-fest "^0.20.2"
 
-globals@^9.18.0:
-  version "9.18.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
-  integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
-
-globby@^11.0.1:
-  version "11.0.3"
-  resolved "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz"
-  integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==
-  dependencies:
-    array-union "^2.1.0"
-    dir-glob "^3.0.1"
-    fast-glob "^3.1.1"
-    ignore "^5.1.4"
-    merge2 "^1.3.0"
-    slash "^3.0.0"
-
 globby@^11.0.3:
   version "11.0.4"
   resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
@@ -5068,134 +1312,9 @@
     merge2 "^1.3.0"
     slash "^3.0.0"
 
-globby@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-4.1.0.tgz#080f54549ec1b82a6c60e631fc82e1211dbe95f8"
-  integrity sha1-CA9UVJ7BuCpsYOYx/ILhIR2+lfg=
-  dependencies:
-    array-union "^1.0.1"
-    arrify "^1.0.0"
-    glob "^6.0.1"
-    object-assign "^4.0.1"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-
-globby@^6.1.0:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
-  integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
-  dependencies:
-    array-union "^1.0.1"
-    glob "^7.0.3"
-    object-assign "^4.0.1"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-
-globby@^8.0.1:
-  version "8.0.2"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d"
-  integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==
-  dependencies:
-    array-union "^1.0.1"
-    dir-glob "2.0.0"
-    fast-glob "^2.0.2"
-    glob "^7.1.2"
-    ignore "^3.3.5"
-    pify "^3.0.0"
-    slash "^1.0.0"
-
-globby@^9.2.0:
-  version "9.2.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
-  integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==
-  dependencies:
-    "@types/glob" "^7.1.1"
-    array-union "^1.0.2"
-    dir-glob "^2.2.2"
-    fast-glob "^2.2.6"
-    glob "^7.1.3"
-    ignore "^4.0.3"
-    pify "^4.0.1"
-    slash "^2.0.0"
-
-got@^11.8.0:
-  version "11.8.2"
-  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
-  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
-  dependencies:
-    "@sindresorhus/is" "^4.0.0"
-    "@szmarczak/http-timer" "^4.0.5"
-    "@types/cacheable-request" "^6.0.1"
-    "@types/responselike" "^1.0.0"
-    cacheable-lookup "^5.0.3"
-    cacheable-request "^7.0.1"
-    decompress-response "^6.0.0"
-    http2-wrapper "^1.0.0-beta.5.2"
-    lowercase-keys "^2.0.0"
-    p-cancelable "^2.0.0"
-    responselike "^2.0.0"
-
-got@^5.0.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
-  integrity sha1-X4FjWmHkplifGAVp6k44FoClHzU=
-  dependencies:
-    create-error-class "^3.0.1"
-    duplexer2 "^0.1.4"
-    is-redirect "^1.0.0"
-    is-retry-allowed "^1.0.0"
-    is-stream "^1.0.0"
-    lowercase-keys "^1.0.0"
-    node-status-codes "^1.0.0"
-    object-assign "^4.0.1"
-    parse-json "^2.1.0"
-    pinkie-promise "^2.0.0"
-    read-all-stream "^3.0.0"
-    readable-stream "^2.0.5"
-    timed-out "^3.0.0"
-    unzip-response "^1.0.2"
-    url-parse-lax "^1.0.0"
-
-got@^6.2.0, got@^6.7.1:
-  version "6.7.1"
-  resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
-  integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
-  dependencies:
-    create-error-class "^3.0.0"
-    duplexer3 "^0.1.4"
-    get-stream "^3.0.0"
-    is-redirect "^1.0.0"
-    is-retry-allowed "^1.0.0"
-    is-stream "^1.0.0"
-    lowercase-keys "^1.0.0"
-    safe-buffer "^5.0.1"
-    timed-out "^4.0.0"
-    unzip-response "^2.0.1"
-    url-parse-lax "^1.0.0"
-
-got@^7.0.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
-  integrity sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==
-  dependencies:
-    decompress-response "^3.2.0"
-    duplexer3 "^0.1.4"
-    get-stream "^3.0.0"
-    is-plain-obj "^1.1.0"
-    is-retry-allowed "^1.0.0"
-    is-stream "^1.0.0"
-    isurl "^1.0.0-alpha5"
-    lowercase-keys "^1.0.0"
-    p-cancelable "^0.3.0"
-    p-timeout "^1.1.1"
-    safe-buffer "^5.0.1"
-    timed-out "^4.0.0"
-    url-parse-lax "^1.0.0"
-    url-to-options "^1.0.1"
-
 got@^9.6.0:
   version "9.6.0"
-  resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz"
+  resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
   integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
   dependencies:
     "@sindresorhus/is" "^0.14.0"
@@ -5210,24 +1329,10 @@
     to-readable-stream "^1.0.0"
     url-parse-lax "^3.0.0"
 
-graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.2.0:
-  version "4.2.6"
-  resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz"
-  integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
-
-grouped-queue@^0.3.0:
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c"
-  integrity sha1-wWfSpTGcWg4JZO9qJbfC34mWyFw=
-  dependencies:
-    lodash "^4.17.2"
-
-grouped-queue@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-1.1.0.tgz#63e3f9ca90af952269d1d40879e41221eacc74cb"
-  integrity sha512-rZOFKfCqLhsu5VqjBjEWiwrYqJR07KxIkH4mLZlNlGDfntbb4FbMyGFP14TlvRPrU9S3Hnn/sgxbC5ZeN0no3Q==
-  dependencies:
-    lodash "^4.17.15"
+graceful-fs@^4.1.2:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 
 gts@^3.1.0:
   version "3.1.0"
@@ -5251,214 +1356,62 @@
     update-notifier "^5.0.0"
     write-file-atomic "^3.0.3"
 
-gulp-if@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/gulp-if/-/gulp-if-2.0.2.tgz#a497b7e7573005041caa2bc8b7dda3c80444d629"
-  integrity sha1-pJe351cwBQQcqivIt92jyARE1ik=
-  dependencies:
-    gulp-match "^1.0.3"
-    ternary-stream "^2.0.1"
-    through2 "^2.0.1"
-
-gulp-match@^1.0.3:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/gulp-match/-/gulp-match-1.1.0.tgz#552b7080fc006ee752c90563f9fec9d61aafdf4f"
-  integrity sha512-DlyVxa1Gj24DitY2OjEsS+X6tDpretuxD6wTfhXE/Rw2hweqc1f6D/XtsJmoiCwLWfXgR87W9ozEityPCVzGtQ==
-  dependencies:
-    minimatch "^3.0.3"
-
-gulp-sourcemaps@1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz#b86ff349d801ceb56e1d9e7dc7bbcb4b7dee600c"
-  integrity sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=
-  dependencies:
-    convert-source-map "^1.1.1"
-    graceful-fs "^4.1.2"
-    strip-bom "^2.0.0"
-    through2 "^2.0.0"
-    vinyl "^1.0.0"
-
-gunzip-maybe@^1.3.1:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"
-  integrity sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==
-  dependencies:
-    browserify-zlib "^0.1.4"
-    is-deflate "^1.0.0"
-    is-gzip "^1.0.0"
-    peek-stream "^1.1.0"
-    pumpify "^1.3.3"
-    through2 "^2.0.3"
-
-handle-thing@^1.2.5:
-  version "1.2.5"
-  resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
-  integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=
-
-har-schema@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
-  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
-
-har-validator@~5.1.0, har-validator@~5.1.3:
-  version "5.1.5"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
-  integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
-  dependencies:
-    ajv "^6.12.3"
-    har-schema "^2.0.0"
-
 hard-rejection@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
   integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
 
-has-ansi@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
-  integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
-  dependencies:
-    ansi-regex "^2.0.0"
-
 has-bigints@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
   integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
 
-has-binary2@~1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
-  integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
-  dependencies:
-    isarray "2.0.1"
-
-has-color@~0.1.0:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
-  integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=
-
-has-cors@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
-  integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
-
 has-flag@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
   integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
 
 has-flag@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbol-support-x@^1.4.1:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz#1409f98bc00247da45da67cee0a36f282ff26455"
-  integrity sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==
-
 has-symbols@^1.0.1, has-symbols@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
   integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
 
-has-to-string-tag-x@^1.2.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz#a045ab383d7b4b2012a00148ab0aa5f290044d4d"
-  integrity sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==
-  dependencies:
-    has-symbol-support-x "^1.4.1"
-
-has-value@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
-  integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
-  dependencies:
-    get-value "^2.0.3"
-    has-values "^0.1.4"
-    isobject "^2.0.0"
-
-has-value@^1.0.0:
+has-tostringtag@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
-  integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+  resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+  integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
   dependencies:
-    get-value "^2.0.6"
-    has-values "^1.0.0"
-    isobject "^3.0.0"
-
-has-values@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
-  integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
-
-has-values@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
-  integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
-  dependencies:
-    is-number "^3.0.0"
-    kind-of "^4.0.0"
+    has-symbols "^1.0.2"
 
 has-yarn@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77"
   integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==
 
 has@^1.0.3:
   version "1.0.3"
-  resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
   integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
   dependencies:
     function-bind "^1.1.1"
 
-he@1.2.x:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
-  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-
-homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
-  integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
-  dependencies:
-    parse-passwd "^1.0.0"
-
 hosted-git-info@^2.1.4:
   version "2.8.9"
-  resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
   integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 
 hosted-git-info@^4.0.1:
   version "4.0.2"
-  resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
   integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
   dependencies:
     lru-cache "^6.0.0"
 
-hpack.js@^2.1.6:
-  version "2.1.6"
-  resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
-  integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
-  dependencies:
-    inherits "^2.0.1"
-    obuf "^1.0.0"
-    readable-stream "^2.0.1"
-    wbuf "^1.1.0"
-
-html-minifier@^3.5.10:
-  version "3.5.21"
-  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c"
-  integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==
-  dependencies:
-    camel-case "3.0.x"
-    clean-css "4.2.x"
-    commander "2.17.x"
-    he "1.2.x"
-    param-case "2.1.x"
-    relateurl "0.2.x"
-    uglify-js "3.4.x"
-
 htmlparser2@^3.9.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
@@ -5473,7 +1426,7 @@
 
 htmlparser2@^6.0.1:
   version "6.1.0"
-  resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
   integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
   dependencies:
     domelementtype "^2.0.1"
@@ -5483,138 +1436,34 @@
 
 http-cache-semantics@^4.0.0:
   version "4.1.0"
-  resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
   integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
 
-http-deceiver@^1.2.7:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
-  integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
-
-http-errors@1.7.2:
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
-  integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.3"
-    setprototypeof "1.1.1"
-    statuses ">= 1.5.0 < 2"
-    toidentifier "1.0.0"
-
-http-errors@~1.6.2:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
-  integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.3"
-    setprototypeof "1.1.0"
-    statuses ">= 1.4.0 < 2"
-
-http-errors@~1.7.2:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
-  integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.4"
-    setprototypeof "1.1.1"
-    statuses ">= 1.5.0 < 2"
-    toidentifier "1.0.0"
-
-http-proxy-middleware@^0.17.2:
-  version "0.17.4"
-  resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
-  integrity sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=
-  dependencies:
-    http-proxy "^1.16.2"
-    is-glob "^3.1.0"
-    lodash "^4.17.2"
-    micromatch "^2.3.11"
-
-http-proxy@^1.16.2:
-  version "1.18.1"
-  resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
-  integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
-  dependencies:
-    eventemitter3 "^4.0.0"
-    follow-redirects "^1.0.0"
-    requires-port "^1.0.0"
-
-http-signature@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
-  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
-  dependencies:
-    assert-plus "^1.0.0"
-    jsprim "^1.2.2"
-    sshpk "^1.7.0"
-
-http2-wrapper@^1.0.0-beta.5.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
-  integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==
-  dependencies:
-    quick-lru "^5.1.1"
-    resolve-alpn "^1.0.0"
-
-https-proxy-agent@^2.2.1:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
-  integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
-  dependencies:
-    agent-base "^4.3.0"
-    debug "^3.1.0"
-
-https-proxy-agent@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
-  integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
-  dependencies:
-    agent-base "6"
-    debug "4"
-
-human-signals@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
-  integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
-
 human-signals@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
   integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24:
+iconv-lite@^0.4.24:
   version "0.4.24"
-  resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.13:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
-  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-ignore@^3.3.5:
-  version "3.3.10"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
-  integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
-
-ignore@^4.0.3, ignore@^4.0.6:
+ignore@^4.0.6:
   version "4.0.6"
-  resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
 ignore@^5.1.1, ignore@^5.1.4:
   version "5.1.8"
-  resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
   integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
 
 import-fresh@^3.0.0, import-fresh@^3.2.1:
   version "3.3.0"
-  resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
   dependencies:
     parent-module "^1.0.0"
@@ -5622,87 +1471,45 @@
 
 import-lazy@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
   integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
 
 imurmurhash@^0.1.4:
   version "0.1.4"
-  resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
+  resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
 
-indent-string@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
-  integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
-  dependencies:
-    repeating "^2.0.0"
-
 indent-string@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
   integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
 
-indent@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/indent/-/indent-0.0.2.tgz#8c79f080190559b687034b84c7aefa97d5a911d9"
-  integrity sha1-jHnwgBkFWbaHA0uEx676l9WpEdk=
-
-indexof@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
-  integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
-
 inflight@^1.0.4:
   version "1.0.6"
-  resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
   integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
   dependencies:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@^2.0.1, inherits@^2.0.3:
   version "2.0.4"
-  resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-inherits@2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
-  integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
-
 ini@2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
   integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
 
-ini@^1.3.4, ini@~1.3.0:
+ini@~1.3.0:
   version "1.3.8"
-  resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
   integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
 
-inquirer@^1.0.2:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918"
-  integrity sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=
-  dependencies:
-    ansi-escapes "^1.1.0"
-    chalk "^1.0.0"
-    cli-cursor "^1.0.1"
-    cli-width "^2.0.0"
-    external-editor "^1.1.0"
-    figures "^1.3.5"
-    lodash "^4.3.0"
-    mute-stream "0.0.6"
-    pinkie-promise "^2.0.0"
-    run-async "^2.2.0"
-    rx "^4.1.0"
-    string-width "^1.0.1"
-    strip-ansi "^3.0.0"
-    through "^2.3.6"
-
-inquirer@^7.1.0, inquirer@^7.3.3:
+inquirer@^7.3.3:
   version "7.3.3"
-  resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz"
+  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
   integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
   dependencies:
     ansi-escapes "^4.2.1"
@@ -5719,237 +1526,86 @@
     strip-ansi "^6.0.0"
     through "^2.3.6"
 
-interpret@^1.0.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
-  integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
-
-intersect@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/intersect/-/intersect-1.0.1.tgz#332650e10854d8c0ac58c192bdc27a8bf7e7a30c"
-  integrity sha1-MyZQ4QhU2MCsWMGSvcJ6i/fnoww=
-
-invariant@^2.2.2:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
-  integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+internal-slot@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+  integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
   dependencies:
-    loose-envify "^1.0.0"
-
-ipaddr.js@1.9.1:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
-  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
-
-is-accessor-descriptor@^0.1.6:
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
-  integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-accessor-descriptor@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
-  integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
-  dependencies:
-    kind-of "^6.0.0"
+    get-intrinsic "^1.1.0"
+    has "^1.0.3"
+    side-channel "^1.0.4"
 
 is-arrayish@^0.2.1:
   version "0.2.1"
-  resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
   integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
 
-is-arrayish@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
-  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
-
 is-bigint@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz"
-  integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==
-
-is-binary-path@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
-  integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
+  integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
   dependencies:
-    binary-extensions "^1.0.0"
+    has-bigints "^1.0.1"
 
 is-boolean-object@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz"
-  integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
+  integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
   dependencies:
-    call-bind "^1.0.0"
-
-is-buffer@^1.1.5, is-buffer@~1.1.6:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
-  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
 
 is-callable@^1.1.4, is-callable@^1.2.3:
-  version "1.2.3"
-  resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz"
-  integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
-
-is-ci@^1.0.10:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
-  integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
-  dependencies:
-    ci-info "^1.5.0"
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
+  integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
 
 is-ci@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
   integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
   dependencies:
     ci-info "^2.0.0"
 
-is-core-module@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz"
-  integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==
+is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
+  integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
   dependencies:
     has "^1.0.3"
 
-is-data-descriptor@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
-  integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-data-descriptor@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
-  integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
-  dependencies:
-    kind-of "^6.0.0"
-
 is-date-object@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz"
-  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
-
-is-deflate@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14"
-  integrity sha1-yGKQHDwWH7CdrHzcfnhPgOmPLxQ=
-
-is-descriptor@^0.1.0:
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
-  integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
+  integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
   dependencies:
-    is-accessor-descriptor "^0.1.6"
-    is-data-descriptor "^0.1.4"
-    kind-of "^5.0.0"
+    has-tostringtag "^1.0.0"
 
-is-descriptor@^1.0.0, is-descriptor@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
-  integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
-  dependencies:
-    is-accessor-descriptor "^1.0.0"
-    is-data-descriptor "^1.0.0"
-    kind-of "^6.0.2"
-
-is-dotfile@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
-  integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=
-
-is-equal-shallow@^0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
-  integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=
-  dependencies:
-    is-primitive "^2.0.0"
-
-is-extendable@^0.1.0, is-extendable@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
-  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
-
-is-extendable@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
-  integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
-  dependencies:
-    is-plain-object "^2.0.4"
-
-is-extglob@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
-  integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=
-
-is-extglob@^2.1.0, is-extglob@^2.1.1:
+is-extglob@^2.1.1:
   version "2.1.1"
-  resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
 
-is-finite@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
-  integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
-
-is-fullwidth-code-point@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
-  integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
-  dependencies:
-    number-is-nan "^1.0.0"
-
 is-fullwidth-code-point@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
   integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
 
 is-fullwidth-code-point@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
-is-glob@^2.0.0, is-glob@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
-  integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=
-  dependencies:
-    is-extglob "^1.0.0"
-
-is-glob@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
-  integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
-  dependencies:
-    is-extglob "^2.1.0"
-
 is-glob@^4.0.0, is-glob@^4.0.1:
   version "4.0.1"
-  resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
   integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
   dependencies:
     is-extglob "^2.1.1"
 
-is-gzip@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83"
-  integrity sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=
-
-is-installed-globally@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
-  integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
-  dependencies:
-    global-dirs "^0.1.0"
-    is-path-inside "^1.0.0"
-
 is-installed-globally@^0.4.0:
   version "0.4.0"
-  resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
   integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==
   dependencies:
     global-dirs "^3.0.0"
@@ -5957,325 +1613,106 @@
 
 is-negative-zero@^2.0.1:
   version "2.0.1"
-  resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
   integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
 
-is-npm@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
-  integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
-
 is-npm@^5.0.0:
   version "5.0.0"
-  resolved "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8"
   integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==
 
 is-number-object@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz"
-  integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
-
-is-number@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
-  integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
+  integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==
   dependencies:
-    kind-of "^3.0.2"
-
-is-number@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
-  integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
-  dependencies:
-    kind-of "^3.0.2"
-
-is-number@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
-  integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
+    has-tostringtag "^1.0.0"
 
 is-number@^7.0.0:
   version "7.0.0"
-  resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-obj@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
-  integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
-
 is-obj@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
   integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
 
-is-object@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf"
-  integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
-
-is-path-cwd@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
-  integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=
-
-is-path-in-cwd@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
-  integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==
-  dependencies:
-    is-path-inside "^1.0.0"
-
-is-path-inside@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
-  integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
-  dependencies:
-    path-is-inside "^1.0.1"
-
 is-path-inside@^3.0.2:
   version "3.0.3"
-  resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
   integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
 
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.1.0:
   version "1.1.0"
-  resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
 
-is-plain-object@^2.0.3, is-plain-object@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
-  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
-  dependencies:
-    isobject "^3.0.1"
-
-is-plain-object@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
-  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
-
-is-posix-bracket@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
-  integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=
-
-is-potential-custom-element-name@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
-  integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
-
-is-primitive@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
-  integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
-
-is-redirect@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
-  integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
-
-is-regex@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz"
-  integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
+is-regex@^1.1.3:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+  integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
   dependencies:
     call-bind "^1.0.2"
-    has-symbols "^1.0.1"
-
-is-retry-allowed@^1.0.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
-  integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
-
-is-scoped@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-scoped/-/is-scoped-1.0.0.tgz#449ca98299e713038256289ecb2b540dc437cb30"
-  integrity sha1-RJypgpnnEwOCViieyytUDcQ3yzA=
-  dependencies:
-    scoped-regex "^1.0.0"
-
-is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
-  integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+    has-tostringtag "^1.0.0"
 
 is-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz"
-  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
-is-string@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz"
-  integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+is-string@^1.0.5, is-string@^1.0.6:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+  integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+  dependencies:
+    has-tostringtag "^1.0.0"
 
 is-symbol@^1.0.2, is-symbol@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz"
-  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+  integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
   dependencies:
-    has-symbols "^1.0.1"
+    has-symbols "^1.0.2"
 
-is-typedarray@^1.0.0, is-typedarray@~1.0.0:
+is-typedarray@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
 
-is-utf8@^0.2.0, is-utf8@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
-  integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
-
-is-valid-glob@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-0.3.0.tgz#d4b55c69f51886f9b65c70d6c2622d37e29f48fe"
-  integrity sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=
-
-is-windows@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
-  integrity sha1-3hqm1j6indJIc3tp8f+LgALSEIw=
-
-is-windows@^1.0.1, is-windows@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
-  integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-
 is-yarn-global@^0.3.0:
   version "0.3.0"
-  resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
   integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
 
-isarray@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
-  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-
-isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
-  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-
-isarray@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
-  integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
-
-isbinaryfile@^3.0.2:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80"
-  integrity sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==
-  dependencies:
-    buffer-alloc "^1.2.0"
-
-isbinaryfile@^4.0.0:
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
-  integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg==
-
 isexe@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
   integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
 
-isobject@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
-  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
-  dependencies:
-    isarray "1.0.0"
-
-isobject@^3.0.0, isobject@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
-  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
-
-isstream@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
-  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
-
-istextorbinary@^2.2.1, istextorbinary@^2.5.1:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.6.0.tgz#60776315fb0fa3999add276c02c69557b9ca28ab"
-  integrity sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==
-  dependencies:
-    binaryextensions "^2.1.2"
-    editions "^2.2.0"
-    textextensions "^2.5.0"
-
-isurl@^1.0.0-alpha5:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
-  integrity sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==
-  dependencies:
-    has-to-string-tag-x "^1.2.0"
-    is-object "^1.0.1"
-
-jake@^10.6.1:
-  version "10.8.2"
-  resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
-  integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==
-  dependencies:
-    async "0.9.x"
-    chalk "^2.4.2"
-    filelist "^1.0.1"
-    minimatch "^3.0.4"
-
-"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+js-tokens@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
-js-tokens@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-  integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
-
 js-yaml@^3.13.1:
   version "3.14.1"
-  resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
   integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
   dependencies:
     argparse "^1.0.7"
     esprima "^4.0.0"
 
-jsbn@~0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
-  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
-
 jsdoctypeparser@^9.0.0:
   version "9.0.0"
-  resolved "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26"
   integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==
 
-jsesc@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
-  integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s=
-
-jsesc@^2.5.1:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
-  integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
-
-jsesc@~0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
-  integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
-
 json-buffer@3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
   integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
 
-json-buffer@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
-  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
-
 json-parse-better-errors@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@@ -6283,168 +1720,60 @@
 
 json-parse-even-better-errors@^2.3.0:
   version "2.3.1"
-  resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
   integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
 
 json-schema-traverse@^0.4.1:
   version "0.4.1"
-  resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
   integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
 
 json-schema-traverse@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
   integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
 
-json-schema@0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
-  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
-
 json-stable-stringify-without-jsonify@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
   integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
 
-json-stringify-safe@~5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
-  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
-
 json5@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
   integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.2, json5@^2.1.3:
+json5@^2.1.3:
   version "2.2.0"
-  resolved "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
   integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
   dependencies:
     minimist "^1.2.5"
 
-jsonparse@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
-  integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
-
-jsonschema@^1.1.0, jsonschema@^1.1.1:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2"
-  integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==
-
-jsprim@^1.2.2:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
-  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
-  dependencies:
-    assert-plus "1.0.0"
-    extsprintf "1.3.0"
-    json-schema "0.2.3"
-    verror "1.10.0"
-
 keyv@^3.0.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
   integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
   dependencies:
     json-buffer "3.0.0"
 
-keyv@^4.0.0:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
-  integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
-  dependencies:
-    json-buffer "3.0.1"
-
-kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
-  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
-  dependencies:
-    is-buffer "^1.1.5"
-
-kind-of@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
-  integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
-  dependencies:
-    is-buffer "^1.1.5"
-
-kind-of@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
-  integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
-
-kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
+kind-of@^6.0.3:
   version "6.0.3"
-  resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
-kuler@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
-  integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
-
-latest-version@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-2.0.0.tgz#56f8d6139620847b8017f8f1f4d78e211324168b"
-  integrity sha1-VvjWE5YghHuAF/jx9NeOIRMkFos=
-  dependencies:
-    package-json "^2.0.0"
-
-latest-version@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
-  integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
-  dependencies:
-    package-json "^4.0.0"
-
 latest-version@^5.1.0:
   version "5.1.0"
-  resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
   integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
   dependencies:
     package-json "^6.3.0"
 
-launchpad@^0.7.0:
-  version "0.7.5"
-  resolved "https://registry.yarnpkg.com/launchpad/-/launchpad-0.7.5.tgz#a16950c937572f10ef01c9be945a96f7aef8e427"
-  integrity sha512-gsYFgT8XKL3X2XZHPPPrgwM0JqeQwGpSWnzg7EYadBY3MirbQrTVq6L4fm6l7UE2T+7gnfuhiGkKr/xxuU/fdw==
-  dependencies:
-    async "^2.0.1"
-    browserstack "^1.2.0"
-    debug "^2.2.0"
-    mkdirp "^0.5.1"
-    plist "^2.0.1"
-    q "^1.4.1"
-    rimraf "^3.0.0"
-    underscore "^1.8.3"
-
-lazy-cache@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264"
-  integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=
-  dependencies:
-    set-getter "^0.1.0"
-
-lazy-req@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/lazy-req/-/lazy-req-1.1.0.tgz#bdaebead30f8d824039ce0ce149d4daa07ba1fac"
-  integrity sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=
-
-lazystream@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
-  integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
-  dependencies:
-    readable-stream "^2.0.5"
-
 levn@^0.4.1:
   version "0.4.1"
-  resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
   integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
   dependencies:
     prelude-ls "^1.2.1"
@@ -6452,30 +1781,9 @@
 
 lines-and-columns@^1.1.6:
   version "1.1.6"
-  resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
   integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
 
-load-json-file@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
-  integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^2.2.0"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-    strip-bom "^2.0.0"
-
-load-json-file@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz"
-  integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
-  dependencies:
-    graceful-fs "^4.1.2"
-    parse-json "^2.2.0"
-    pify "^2.0.0"
-    strip-bom "^3.0.0"
-
 load-json-file@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -6488,381 +1796,81 @@
 
 locate-path@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
   integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
   dependencies:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
-locate-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
-  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
-  dependencies:
-    p-locate "^3.0.0"
-    path-exists "^3.0.0"
-
 locate-path@^5.0.0:
   version "5.0.0"
-  resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
   integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
   dependencies:
     p-locate "^4.1.0"
 
-lodash._reinterpolate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
-  integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
-
-lodash.camelcase@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
-  integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
-
 lodash.clonedeep@^4.5.0:
   version "4.5.0"
-  resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
   integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
 
-lodash.defaults@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
-  integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
-
-lodash.difference@^4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
-  integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=
-
-lodash.flatten@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz"
-  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
-
-lodash.get@^4.4.2:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
-
-lodash.isequal@^4.0.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
-  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
-
-lodash.isplainobject@^4.0.6:
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
-  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
-
-lodash.mapvalues@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
-  integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=
-
 lodash.merge@^4.6.2:
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash.padend@^4.6.1:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
-  integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=
-
-lodash.set@^4.3.2:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
-  integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
-
-lodash.sortby@^4.7.0:
-  version "4.7.0"
-  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
-  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-
-lodash.template@^4.4.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
-  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
-  dependencies:
-    lodash._reinterpolate "^3.0.0"
-    lodash.templatesettings "^4.0.0"
-
-lodash.templatesettings@^4.0.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
-  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
-  dependencies:
-    lodash._reinterpolate "^3.0.0"
-
 lodash.truncate@^4.4.2:
   version "4.4.2"
-  resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz"
+  resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
   integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
 
-lodash.union@^4.6.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
-  integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
-
-lodash.uniq@^4.5.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-
-lodash@^3.0.0, lodash@^3.10.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-  integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
-
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0:
+lodash@4.17.21, lodash@^4.15.0, lodash@^4.17.19, lodash@^4.17.21:
   version "4.17.21"
-  resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-log-symbols@^1.0.0, log-symbols@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
-  integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=
-  dependencies:
-    chalk "^1.0.0"
-
-log-symbols@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
-  integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
-  dependencies:
-    chalk "^2.0.1"
-
-logform@^1.9.1:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/logform/-/logform-1.10.0.tgz#c9d5598714c92b546e23f4e78147c40f1e02012e"
-  integrity sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==
-  dependencies:
-    colors "^1.2.1"
-    fast-safe-stringify "^2.0.4"
-    fecha "^2.3.3"
-    ms "^2.1.1"
-    triple-beam "^1.2.0"
-
-logform@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
-  integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
-  dependencies:
-    colors "^1.2.1"
-    fast-safe-stringify "^2.0.4"
-    fecha "^4.2.0"
-    ms "^2.1.1"
-    triple-beam "^1.3.0"
-
-lolex@^1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
-  integrity sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=
-
 long@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
   integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
 
-loose-envify@^1.0.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
-  integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
-  dependencies:
-    js-tokens "^3.0.0 || ^4.0.0"
-
-loud-rejection@^1.0.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
-  integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
-  dependencies:
-    currently-unhandled "^0.4.1"
-    signal-exit "^3.0.0"
-
-lower-case@^1.1.1:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
-  integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
-
 lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
   integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
 
 lowercase-keys@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
   integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
 
-lru-cache@^4.0.1, lru-cache@^4.0.2:
-  version "4.1.5"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
-  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
-  dependencies:
-    pseudomap "^1.0.2"
-    yallist "^2.1.2"
-
 lru-cache@^6.0.0:
   version "6.0.0"
-  resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
   integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
   dependencies:
     yallist "^4.0.0"
 
-macos-release@^2.2.0:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac"
-  integrity sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==
-
-magic-string@^0.22.4:
-  version "0.22.5"
-  resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
-  integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==
-  dependencies:
-    vlq "^0.2.2"
-
-make-dir@^1.0.0, make-dir@^1.1.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
-  integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
-  dependencies:
-    pify "^3.0.0"
-
 make-dir@^3.0.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
   integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
   dependencies:
     semver "^6.0.0"
 
-map-cache@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
-  integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
-
-map-obj@^1.0.0, map-obj@^1.0.1:
+map-obj@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
   integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
 
 map-obj@^4.0.0:
   version "4.2.1"
-  resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.1.tgz#e4ea399dbc979ae735c83c863dd31bdf364277b7"
   integrity sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==
 
-map-visit@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
-  integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
-  dependencies:
-    object-visit "^1.0.0"
-
-matcher@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/matcher/-/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2"
-  integrity sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==
-  dependencies:
-    escape-string-regexp "^1.0.4"
-
-math-random@^1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
-  integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
-
-md5@^2.2.1:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
-  integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
-  dependencies:
-    charenc "0.0.2"
-    crypt "0.0.2"
-    is-buffer "~1.1.6"
-
-media-typer@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
-  integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
-
-mem-fs-editor@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-5.1.0.tgz#51972241640be8567680a04f7adaffe5fc603667"
-  integrity sha512-2Yt2GCYEbcotYbIJagmow4gEtHDqzpq5XN94+yAx/NT5+bGqIjkXnm3KCUQfE6kRfScGp9IZknScoGRKu8L78w==
-  dependencies:
-    commondir "^1.0.1"
-    deep-extend "^0.6.0"
-    ejs "^2.5.9"
-    glob "^7.0.3"
-    globby "^8.0.1"
-    isbinaryfile "^3.0.2"
-    mkdirp "^0.5.0"
-    multimatch "^2.0.0"
-    rimraf "^2.2.8"
-    through2 "^2.0.0"
-    vinyl "^2.0.1"
-
-mem-fs-editor@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-6.0.0.tgz#d63607cf0a52fe6963fc376c6a7aa52db3edabab"
-  integrity sha512-e0WfJAMm8Gv1mP5fEq/Blzy6Lt1VbLg7gNnZmZak7nhrBTibs+c6nQ4SKs/ZyJYHS1mFgDJeopsLAv7Ow0FMFg==
-  dependencies:
-    commondir "^1.0.1"
-    deep-extend "^0.6.0"
-    ejs "^2.6.1"
-    glob "^7.1.4"
-    globby "^9.2.0"
-    isbinaryfile "^4.0.0"
-    mkdirp "^0.5.0"
-    multimatch "^4.0.0"
-    rimraf "^2.6.3"
-    through2 "^3.0.1"
-    vinyl "^2.2.0"
-
-mem-fs-editor@^7.0.1:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-7.1.0.tgz#2a16f143228df87bf918874556723a7ee73bfe88"
-  integrity sha512-BH6QEqCXSqGeX48V7zu+e3cMwHU7x640NB8Zk8VNvVZniz+p4FK60pMx/3yfkzo6miI6G3a8pH6z7FeuIzqrzA==
-  dependencies:
-    commondir "^1.0.1"
-    deep-extend "^0.6.0"
-    ejs "^3.1.5"
-    glob "^7.1.4"
-    globby "^9.2.0"
-    isbinaryfile "^4.0.0"
-    mkdirp "^1.0.0"
-    multimatch "^4.0.0"
-    rimraf "^3.0.0"
-    through2 "^3.0.2"
-    vinyl "^2.2.1"
-
-mem-fs@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.2.0.tgz#5f29b2d02a5875cd14cd836c388385892d556cde"
-  integrity sha512-b8g0jWKdl8pM0LqAPdK9i8ERL7nYrzmJfRhxMiWH2uYdfYnb7uXnmwVb0ZGe7xyEl4lj+nLIU3yf4zPUT+XsVQ==
-  dependencies:
-    through2 "^3.0.0"
-    vinyl "^2.0.1"
-    vinyl-file "^3.0.0"
-
-meow@^3.7.0:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
-  integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
-  dependencies:
-    camelcase-keys "^2.0.0"
-    decamelize "^1.1.2"
-    loud-rejection "^1.0.0"
-    map-obj "^1.0.1"
-    minimist "^1.1.3"
-    normalize-package-data "^2.3.4"
-    object-assign "^4.0.1"
-    read-pkg-up "^1.0.1"
-    redent "^1.0.0"
-    trim-newlines "^1.0.0"
-
 meow@^9.0.0:
   version "9.0.0"
-  resolved "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
   integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
   dependencies:
     "@types/minimist" "^1.2.0"
@@ -6878,145 +1886,39 @@
     type-fest "^0.18.0"
     yargs-parser "^20.2.3"
 
-merge-descriptors@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
-  integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
-
-merge-stream@^1.0.0, merge-stream@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
-  integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=
-  dependencies:
-    readable-stream "^2.0.1"
-
 merge-stream@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
   integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
 
-merge2@^1.2.3, merge2@^1.3.0:
+merge2@^1.3.0:
   version "1.4.1"
-  resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
+  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
-methods@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-  integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-
-micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7:
-  version "2.3.11"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
-  integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=
-  dependencies:
-    arr-diff "^2.0.0"
-    array-unique "^0.2.1"
-    braces "^1.8.2"
-    expand-brackets "^0.1.4"
-    extglob "^0.3.1"
-    filename-regex "^2.0.0"
-    is-extglob "^1.0.0"
-    is-glob "^2.0.1"
-    kind-of "^3.0.2"
-    normalize-path "^2.0.1"
-    object.omit "^2.0.0"
-    parse-glob "^3.0.4"
-    regex-cache "^0.4.2"
-
-micromatch@^3.0.4, micromatch@^3.1.10:
-  version "3.1.10"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
-  integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
-  dependencies:
-    arr-diff "^4.0.0"
-    array-unique "^0.3.2"
-    braces "^2.3.1"
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    extglob "^2.0.4"
-    fragment-cache "^0.2.1"
-    kind-of "^6.0.2"
-    nanomatch "^1.2.9"
-    object.pick "^1.3.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.2"
-
-micromatch@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz"
-  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+micromatch@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
   dependencies:
     braces "^3.0.1"
-    picomatch "^2.0.5"
-
-mime-db@1.47.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0:
-  version "1.47.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
-  integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==
-
-mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
-  version "2.1.30"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d"
-  integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==
-  dependencies:
-    mime-db "1.47.0"
-
-mime@1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
-  integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
-
-mime@1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-
-mime@^2.3.1:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
-  integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
+    picomatch "^2.2.3"
 
 mimic-fn@^2.1.0:
   version "2.1.0"
-  resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
 mimic-response@^1.0.0, mimic-response@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
-mimic-response@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
-  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
-
 min-indent@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
-minimalistic-assert@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
-  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-
-minimatch-all@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/minimatch-all/-/minimatch-all-1.1.0.tgz#40c496a27a2e128d19bf758e76bb01a0c7145787"
-  integrity sha1-QMSWonouEo0Zv3WOdrsBoMcUV4c=
-  dependencies:
-    minimatch "^3.0.2"
-
-"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz"
-  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
-  dependencies:
-    brace-expansion "^1.1.7"
-
 minimatch@3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
@@ -7024,68 +1926,35 @@
   dependencies:
     brace-expansion "^1.0.0"
 
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
 minimist-options@4.1.0:
   version "4.1.0"
-  resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
   integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
   dependencies:
     arrify "^1.0.1"
     is-plain-obj "^1.1.0"
     kind-of "^6.0.3"
 
-minimist@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.1.tgz#827ba4e7593464e7c221e8c5bed930904ee2c455"
-  integrity sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg==
-
-minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.5"
-  resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-mixin-deep@^1.2.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
-  integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
-  dependencies:
-    for-in "^1.0.2"
-    is-extendable "^1.0.1"
-
-mkdirp@^0.5.0, mkdirp@^0.5.1:
-  version "0.5.5"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
-  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
-  dependencies:
-    minimist "^1.2.5"
-
-mkdirp@^1.0.0, mkdirp@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
-  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
-moment@^2.15.1, moment@^2.24.0:
-  version "2.29.1"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
-  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
-
-mout@^1.0.0:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/mout/-/mout-1.2.2.tgz#c9b718a499806a0632cede178e80f436259e777d"
-  integrity sha512-w0OUxFEla6z3d7sVpMZGBCpQvYh8PHS1wZ6Wu9GNKHMpAHWJ0if0LsQZh3DlOqw55HlhJEOMLpFnwtxp99Y5GA==
-
 ms@2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
   integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
 
-ms@2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
-  integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
-
 ms@2.1.2:
   version "2.1.2"
-  resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
 ms@^2.1.1:
@@ -7093,148 +1962,24 @@
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
 
-multer@^1.3.0:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a"
-  integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==
-  dependencies:
-    append-field "^1.0.0"
-    busboy "^0.2.11"
-    concat-stream "^1.5.2"
-    mkdirp "^0.5.1"
-    object-assign "^4.1.1"
-    on-finished "^2.3.0"
-    type-is "^1.6.4"
-    xtend "^4.0.0"
-
-multimatch@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b"
-  integrity sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=
-  dependencies:
-    array-differ "^1.0.0"
-    array-union "^1.0.1"
-    arrify "^1.0.0"
-    minimatch "^3.0.0"
-
-multimatch@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-4.0.0.tgz#8c3c0f6e3e8449ada0af3dd29efb491a375191b3"
-  integrity sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==
-  dependencies:
-    "@types/minimatch" "^3.0.3"
-    array-differ "^3.0.0"
-    array-union "^2.1.0"
-    arrify "^2.0.1"
-    minimatch "^3.0.4"
-
-multipipe@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-1.0.2.tgz#cc13efd833c9cda99f224f868461b8e1a3fd939d"
-  integrity sha1-zBPv2DPJzamfIk+GhGG44aP9k50=
-  dependencies:
-    duplexer2 "^0.1.2"
-    object-assign "^4.1.0"
-
-mute-stream@0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db"
-  integrity sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=
-
 mute-stream@0.0.8:
   version "0.0.8"
-  resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz"
+  resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
   integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
 
-mz@^2.4.0, mz@^2.6.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
-  integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
-  dependencies:
-    any-promise "^1.0.0"
-    object-assign "^4.0.1"
-    thenify-all "^1.0.0"
-
-nan@^2.12.1:
-  version "2.14.2"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
-  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
-
-nanomatch@^1.2.9:
-  version "1.2.13"
-  resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
-  integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
-  dependencies:
-    arr-diff "^4.0.0"
-    array-unique "^0.3.2"
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    fragment-cache "^0.2.1"
-    is-windows "^1.0.2"
-    kind-of "^6.0.2"
-    object.pick "^1.3.0"
-    regex-not "^1.0.0"
-    snapdragon "^0.8.1"
-    to-regex "^3.0.1"
-
-native-promise-only@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11"
-  integrity sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=
-
 natural-compare@^1.4.0:
   version "1.4.0"
-  resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
+  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
 ncp@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
   integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
 
-negotiator@0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
-  integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
-
-nice-try@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
-  integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-
-no-case@^2.2.0:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
-  integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
-  dependencies:
-    lower-case "^1.1.1"
-
-node-fetch@^2.6.0, node-fetch@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
-  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
-
-node-releases@^1.1.70:
-  version "1.1.71"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
-  integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
-
-node-status-codes@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
-  integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=
-
-nomnom@^1.8.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
-  integrity sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=
-  dependencies:
-    chalk "~0.4.0"
-    underscore "~1.6.0"
-
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
+normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
   version "2.5.0"
-  resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
   integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
   dependencies:
     hosted-git-info "^2.1.4"
@@ -7243,54 +1988,23 @@
     validate-npm-package-license "^3.0.1"
 
 normalize-package-data@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz"
-  integrity sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
+  integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
   dependencies:
     hosted-git-info "^4.0.1"
-    resolve "^1.20.0"
+    is-core-module "^2.5.0"
     semver "^7.3.4"
     validate-npm-package-license "^3.0.1"
 
-normalize-path@^2.0.0, normalize-path@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
-  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
-  dependencies:
-    remove-trailing-separator "^1.0.1"
-
-normalize-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
-  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
 normalize-url@^4.1.0:
-  version "4.5.0"
-  resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz"
-  integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
+  integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
 
-npm-api@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/npm-api/-/npm-api-1.0.1.tgz#3def9b51afedca57db14ca0c970d92442d21c9c5"
-  integrity sha512-4sITrrzEbPcr0aNV28QyOmgn6C9yKiF8k92jn4buYAK8wmA5xo1qL3II5/gT1r7wxbXBflSduZ2K3FbtOrtGkA==
-  dependencies:
-    JSONStream "^1.3.5"
-    clone-deep "^4.0.1"
-    download-stats "^0.3.4"
-    moment "^2.24.0"
-    node-fetch "^2.6.0"
-    paged-request "^2.0.1"
-
-npm-run-path@^2.0.0:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
-  integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
-  dependencies:
-    path-key "^2.0.0"
-
-npm-run-path@^4.0.0, npm-run-path@^4.0.1:
+npm-run-path@^4.0.1:
   version "4.0.1"
-  resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
   integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
   dependencies:
     path-key "^3.0.0"
@@ -7302,50 +2016,19 @@
   dependencies:
     boolbase "~1.0.0"
 
-number-is-nan@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
-  integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
-
-oauth-sign@~0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
-  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-
-object-assign@^4, object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
-  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-
-object-copy@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
-  integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
-  dependencies:
-    copy-descriptor "^0.1.0"
-    define-property "^0.2.5"
-    kind-of "^3.0.3"
-
-object-inspect@^1.9.0:
-  version "1.9.0"
-  resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz"
-  integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
+object-inspect@^1.11.0, object-inspect@^1.9.0:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
+  integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
 
 object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
-  resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object-visit@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
-  integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
-  dependencies:
-    isobject "^3.0.0"
-
-object.assign@^4.1.0, object.assign@^4.1.2:
+object.assign@^4.1.2:
   version "4.1.2"
-  resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
   integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
   dependencies:
     call-bind "^1.0.0"
@@ -7353,89 +2036,32 @@
     has-symbols "^1.0.1"
     object-keys "^1.1.1"
 
-object.omit@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
-  integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=
-  dependencies:
-    for-own "^0.1.4"
-    is-extendable "^0.1.1"
-
-object.pick@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
-  integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
-  dependencies:
-    isobject "^3.0.1"
-
-object.values@^1.1.1:
-  version "1.1.3"
-  resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.3.tgz"
-  integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw==
+object.values@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
+  integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
   dependencies:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.2"
-    has "^1.0.3"
-
-obuf@^1.0.0, obuf@^1.1.1:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
-  integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
-
-octokit-pagination-methods@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
-  integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
-
-on-finished@^2.3.0, on-finished@~2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
-  integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
-  dependencies:
-    ee-first "1.1.1"
-
-on-headers@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
-  integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+    es-abstract "^1.18.2"
 
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
-  resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
   dependencies:
     wrappy "1"
 
-one-time@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
-  integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
-  dependencies:
-    fn.name "1.x.x"
-
-onetime@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
-  integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
-
 onetime@^5.1.0, onetime@^5.1.2:
   version "5.1.2"
-  resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
   integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
   dependencies:
     mimic-fn "^2.1.0"
 
-opn@^3.0.2:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a"
-  integrity sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=
-  dependencies:
-    object-assign "^4.0.1"
-
 optionator@^0.9.1:
   version "0.9.1"
-  resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
   integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
   dependencies:
     deep-is "^0.1.3"
@@ -7445,145 +2071,57 @@
     type-check "^0.4.0"
     word-wrap "^1.2.3"
 
-ordered-read-streams@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b"
-  integrity sha1-cTfmmzKYuzQiR6G77jiByA4v14s=
-  dependencies:
-    is-stream "^1.0.1"
-    readable-stream "^2.0.1"
-
-os-homedir@^1.0.0, os-homedir@^1.0.1:
+os-tmpdir@~1.0.2:
   version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-
-os-name@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
-  integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==
-  dependencies:
-    macos-release "^2.2.0"
-    windows-release "^3.1.0"
-
-os-shim@^0.1.2:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
-  integrity sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=
-
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
 
-osenv@^0.1.0, osenv@^0.1.3:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
-  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
-p-cancelable@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
-  integrity sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==
-
 p-cancelable@^1.0.0:
   version "1.1.0"
-  resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
   integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
 
-p-cancelable@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.0.tgz#4d51c3b91f483d02a0d300765321fca393d758dd"
-  integrity sha512-HAZyB3ZodPo+BDpb4/Iu7Jv4P6cSazBz9ZM0ChhEXp70scx834aWCEjQRwgt41UzzejUAPdbqqONfRWTPYrPAQ==
-
-p-finally@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
-  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
-
 p-limit@^1.1.0:
   version "1.3.0"
-  resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
   integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
   dependencies:
     p-try "^1.0.0"
 
-p-limit@^2.0.0, p-limit@^2.2.0:
+p-limit@^2.2.0:
   version "2.3.0"
-  resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
   integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
   dependencies:
     p-try "^2.0.0"
 
 p-locate@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
   integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
   dependencies:
     p-limit "^1.1.0"
 
-p-locate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
-  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
-  dependencies:
-    p-limit "^2.0.0"
-
 p-locate@^4.1.0:
   version "4.1.0"
-  resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
   integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
   dependencies:
     p-limit "^2.2.0"
 
-p-map@^1.1.1:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
-  integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==
-
-p-timeout@^1.1.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386"
-  integrity sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=
-  dependencies:
-    p-finally "^1.0.0"
-
 p-try@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
   integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
 
-p-try@^2.0.0, p-try@^2.1.0:
+p-try@^2.0.0:
   version "2.2.0"
-  resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
-package-json@^2.0.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb"
-  integrity sha1-DRW9Z9HLvduyyiIv8u24a8sxqLs=
-  dependencies:
-    got "^5.0.0"
-    registry-auth-token "^3.0.1"
-    registry-url "^3.0.3"
-    semver "^5.1.0"
-
-package-json@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
-  integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=
-  dependencies:
-    got "^6.7.1"
-    registry-auth-token "^3.0.1"
-    registry-url "^3.0.3"
-    semver "^5.1.0"
-
 package-json@^6.3.0:
   version "6.5.0"
-  resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz"
+  resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
   integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
   dependencies:
     got "^9.6.0"
@@ -7591,49 +2129,13 @@
     registry-url "^5.0.0"
     semver "^6.2.0"
 
-paged-request@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/paged-request/-/paged-request-2.0.2.tgz#4d621a08b8d6bee4440a0a92112354eeece5b5b0"
-  integrity sha512-NWrGqneZImDdcMU/7vMcAOo1bIi5h/pmpJqe7/jdsy85BA/s5MSaU/KlpxwW/IVPmIwBcq2uKPrBWWhEWhtxag==
-  dependencies:
-    axios "^0.21.1"
-
-pako@~0.2.0:
-  version "0.2.9"
-  resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
-  integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=
-
-param-case@2.1.x:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
-  integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
-  dependencies:
-    no-case "^2.2.0"
-
 parent-module@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
   integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
   dependencies:
     callsites "^3.0.0"
 
-parse-glob@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
-  integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw=
-  dependencies:
-    glob-base "^0.3.0"
-    is-dotfile "^1.0.0"
-    is-extglob "^1.0.0"
-    is-glob "^2.0.0"
-
-parse-json@^2.1.0, parse-json@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz"
-  integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
-  dependencies:
-    error-ex "^1.2.0"
-
 parse-json@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -7644,7 +2146,7 @@
 
 parse-json@^5.0.0:
   version "5.2.0"
-  resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
   integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
   dependencies:
     "@babel/code-frame" "^7.0.0"
@@ -7652,10 +2154,12 @@
     json-parse-even-better-errors "^2.3.0"
     lines-and-columns "^1.1.6"
 
-parse-passwd@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
-  integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+parse5-htmlparser2-tree-adapter@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+  integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
+  dependencies:
+    parse5 "^6.0.1"
 
 parse5@^3.0.1:
   version "3.0.3"
@@ -7664,105 +2168,35 @@
   dependencies:
     "@types/node" "*"
 
-parse5@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
-  integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
-
-parseqs@0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
-  integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
-
-parseuri@0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
-  integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
-
-parseurl@~1.3.3:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
-  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
-pascalcase@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-  integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
-
-path-dirname@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
-  integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
-
-path-exists@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
-  integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
-  dependencies:
-    pinkie-promise "^2.0.0"
+parse5@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+  integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
 path-exists@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
   integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
 
 path-exists@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
   integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
 
 path-is-absolute@^1.0.0:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
-path-is-inside@^1.0.1, path-is-inside@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
-  integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
-
-path-key@^2.0.0, path-key@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
-  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
-
 path-key@^3.0.0, path-key@^3.1.0:
   version "3.1.1"
-  resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
   integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
 
 path-parse@^1.0.6:
-  version "1.0.6"
-  resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz"
-  integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
-
-path-to-regexp@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
-  integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
-
-path-to-regexp@^1.0.1, path-to-regexp@^1.7.0:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
-  integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
-  dependencies:
-    isarray "0.0.1"
-
-path-type@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
-  integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
-  dependencies:
-    graceful-fs "^4.1.2"
-    pify "^2.0.0"
-    pinkie-promise "^2.0.0"
-
-path-type@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz"
-  integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
-  dependencies:
-    pify "^2.0.0"
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
 path-type@^3.0.0:
   version "3.0.0"
@@ -7773,389 +2207,46 @@
 
 path-type@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
-peek-stream@^1.1.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67"
-  integrity sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==
-  dependencies:
-    buffer-from "^1.0.0"
-    duplexify "^3.5.0"
-    through2 "^2.0.3"
-
-pem@^1.8.3:
-  version "1.14.4"
-  resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.4.tgz#a68c70c6e751ccc5b3b5bcd7af78b0aec1177ff9"
-  integrity sha512-v8lH3NpirgiEmbOqhx0vwQTxwi0ExsiWBGYh0jYNq7K6mQuO4gI6UEFlr6fLAdv9TPXRt6GqiwE37puQdIDS8g==
-  dependencies:
-    es6-promisify "^6.0.0"
-    md5 "^2.2.1"
-    os-tmpdir "^1.0.1"
-    which "^2.0.2"
-
-pend@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
-  integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
-
-performance-now@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
-  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
-
-picomatch@^2.0.5, picomatch@^2.2.1:
-  version "2.2.2"
-  resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz"
-  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
-
-pify@^2.0.0, pify@^2.3.0:
+picomatch@^2.2.3:
   version "2.3.0"
-  resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
-  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
+  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
 
 pify@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
   integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
 
-pify@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
-  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
-
-pinkie-promise@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
-  integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
-  dependencies:
-    pinkie "^2.0.0"
-
-pinkie@^2.0.0:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
-  integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
-
 pkg-dir@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
   integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
   dependencies:
     find-up "^2.1.0"
 
-plist@^2.0.1:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
-  integrity sha1-V8zbeggh3yGDEhejytVOPhRqECU=
+pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
+  integrity sha1-yBmscoBZpGHKscOImivjxJoATX8=
   dependencies:
-    base64-js "1.2.0"
-    xmlbuilder "8.2.2"
-    xmldom "0.1.x"
-
-plylog@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/plylog/-/plylog-1.1.0.tgz#f6f354e2ae0b01f6db4ed111f4b3855da9c37417"
-  integrity sha512-/QnY5aSVaP54va6hruzNtAj02HpsLlAt7V5EndMrtq6ZUTZJKUja43rgiUtGXqm95yrSJjbZoPW0yQQQwLpoJA==
-  dependencies:
-    logform "^1.9.1"
-    winston "^3.0.0"
-    winston-transport "^4.2.0"
-
-polymer-analyzer@^3.0.0, polymer-analyzer@^3.1.3, polymer-analyzer@^3.2.2:
-  version "3.2.4"
-  resolved "https://registry.yarnpkg.com/polymer-analyzer/-/polymer-analyzer-3.2.4.tgz#7d76356620a2328e8bc9e30e47069f9729260ca1"
-  integrity sha512-JmxUhMajTuC18tLXbTtu2+aN2x9bTX+4MvCD4IZKJ0rtAL8jWi1iRLfogpHJB4Ig9Dc8EEEuEYipLuzPFl3vqA==
-  dependencies:
-    "@babel/generator" "^7.0.0-beta.42"
-    "@babel/traverse" "^7.0.0-beta.42"
-    "@babel/types" "^7.0.0-beta.42"
-    "@types/babel-generator" "^6.25.1"
-    "@types/babel-traverse" "^6.25.2"
-    "@types/babel-types" "^6.25.1"
-    "@types/babylon" "^6.16.2"
-    "@types/chai-subset" "^1.3.0"
-    "@types/chalk" "^0.4.30"
-    "@types/clone" "^0.1.30"
-    "@types/cssbeautify" "^0.3.1"
-    "@types/doctrine" "^0.0.1"
-    "@types/is-windows" "^0.2.0"
-    "@types/minimatch" "^3.0.1"
-    "@types/parse5" "^2.2.34"
-    "@types/path-is-inside" "^1.0.0"
-    "@types/resolve" "0.0.6"
-    "@types/whatwg-url" "^6.4.0"
-    babylon "^7.0.0-beta.42"
-    cancel-token "^0.1.1"
-    chalk "^1.1.3"
-    clone "^2.0.0"
-    cssbeautify "^0.3.1"
-    doctrine "^2.0.2"
-    dom5 "^3.0.0"
-    indent "0.0.2"
-    is-windows "^1.0.2"
-    jsonschema "^1.1.0"
-    minimatch "^3.0.4"
-    parse5 "^4.0.0"
-    path-is-inside "^1.0.2"
-    resolve "^1.5.0"
-    shady-css-parser "^0.1.0"
-    stable "^0.1.6"
-    strip-indent "^2.0.0"
-    vscode-uri "=1.0.6"
-    whatwg-url "^6.4.0"
-
-polymer-build@^3.1.0, polymer-build@^3.1.4:
-  version "3.1.4"
-  resolved "https://registry.yarnpkg.com/polymer-build/-/polymer-build-3.1.4.tgz#ab539f1a13d803518b13b73ffd09198431d98142"
-  integrity sha512-OhTOPG5Y/tK2HqGZ5XA/CVDh+TuOaDv7wTZWXDCg6hxeMgNKuljDMn2coyGU5NLM0pLbS+gwFAc2ZJ5cWHCHNg==
-  dependencies:
-    "@babel/core" "^7.0.0"
-    "@babel/plugin-external-helpers" "^7.0.0"
-    "@babel/plugin-proposal-async-generator-functions" "^7.0.0"
-    "@babel/plugin-proposal-object-rest-spread" "^7.0.0"
-    "@babel/plugin-syntax-async-generators" "^7.0.0"
-    "@babel/plugin-syntax-dynamic-import" "^7.0.0"
-    "@babel/plugin-syntax-import-meta" "^7.0.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.0.0"
-    "@babel/plugin-transform-arrow-functions" "^7.0.0"
-    "@babel/plugin-transform-async-to-generator" "^7.0.0"
-    "@babel/plugin-transform-block-scoped-functions" "^7.0.0"
-    "@babel/plugin-transform-block-scoping" "^7.0.0"
-    "@babel/plugin-transform-classes" "^7.0.0"
-    "@babel/plugin-transform-computed-properties" "^7.0.0"
-    "@babel/plugin-transform-destructuring" "^7.0.0"
-    "@babel/plugin-transform-duplicate-keys" "^7.0.0"
-    "@babel/plugin-transform-exponentiation-operator" "^7.0.0"
-    "@babel/plugin-transform-for-of" "^7.0.0"
-    "@babel/plugin-transform-function-name" "^7.0.0"
-    "@babel/plugin-transform-instanceof" "^7.0.0"
-    "@babel/plugin-transform-literals" "^7.0.0"
-    "@babel/plugin-transform-modules-amd" "^7.0.0"
-    "@babel/plugin-transform-object-super" "^7.0.0"
-    "@babel/plugin-transform-parameters" "^7.0.0"
-    "@babel/plugin-transform-regenerator" "^7.0.0"
-    "@babel/plugin-transform-shorthand-properties" "^7.0.0"
-    "@babel/plugin-transform-spread" "^7.0.0"
-    "@babel/plugin-transform-sticky-regex" "^7.0.0"
-    "@babel/plugin-transform-template-literals" "^7.0.0"
-    "@babel/plugin-transform-typeof-symbol" "^7.0.0"
-    "@babel/plugin-transform-unicode-regex" "^7.0.0"
-    "@babel/traverse" "^7.0.0"
-    "@polymer/esm-amd-loader" "^1.0.0"
-    "@types/babel-types" "^6.25.1"
-    "@types/babylon" "^6.16.2"
-    "@types/gulp-if" "0.0.33"
-    "@types/html-minifier" "^3.5.1"
-    "@types/is-windows" "^0.2.0"
-    "@types/mz" "0.0.31"
-    "@types/parse5" "^2.2.34"
-    "@types/resolve" "0.0.7"
-    "@types/uuid" "^3.4.3"
-    "@types/vinyl" "^2.0.0"
-    "@types/vinyl-fs" "^2.4.8"
-    babel-plugin-minify-guarded-expressions "^0.4.3"
-    babel-preset-minify "^0.5.0"
-    babylon "^7.0.0-beta.42"
-    css-slam "^2.1.2"
-    dom5 "^3.0.0"
-    gulp-if "^2.0.2"
-    html-minifier "^3.5.10"
-    matcher "^1.1.0"
-    multipipe "^1.0.2"
-    mz "^2.6.0"
-    parse5 "^4.0.0"
-    plylog "^1.0.0"
-    polymer-analyzer "^3.1.3"
-    polymer-bundler "^4.0.9"
-    polymer-project-config "^4.0.3"
-    regenerator-runtime "^0.11.1"
-    stream "0.0.2"
-    sw-precache "^5.1.1"
-    uuid "^3.2.1"
-    vinyl "^1.2.0"
-    vinyl-fs "^2.4.4"
-
-polymer-bundler@^4.0.9:
-  version "4.0.10"
-  resolved "https://registry.yarnpkg.com/polymer-bundler/-/polymer-bundler-4.0.10.tgz#abc8d33977652f031068d034c8104841e80d4cbb"
-  integrity sha512-nwlN3LQlQDqbZ2sUH3394C/dHZUDHq8tpdS5HARvPDb0Q9IXWD+znOR1cr7wSjF0EZN4LiUH5hWyUoV4QSjhpQ==
-  dependencies:
-    "@types/babel-generator" "^6.25.1"
-    "@types/babel-traverse" "^6.25.3"
-    babel-generator "^6.26.1"
-    babel-traverse "^6.26.0"
-    babel-types "^6.26.0"
-    clone "^2.1.0"
-    command-line-args "^5.0.2"
-    command-line-usage "^5.0.5"
-    dom5 "^3.0.0"
-    espree "^3.5.2"
-    magic-string "^0.22.4"
-    mkdirp "^0.5.1"
-    parse5 "^4.0.0"
-    polymer-analyzer "^3.2.2"
-    rollup "^1.3.0"
-    source-map "^0.5.6"
-    vscode-uri "=1.0.6"
-
-polymer-cli@^1.9.11:
-  version "1.9.11"
-  resolved "https://registry.npmjs.org/polymer-cli/-/polymer-cli-1.9.11.tgz"
-  integrity sha512-tiURjHDCOUUtDVPuVYvrfFI9PXe4OOUmBbn6Sg5GJNQ2POtP7r7hv+I5yI8P9qsxmalHTa19chVtf5/t9IBXDg==
-  dependencies:
-    "@octokit/rest" "^16.2.0"
-    "@types/chalk" "^2.2.0"
-    "@types/del" "^3.0.0"
-    "@types/findup-sync" "^0.3.29"
-    "@types/globby" "^6.1.0"
-    "@types/inquirer" "0.0.32"
-    "@types/merge-stream" "^1.0.28"
-    "@types/mz" "^0.0.31"
-    "@types/request" "2.0.3"
-    "@types/resolve" "0.0.4"
-    "@types/rimraf" "^0.0.28"
-    "@types/semver" "^5.3.30"
-    "@types/temp" "^0.8.28"
-    "@types/update-notifier" "^1.0.0"
-    "@types/vinyl" "^2.0.0"
-    "@types/vinyl-fs" "0.0.28"
-    "@types/yeoman-generator" "^2.0.3"
-    bower "^1.8.8"
-    bower-json "^0.8.1"
-    bower-logger "^0.2.2"
-    chalk "^2.4.2"
-    chokidar "^1.7.0"
-    command-line-args "^5.0.2"
-    command-line-commands "^2.0.1"
-    command-line-usage "^5.0.5"
-    del "^3.0.0"
-    findup-sync "^0.4.2"
-    globby "^8.0.1"
-    gunzip-maybe "^1.3.1"
-    inquirer "^1.0.2"
-    merge-stream "^1.0.1"
-    mz "^2.6.0"
-    plylog "^1.0.0"
-    polymer-analyzer "^3.2.2"
-    polymer-build "^3.1.4"
-    polymer-bundler "^4.0.9"
-    polymer-linter "^3.0.0"
-    polymer-project-config "^4.0.3"
-    polyserve "^0.27.15"
-    request "^2.72.0"
-    rimraf "^2.6.1"
-    semver "^5.3.0"
-    tar-fs "^1.12.0"
-    temp "^0.8.3"
-    update-notifier "^1.0.0"
-    validate-element-name "^2.1.1"
-    vinyl "^1.1.1"
-    vinyl-fs "^2.4.3"
-    web-component-tester "^6.9.0"
-    yeoman-environment "^1.5.2"
-    yeoman-generator "^3.1.1"
-
-polymer-linter@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/polymer-linter/-/polymer-linter-3.0.1.tgz#8804e1705fa2a7c263467b8a22da11bb764ee26b"
-  integrity sha512-eDh2CeswZz4Rwf8gfYXpMN66pieq4qJvP9bH3m39LLGm81hRePo4N5OHoQzR5unen1PUdmtjDv0Iicz3dTYEZQ==
-  dependencies:
-    "@types/fast-levenshtein" "0.0.1"
-    "@types/parse5" "^2.2.34"
-    babel-traverse "^6.26.0"
-    babel-types "^6.26.0"
-    cancel-token "^0.1.1"
-    css-what "^2.1.0"
-    dom5 "^3.0.0"
-    fast-levenshtein "^2.0.6"
-    parse5 "^4.0.0"
-    polymer-analyzer "^3.0.0"
-    shady-css-parser "^0.1.0"
-    stable "^0.1.6"
-    strip-indent "^2.0.0"
-    validate-element-name "^2.1.1"
-
-polymer-project-config@^4.0.0, polymer-project-config@^4.0.3:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/polymer-project-config/-/polymer-project-config-4.0.3.tgz#ef0c1a676ce4809907986c8e910745660de8024f"
-  integrity sha512-Drr+Imq+znhBC8XSt9pMlmPixoGnIOmleV5SD6mto1zOGC5oCDbSNsQL2v89DWOk+9aSUO79vnWwOmEPDSvYfw==
-  dependencies:
-    "@types/parse5" "^2.2.34"
-    browser-capabilities "^1.0.0"
-    jsonschema "^1.1.1"
-    minimatch-all "^1.1.0"
-    plylog "^1.0.0"
-    winston "^3.0.0"
-
-polyserve@^0.27.13, polyserve@^0.27.15:
-  version "0.27.15"
-  resolved "https://registry.yarnpkg.com/polyserve/-/polyserve-0.27.15.tgz#261fa5a0873c8d95fd7068598f44c9dac20cf9c4"
-  integrity sha512-AaFgANt+tUUVgHLw+BnaVYcn649JiwL1ru0TOZUKj1gGGn/Bq2S16gxql+1muGpRaAsgFu13Zu7k5XkwatwwSg==
-  dependencies:
-    "@types/compression" "^0.0.33"
-    "@types/content-type" "^1.1.0"
-    "@types/escape-html" "0.0.20"
-    "@types/express" "^4.0.36"
-    "@types/mime" "^2.0.0"
-    "@types/mz" "0.0.29"
-    "@types/opn" "^3.0.28"
-    "@types/parse5" "^2.2.34"
-    "@types/pem" "^1.8.1"
-    "@types/resolve" "0.0.6"
-    "@types/serve-static" "^1.7.31"
-    "@types/spdy" "^3.4.1"
-    bower-config "^1.4.1"
-    browser-capabilities "^1.0.0"
-    command-line-args "^5.0.2"
-    command-line-usage "^5.0.5"
-    compression "^1.6.2"
-    content-type "^1.0.2"
-    cors "^2.8.4"
-    escape-html "^1.0.3"
-    express "^4.8.5"
-    find-port "^1.0.1"
-    http-proxy-middleware "^0.17.2"
-    lru-cache "^4.0.2"
-    mime "^2.3.1"
-    mz "^2.4.0"
-    opn "^3.0.2"
-    pem "^1.8.3"
-    polymer-build "^3.1.0"
-    polymer-project-config "^4.0.0"
-    requirejs "^2.3.4"
-    resolve "^1.5.0"
-    send "^0.16.2"
-    spdy "^3.3.3"
-
-posix-character-classes@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
-  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+    find-up "^2.1.0"
 
 prelude-ls@^1.2.1:
   version "1.2.1"
-  resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
-prepend-http@^1.0.1:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
-  integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
-
 prepend-http@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
   integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
 
-preserve@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
-  integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
-
 prettier-linter-helpers@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
   integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
   dependencies:
     fast-diff "^1.1.2"
@@ -8166,33 +2257,18 @@
   integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
 
 prettier@^2.1.2:
-  version "2.2.1"
-  resolved "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz"
-  integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
+  integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
 
-pretty-bytes@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
-  integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=
-
-pretty-bytes@^5.1.0, pretty-bytes@^5.2.0:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
-  integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
-
-process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
-  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
-progress@2.0.3, progress@^2.0.0:
+progress@^2.0.0:
   version "2.0.3"
-  resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
 protobufjs@6.8.8:
   version "6.8.8"
-  resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz"
+  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c"
   integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==
   dependencies:
     "@protobufjs/aspromise" "^1.1.2"
@@ -8209,131 +2285,39 @@
     "@types/node" "^10.1.0"
     long "^4.0.0"
 
-proxy-addr@~2.0.5:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
-  integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
-  dependencies:
-    forwarded "~0.1.2"
-    ipaddr.js "1.9.1"
-
-pseudomap@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
-  integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
-
-psl@^1.1.24, psl@^1.1.28:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
-  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
-
-pump@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
-  integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
-  dependencies:
-    end-of-stream "^1.1.0"
-    once "^1.3.1"
-
-pump@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
-  integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
-  dependencies:
-    end-of-stream "^1.1.0"
-    once "^1.3.1"
-
 pump@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
   integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
   dependencies:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
-pumpify@^1.3.3:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
-  integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
-  dependencies:
-    duplexify "^3.6.0"
-    inherits "^2.0.3"
-    pump "^2.0.0"
-
-punycode@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
-
-punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.1.0:
   version "2.1.1"
-  resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
 pupa@^2.1.1:
   version "2.1.1"
-  resolved "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62"
   integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==
   dependencies:
     escape-goat "^2.0.0"
 
-q@^1.4.1, q@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
-  integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
-
-qs@6.7.0:
-  version "6.7.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
-  integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
-
-qs@~6.5.2:
-  version "6.5.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
-  integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
-
 queue-microtask@^1.2.2:
   version "1.2.3"
-  resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
 quick-lru@^4.0.1:
   version "4.0.1"
-  resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
   integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
 
-quick-lru@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932"
-  integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==
-
-randomatic@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
-  integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==
-  dependencies:
-    is-number "^4.0.0"
-    kind-of "^6.0.0"
-    math-random "^1.0.1"
-
-range-parser@~1.2.0, range-parser@~1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
-  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-
-raw-body@2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
-  integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
-  dependencies:
-    bytes "3.1.0"
-    http-errors "1.7.2"
-    iconv-lite "0.4.24"
-    unpipe "1.0.0"
-
-rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
+rc@^1.2.8:
   version "1.2.8"
-  resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
   integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
   dependencies:
     deep-extend "^0.6.0"
@@ -8341,81 +2325,23 @@
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-read-all-stream@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa"
-  integrity sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=
-  dependencies:
-    pinkie-promise "^2.0.0"
-    readable-stream "^2.0.0"
-
-read-chunk@^3.0.0, read-chunk@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-3.2.0.tgz#2984afe78ca9bfbbdb74b19387bf9e86289c16ca"
-  integrity sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==
-  dependencies:
-    pify "^4.0.1"
-    with-open-file "^0.1.6"
-
-read-pkg-up@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
-  integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
-  dependencies:
-    find-up "^1.0.0"
-    read-pkg "^1.0.0"
-
-read-pkg-up@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz"
-  integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
+read-pkg-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
+  integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
   dependencies:
     find-up "^2.0.0"
-    read-pkg "^2.0.0"
-
-read-pkg-up@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
-  integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
-  dependencies:
-    find-up "^3.0.0"
     read-pkg "^3.0.0"
 
-read-pkg-up@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-5.0.0.tgz#b6a6741cb144ed3610554f40162aa07a6db621b8"
-  integrity sha512-XBQjqOBtTzyol2CpsQOw8LHV0XbDZVG7xMMjmXAJomlVY03WOBRmYgDJETlvcg0H63AJvPRwT7GFi5rvOzUOKg==
-  dependencies:
-    find-up "^3.0.0"
-    read-pkg "^5.0.0"
-
 read-pkg-up@^7.0.1:
   version "7.0.1"
-  resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
   integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
   dependencies:
     find-up "^4.1.0"
     read-pkg "^5.2.0"
     type-fest "^0.8.1"
 
-read-pkg@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
-  integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
-  dependencies:
-    load-json-file "^1.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^1.0.0"
-
-read-pkg@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz"
-  integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
-  dependencies:
-    load-json-file "^2.0.0"
-    normalize-package-data "^2.3.2"
-    path-type "^2.0.0"
-
 read-pkg@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
@@ -8425,9 +2351,9 @@
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-read-pkg@^5.0.0, read-pkg@^5.2.0:
+read-pkg@^5.2.0:
   version "5.2.0"
-  resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
   integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
   dependencies:
     "@types/normalize-package-data" "^2.4.0"
@@ -8435,17 +2361,7 @@
     parse-json "^5.0.0"
     type-fest "^0.6.0"
 
-readable-stream@1.1.x:
-  version "1.1.14"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
-  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
-"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0:
+readable-stream@^3.1.1:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
   integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -8454,311 +2370,56 @@
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
-"readable-stream@>=1.0.33-1 <1.1.0-0":
-  version "1.0.34"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
-  integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
-readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6:
-  version "2.3.7"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
-  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.3"
-    isarray "~1.0.0"
-    process-nextick-args "~2.0.0"
-    safe-buffer "~5.1.1"
-    string_decoder "~1.1.1"
-    util-deprecate "~1.0.1"
-
-readdirp@^2.0.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
-  integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
-  dependencies:
-    graceful-fs "^4.1.11"
-    micromatch "^3.1.10"
-    readable-stream "^2.0.2"
-
-rechoir@^0.6.2:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
-  integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
-  dependencies:
-    resolve "^1.1.6"
-
-redent@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
-  integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
-  dependencies:
-    indent-string "^2.1.0"
-    strip-indent "^1.0.1"
-
 redent@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
   integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
   dependencies:
     indent-string "^4.0.0"
     strip-indent "^3.0.0"
 
-reduce-flatten@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
-  integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=
-
-regenerate-unicode-properties@^8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
-  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
-  dependencies:
-    regenerate "^1.4.0"
-
-regenerate@^1.4.0:
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
-  integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-
-regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
-  version "0.11.1"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
-  integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
-regenerator-runtime@^0.13.4:
-  version "0.13.7"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
-  integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
-
-regenerator-transform@^0.14.2:
-  version "0.14.5"
-  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4"
-  integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==
-  dependencies:
-    "@babel/runtime" "^7.8.4"
-
-regex-cache@^0.4.2:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
-  integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==
-  dependencies:
-    is-equal-shallow "^0.1.3"
-
-regex-not@^1.0.0, regex-not@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
-  integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
-  dependencies:
-    extend-shallow "^3.0.2"
-    safe-regex "^1.1.0"
-
 regexpp@^3.0.0, regexpp@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz"
-  integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
-
-regexpu-core@^4.7.1:
-  version "4.7.1"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
-  integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
-  dependencies:
-    regenerate "^1.4.0"
-    regenerate-unicode-properties "^8.2.0"
-    regjsgen "^0.5.1"
-    regjsparser "^0.6.4"
-    unicode-match-property-ecmascript "^1.0.4"
-    unicode-match-property-value-ecmascript "^1.2.0"
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
+  integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
 regextras@^0.7.1:
   version "0.7.1"
-  resolved "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz"
+  resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2"
   integrity sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==
 
-registry-auth-token@^3.0.1:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e"
-  integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==
-  dependencies:
-    rc "^1.1.6"
-    safe-buffer "^5.0.1"
-
 registry-auth-token@^4.0.0:
   version "4.2.1"
-  resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz"
+  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250"
   integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==
   dependencies:
     rc "^1.2.8"
 
-registry-url@^3.0.3:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
-  integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI=
-  dependencies:
-    rc "^1.0.1"
-
 registry-url@^5.0.0:
   version "5.1.0"
-  resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
   integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
   dependencies:
     rc "^1.2.8"
 
-regjsgen@^0.5.1:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
-  integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
-
-regjsparser@^0.6.4:
-  version "0.6.9"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6"
-  integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==
-  dependencies:
-    jsesc "~0.5.0"
-
-relateurl@0.2.x:
-  version "0.2.7"
-  resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
-  integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
-
-remove-trailing-separator@^1.0.1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
-  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
-
-repeat-element@^1.1.2:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
-  integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
-
-repeat-string@^1.5.2, repeat-string@^1.6.1:
-  version "1.6.1"
-  resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
-  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
-
-repeating@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
-  integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
-  dependencies:
-    is-finite "^1.0.0"
-
-replace-ext@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
-  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
-
-replace-ext@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
-  integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
-
-request@2.88.0:
-  version "2.88.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
-  integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.0"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.4.3"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
-request@^2.72.0, request@^2.85.0:
-  version "2.88.2"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
-  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.3"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.5.0"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
 require-from-string@^2.0.2:
   version "2.0.2"
-  resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
   integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
 
-requirejs@^2.3.4:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
-  integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
-
-requires-port@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
-  integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
-
-resolve-alpn@^1.0.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.1.1.tgz#4a006a7d533c81a5dd04681612090fde227cd6e1"
-  integrity sha512-0KbFjFPR2bnJhNx1t8Ad6RqVc8+QPJC4y561FYyC/Q/6OzB3fhUzB5PEgitYhPK6aifwR5gXBSnDMllaDWixGQ==
-
-resolve-dir@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e"
-  integrity sha1-shklmlYC+sXFxJatiUpujMQwJh4=
-  dependencies:
-    expand-tilde "^1.2.2"
-    global-modules "^0.2.3"
-
-resolve-dir@^1.0.0, resolve-dir@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
-  integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
-  dependencies:
-    expand-tilde "^2.0.0"
-    global-modules "^1.0.0"
+requireindex@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
+  integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
 
 resolve-from@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
-resolve-url@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
-  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-
-resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.5.0:
+resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0:
   version "1.20.0"
-  resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
   dependencies:
     is-core-module "^2.2.0"
@@ -8766,507 +2427,139 @@
 
 responselike@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
   integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
   dependencies:
     lowercase-keys "^1.0.0"
 
-responselike@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723"
-  integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==
-  dependencies:
-    lowercase-keys "^2.0.0"
-
-restore-cursor@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
-  integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=
-  dependencies:
-    exit-hook "^1.0.0"
-    onetime "^1.0.0"
-
 restore-cursor@^3.1.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
   integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
   dependencies:
     onetime "^5.1.0"
     signal-exit "^3.0.2"
 
-ret@~0.1.10:
-  version "0.1.15"
-  resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
-  integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
-
 reusify@^1.0.4:
   version "1.0.4"
-  resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
-rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
-  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
-  dependencies:
-    glob "^7.1.3"
-
-rimraf@^3.0.0, rimraf@^3.0.2:
+rimraf@^3.0.2:
   version "3.0.2"
-  resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
   dependencies:
     glob "^7.1.3"
 
-rimraf@~2.6.2:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
-  integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
-  dependencies:
-    glob "^7.1.3"
-
-rollup@^1.3.0:
-  version "1.32.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4"
-  integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==
-  dependencies:
-    "@types/estree" "*"
-    "@types/node" "*"
-    acorn "^7.1.0"
-
 rollup@^2.45.2:
-  version "2.45.2"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.45.2.tgz#8fb85917c9f35605720e92328f3ccbfba6f78b48"
-  integrity sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ==
+  version "2.56.3"
+  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.56.3.tgz#b63edadd9851b0d618a6d0e6af8201955a77aeff"
+  integrity sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==
   optionalDependencies:
-    fsevents "~2.3.1"
+    fsevents "~2.3.2"
 
-run-async@^2.0.0, run-async@^2.2.0, run-async@^2.4.0:
+run-async@^2.4.0:
   version "2.4.1"
-  resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
   integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
 
 run-parallel@^1.1.9:
   version "1.2.0"
-  resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
   integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
   dependencies:
     queue-microtask "^1.2.2"
 
-rx@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
-  integrity sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=
-
-rxjs@^6.4.0, rxjs@^6.6.0:
+rxjs@^6.6.0:
   version "6.6.7"
-  resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
   integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
   dependencies:
     tslib "^1.9.0"
 
-safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
-  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+safe-buffer@~5.2.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
   integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
-safe-regex@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
-  integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
-  dependencies:
-    ret "~0.1.10"
-
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+"safer-buffer@>= 2.1.2 < 3":
   version "2.1.2"
-  resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
+  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-samsam@1.x, samsam@^1.1.3:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
-  integrity sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==
-
-sauce-connect-launcher@^1.0.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.2.tgz#dfc675a258550809a8eaf457eb9162b943ddbaf0"
-  integrity sha512-wf0coUlidJ7rmeClgVVBh6Kw55/yalZCY/Un5RgjSnTXRAeGqagnTsTYpZaqC4dCtrY4myuYpOAZXCdbO7lHfQ==
-  dependencies:
-    adm-zip "~0.4.3"
-    async "^2.1.2"
-    https-proxy-agent "^5.0.0"
-    lodash "^4.16.6"
-    rimraf "^2.5.4"
-
-scoped-regex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
-  integrity sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=
-
-select-hose@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
-  integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
-
-selenium-standalone@^6.7.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.23.0.tgz#91a7d12b1c8ba077a82b44323445c5882eb20ff1"
-  integrity sha512-6dVLSEvbixd/MRSEmrcRQD8dmABrzNsxRqroKFQY+RVzm1JVPgGHIlo6qJzG6akfjc2V8SadHslE6lN4BFVM3w==
-  dependencies:
-    commander "^2.20.3"
-    cross-spawn "^7.0.3"
-    debug "^4.3.1"
-    got "^11.8.0"
-    lodash.mapvalues "^4.6.0"
-    lodash.merge "^4.6.2"
-    minimist "^1.2.5"
-    mkdirp "^1.0.4"
-    progress "2.0.3"
-    tar-stream "2.1.4"
-    which "^2.0.2"
-    yauzl "^2.10.0"
-
-semver-diff@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
-  integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
-  dependencies:
-    semver "^5.0.3"
-
 semver-diff@^3.1.1:
   version "3.1.1"
-  resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
   integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==
   dependencies:
     semver "^6.3.0"
 
-"semver@2 || 3 || 4 || 5", semver@5.6.0, semver@^5.3.0:
-  version "5.6.0"
-  resolved "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz"
-  integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
-
-semver@^5.0.3, semver@^5.1.0, semver@^5.5.0:
+"semver@2 || 3 || 4 || 5":
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
+semver@5.6.0:
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+  integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
+
 semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0:
   version "6.3.0"
-  resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
+semver@^7.2.1, semver@^7.3.4, semver@^7.3.5:
   version "7.3.5"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
   integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
   dependencies:
     lru-cache "^6.0.0"
 
-send@0.17.1:
-  version "0.17.1"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
-  integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
-  dependencies:
-    debug "2.6.9"
-    depd "~1.1.2"
-    destroy "~1.0.4"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    fresh "0.5.2"
-    http-errors "~1.7.2"
-    mime "1.6.0"
-    ms "2.1.1"
-    on-finished "~2.3.0"
-    range-parser "~1.2.1"
-    statuses "~1.5.0"
-
-send@^0.16.1, send@^0.16.2:
-  version "0.16.2"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
-  integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==
-  dependencies:
-    debug "2.6.9"
-    depd "~1.1.2"
-    destroy "~1.0.4"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    fresh "0.5.2"
-    http-errors "~1.6.2"
-    mime "1.4.1"
-    ms "2.0.0"
-    on-finished "~2.3.0"
-    range-parser "~1.2.0"
-    statuses "~1.4.0"
-
-serve-static@1.14.1:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
-  integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
-  dependencies:
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    parseurl "~1.3.3"
-    send "0.17.1"
-
-server-destroy@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd"
-  integrity sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=
-
-serviceworker-cache-polyfill@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/serviceworker-cache-polyfill/-/serviceworker-cache-polyfill-4.0.0.tgz#de19ee73bef21ab3c0740a37b33db62464babdeb"
-  integrity sha1-3hnuc77yGrPAdAo3sz22JGS6ves=
-
-set-getter@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376"
-  integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=
-  dependencies:
-    to-object-path "^0.3.0"
-
-set-value@^2.0.0, set-value@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
-  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
-  dependencies:
-    extend-shallow "^2.0.1"
-    is-extendable "^0.1.1"
-    is-plain-object "^2.0.3"
-    split-string "^3.0.1"
-
-setprototypeof@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
-  integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
-
-setprototypeof@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
-  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
-
-shady-css-parser@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/shady-css-parser/-/shady-css-parser-0.1.0.tgz#534dc79c8ca5884c5ed92a4e5a13d6d863bca428"
-  integrity sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA==
-
-shallow-clone@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
-  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
-  dependencies:
-    kind-of "^6.0.2"
-
-shebang-command@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
-  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
-  dependencies:
-    shebang-regex "^1.0.0"
-
 shebang-command@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
   integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
   dependencies:
     shebang-regex "^3.0.0"
 
-shebang-regex@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
-
 shebang-regex@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-shelljs@^0.8.0, shelljs@^0.8.4:
-  version "0.8.4"
-  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
-  integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
+side-channel@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
   dependencies:
-    glob "^7.0.0"
-    interpret "^1.0.0"
-    rechoir "^0.6.2"
+    call-bind "^1.0.0"
+    get-intrinsic "^1.0.2"
+    object-inspect "^1.9.0"
 
-signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
+signal-exit@^3.0.2, signal-exit@^3.0.3:
   version "3.0.3"
-  resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
-simple-swizzle@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
-  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
-  dependencies:
-    is-arrayish "^0.3.1"
-
-sinon-chai@^2.10.0:
-  version "2.14.0"
-  resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d"
-  integrity sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==
-
-sinon@^2.3.5:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36"
-  integrity sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==
-  dependencies:
-    diff "^3.1.0"
-    formatio "1.2.0"
-    lolex "^1.6.0"
-    native-promise-only "^0.8.1"
-    path-to-regexp "^1.7.0"
-    samsam "^1.1.3"
-    text-encoding "0.6.4"
-    type-detect "^4.0.0"
-
-slash@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
-  integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
-
-slash@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
-  integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
-
 slash@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
 slice-ansi@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
   integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
   dependencies:
     ansi-styles "^4.0.0"
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
-slide@^1.1.5:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
-  integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
-
-snapdragon-node@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
-  integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
-  dependencies:
-    define-property "^1.0.0"
-    isobject "^3.0.0"
-    snapdragon-util "^3.0.1"
-
-snapdragon-util@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
-  integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
-  dependencies:
-    kind-of "^3.2.0"
-
-snapdragon@^0.8.1:
-  version "0.8.2"
-  resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
-  integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
-  dependencies:
-    base "^0.11.1"
-    debug "^2.2.0"
-    define-property "^0.2.5"
-    extend-shallow "^2.0.1"
-    map-cache "^0.2.2"
-    source-map "^0.5.6"
-    source-map-resolve "^0.5.0"
-    use "^3.1.0"
-
-socket.io-adapter@~1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
-  integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
-
-socket.io-client@2.4.0:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35"
-  integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==
-  dependencies:
-    backo2 "1.0.2"
-    component-bind "1.0.0"
-    component-emitter "~1.3.0"
-    debug "~3.1.0"
-    engine.io-client "~3.5.0"
-    has-binary2 "~1.0.2"
-    indexof "0.0.1"
-    parseqs "0.0.6"
-    parseuri "0.0.6"
-    socket.io-parser "~3.3.0"
-    to-array "0.1.4"
-
-socket.io-parser@~3.3.0:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.2.tgz#ef872009d0adcf704f2fbe830191a14752ad50b6"
-  integrity sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==
-  dependencies:
-    component-emitter "~1.3.0"
-    debug "~3.1.0"
-    isarray "2.0.1"
-
-socket.io-parser@~3.4.0:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a"
-  integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==
-  dependencies:
-    component-emitter "1.2.1"
-    debug "~4.1.0"
-    isarray "2.0.1"
-
-socket.io@^2.0.3:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2"
-  integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==
-  dependencies:
-    debug "~4.1.0"
-    engine.io "~3.5.0"
-    has-binary2 "~1.0.2"
-    socket.io-adapter "~1.1.0"
-    socket.io-client "2.4.0"
-    socket.io-parser "~3.4.0"
-
-sort-keys-length@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188"
-  integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=
-  dependencies:
-    sort-keys "^1.0.0"
-
-sort-keys@^1.0.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
-  integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0=
-  dependencies:
-    is-plain-obj "^1.0.0"
-
-source-map-resolve@^0.5.0:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
-  integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
-  dependencies:
-    atob "^2.1.2"
-    decode-uri-component "^0.2.0"
-    resolve-url "^0.2.1"
-    source-map-url "^0.4.0"
-    urix "^0.1.0"
-
 source-map-support@0.5.9:
   version "0.5.9"
-  resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
   integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
   dependencies:
     buffer-from "^1.0.0"
@@ -9280,19 +2573,9 @@
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
-source-map-url@^0.4.0:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
-  integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
-
-source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
-  version "0.5.7"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-  integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
-
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
+source-map@^0.6.0:
   version "0.6.1"
-  resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
 source-map@~0.7.2:
@@ -9300,17 +2583,9 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
-spawn-sync@^1.0.15:
-  version "1.0.15"
-  resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
-  integrity sha1-sAeZVX63+wyDdsKdROih6mfldHY=
-  dependencies:
-    concat-stream "^1.4.7"
-    os-shim "^0.1.2"
-
 spdx-correct@^3.0.0:
   version "3.1.1"
-  resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
   integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
   dependencies:
     spdx-expression-parse "^3.0.0"
@@ -9318,152 +2593,30 @@
 
 spdx-exceptions@^2.1.0:
   version "2.3.0"
-  resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
   integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
 
 spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1:
   version "3.0.1"
-  resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
   integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
   dependencies:
     spdx-exceptions "^2.1.0"
     spdx-license-ids "^3.0.0"
 
 spdx-license-ids@^3.0.0:
-  version "3.0.7"
-  resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz"
-  integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==
-
-spdy-transport@^2.0.18:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.1.1.tgz#c54815d73858aadd06ce63001e7d25fa6441623b"
-  integrity sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==
-  dependencies:
-    debug "^2.6.8"
-    detect-node "^2.0.3"
-    hpack.js "^2.1.6"
-    obuf "^1.1.1"
-    readable-stream "^2.2.9"
-    safe-buffer "^5.0.1"
-    wbuf "^1.7.2"
-
-spdy@^3.3.3:
-  version "3.4.7"
-  resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc"
-  integrity sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=
-  dependencies:
-    debug "^2.6.8"
-    handle-thing "^1.2.5"
-    http-deceiver "^1.2.7"
-    safe-buffer "^5.0.1"
-    select-hose "^2.0.0"
-    spdy-transport "^2.0.18"
-
-split-string@^3.0.1, split-string@^3.0.2:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
-  integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
-  dependencies:
-    extend-shallow "^3.0.0"
+  version "3.0.10"
+  resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
+  integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
 
 sprintf-js@~1.0.2:
   version "1.0.3"
-  resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
   integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
 
-sshpk@^1.7.0:
-  version "1.16.1"
-  resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
-  integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
-  dependencies:
-    asn1 "~0.2.3"
-    assert-plus "^1.0.0"
-    bcrypt-pbkdf "^1.0.0"
-    dashdash "^1.12.0"
-    ecc-jsbn "~0.1.1"
-    getpass "^0.1.1"
-    jsbn "~0.1.0"
-    safer-buffer "^2.0.2"
-    tweetnacl "~0.14.0"
-
-stable@^0.1.6:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
-  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
-
-stack-trace@0.0.x:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-  integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
-
-stacky@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/stacky/-/stacky-1.3.1.tgz#3f117e5187b9a73d23f876d69f05c85b11804a12"
-  integrity sha1-PxF+UYe5pz0j+HbWnwXIWxGAShI=
-  dependencies:
-    chalk "^1.1.1"
-    lodash "^3.0.0"
-
-static-extend@^0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
-  integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
-  dependencies:
-    define-property "^0.2.5"
-    object-copy "^0.1.0"
-
-"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
-  integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-
-statuses@~1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
-  integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
-
-stream-shift@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
-  integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
-
-stream@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef"
-  integrity sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=
-  dependencies:
-    emitter-component "^1.1.1"
-
-streamsearch@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
-  integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
-
-string-template@~0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
-  integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
-
-string-width@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
-  integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
-  dependencies:
-    code-point-at "^1.0.0"
-    is-fullwidth-code-point "^1.0.0"
-    strip-ansi "^3.0.0"
-
-string-width@^2.0.0, string-width@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
-  integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
-  dependencies:
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^4.0.0"
-
 string-width@^3.0.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
   dependencies:
     emoji-regex "^7.0.1"
@@ -9472,7 +2625,7 @@
 
 string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.2"
-  resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
   integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
   dependencies:
     emoji-regex "^8.0.0"
@@ -9481,7 +2634,7 @@
 
 string.prototype.trimend@^1.0.4:
   version "1.0.4"
-  resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
   integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
   dependencies:
     call-bind "^1.0.2"
@@ -9489,7 +2642,7 @@
 
 string.prototype.trimstart@^1.0.4:
   version "1.0.4"
-  resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
   integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
   dependencies:
     call-bind "^1.0.2"
@@ -9502,491 +2655,120 @@
   dependencies:
     safe-buffer "~5.2.0"
 
-string_decoder@~0.10.x:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
-
-string_decoder@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
-  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
-  dependencies:
-    safe-buffer "~5.1.0"
-
-strip-ansi@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
-  integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
-  dependencies:
-    ansi-regex "^2.0.0"
-
-strip-ansi@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
-  integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
-  dependencies:
-    ansi-regex "^3.0.0"
-
 strip-ansi@^5.1.0:
   version "5.2.0"
-  resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
   integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
   dependencies:
     ansi-regex "^4.1.0"
 
 strip-ansi@^6.0.0:
   version "6.0.0"
-  resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
   integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
   dependencies:
     ansi-regex "^5.0.0"
 
-strip-ansi@~0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
-  integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=
-
-strip-bom-buf@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz#1cb45aaf57530f4caf86c7f75179d2c9a51dd572"
-  integrity sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=
-  dependencies:
-    is-utf8 "^0.2.1"
-
-strip-bom-stream@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee"
-  integrity sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=
-  dependencies:
-    first-chunk-stream "^1.0.0"
-    strip-bom "^2.0.0"
-
-strip-bom-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca"
-  integrity sha1-+H217yYT9paKpUWr/h7HKLaoKco=
-  dependencies:
-    first-chunk-stream "^2.0.0"
-    strip-bom "^2.0.0"
-
-strip-bom@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
-  integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
-  dependencies:
-    is-utf8 "^0.2.0"
-
 strip-bom@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
-strip-eof@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
-  integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
-
 strip-final-newline@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-indent@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
-  integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
-  dependencies:
-    get-stdin "^4.0.1"
-
-strip-indent@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
-  integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
-
 strip-indent@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
   integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
   dependencies:
     min-indent "^1.0.0"
 
 strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   version "3.1.1"
-  resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
 strip-json-comments@~2.0.1:
   version "2.0.1"
-  resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
-supports-color@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
-  integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-
 supports-color@^5.3.0:
   version "5.5.0"
-  resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
   integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
   dependencies:
     has-flag "^3.0.0"
 
 supports-color@^7.1.0:
   version "7.2.0"
-  resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
   integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
   dependencies:
     has-flag "^4.0.0"
 
-sw-precache@^5.1.1:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/sw-precache/-/sw-precache-5.2.1.tgz#06134f319eec68f3b9583ce9a7036b1c119f7179"
-  integrity sha512-8FAy+BP/FXE+ILfiVTt+GQJ6UEf4CVHD9OfhzH0JX+3zoy2uFk7Vn9EfXASOtVmmIVbL3jE/W8Z66VgPSZcMhw==
-  dependencies:
-    dom-urls "^1.1.0"
-    es6-promise "^4.0.5"
-    glob "^7.1.1"
-    lodash.defaults "^4.2.0"
-    lodash.template "^4.4.0"
-    meow "^3.7.0"
-    mkdirp "^0.5.1"
-    pretty-bytes "^4.0.2"
-    sw-toolbox "^3.4.0"
-    update-notifier "^2.3.0"
-
-sw-toolbox@^3.4.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/sw-toolbox/-/sw-toolbox-3.6.0.tgz#26df1d1c70348658e4dea2884319149b7b3183b5"
-  integrity sha1-Jt8dHHA0hljk3qKIQxkUm3sxg7U=
-  dependencies:
-    path-to-regexp "^1.0.1"
-    serviceworker-cache-polyfill "^4.0.0"
-
-table-layout@^0.4.3:
-  version "0.4.5"
-  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378"
-  integrity sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==
-  dependencies:
-    array-back "^2.0.0"
-    deep-extend "~0.6.0"
-    lodash.padend "^4.6.1"
-    typical "^2.6.1"
-    wordwrapjs "^3.0.0"
-
-table@^6.0.4:
-  version "6.0.9"
-  resolved "https://registry.npmjs.org/table/-/table-6.0.9.tgz"
-  integrity sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==
+table@^6.0.9:
+  version "6.7.1"
+  resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"
+  integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==
   dependencies:
     ajv "^8.0.1"
-    is-boolean-object "^1.1.0"
-    is-number-object "^1.0.4"
-    is-string "^1.0.5"
     lodash.clonedeep "^4.5.0"
-    lodash.flatten "^4.4.0"
     lodash.truncate "^4.4.2"
     slice-ansi "^4.0.0"
     string-width "^4.2.0"
-
-tar-fs@^1.12.0:
-  version "1.16.3"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
-  integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
-  dependencies:
-    chownr "^1.0.1"
-    mkdirp "^0.5.1"
-    pump "^1.0.0"
-    tar-stream "^1.1.2"
-
-tar-stream@2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa"
-  integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==
-  dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-tar-stream@^1.1.2:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
-  integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
-  dependencies:
-    bl "^1.0.0"
-    buffer-alloc "^1.2.0"
-    end-of-stream "^1.0.0"
-    fs-constants "^1.0.0"
-    readable-stream "^2.3.0"
-    to-buffer "^1.1.1"
-    xtend "^4.0.0"
-
-tar-stream@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
-  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
-  dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
-temp@^0.8.1, temp@^0.8.3:
-  version "0.8.4"
-  resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2"
-  integrity sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==
-  dependencies:
-    rimraf "~2.6.2"
-
-term-size@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
-  integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
-  dependencies:
-    execa "^0.7.0"
-
-ternary-stream@^2.0.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/ternary-stream/-/ternary-stream-2.1.1.tgz#4ad64b98668d796a085af2c493885a435a8a8bfc"
-  integrity sha512-j6ei9hxSoyGlqTmoMjOm+QNvUKDOIY6bNl4Uh1lhBvl6yjPW2iLqxDUYyfDPZknQ4KdRziFl+ec99iT4l7g0cw==
-  dependencies:
-    duplexify "^3.5.0"
-    fork-stream "^0.0.4"
-    merge-stream "^1.0.0"
-    through2 "^2.0.1"
+    strip-ansi "^6.0.0"
 
 terser@^5.6.1:
-  version "5.6.1"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c"
-  integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==
+  version "5.7.2"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.2.tgz#d4d95ed4f8bf735cb933e802f2a1829abf545e3f"
+  integrity sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==
   dependencies:
     commander "^2.20.0"
     source-map "~0.7.2"
     source-map-support "~0.5.19"
 
-text-encoding@0.6.4:
-  version "0.6.4"
-  resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
-  integrity sha1-45mpgiV6J22uQou5KEXLcb3CbRk=
-
-text-hex@1.0.x:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
-  integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
-
 text-table@^0.2.0:
   version "0.2.0"
-  resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
+  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
 
-textextensions@^2.5.0:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4"
-  integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==
-
-thenify-all@^1.0.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
-  integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=
-  dependencies:
-    thenify ">= 3.1.0 < 4"
-
-"thenify@>= 3.1.0 < 4":
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
-  integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
-  dependencies:
-    any-promise "^1.0.0"
-
-through2-filter@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec"
-  integrity sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=
-  dependencies:
-    through2 "~2.0.0"
-    xtend "~4.0.0"
-
-through2-filter@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
-  integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==
-  dependencies:
-    through2 "~2.0.0"
-    xtend "~4.0.0"
-
-through2@^0.6.0:
-  version "0.6.5"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
-  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
-  dependencies:
-    readable-stream ">=1.0.33-1 <1.1.0-0"
-    xtend ">=4.0.0 <4.1.0-0"
-
-through2@^2.0.0, through2@^2.0.1, through2@^2.0.3, through2@~2.0.0:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
-  integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
-  dependencies:
-    readable-stream "~2.3.6"
-    xtend "~4.0.1"
-
-through2@^3.0.0, through2@^3.0.1, through2@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4"
-  integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==
-  dependencies:
-    inherits "^2.0.4"
-    readable-stream "2 || 3"
-
-"through@>=2.2.7 <3", through@^2.3.6:
+through@^2.3.6:
   version "2.3.8"
-  resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
+  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
 
-timed-out@^3.0.0:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217"
-  integrity sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=
-
-timed-out@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
-  integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
-
-tmp@^0.0.29:
-  version "0.0.29"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0"
-  integrity sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=
-  dependencies:
-    os-tmpdir "~1.0.1"
-
 tmp@^0.0.33:
   version "0.0.33"
-  resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
   integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
   dependencies:
     os-tmpdir "~1.0.2"
 
-to-absolute-glob@^0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f"
-  integrity sha1-HN+kcqnvUMI57maZm2YsoOs5k38=
-  dependencies:
-    extend-shallow "^2.0.1"
-
-to-array@0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
-  integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
-
-to-buffer@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
-  integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
-
-to-fast-properties@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
-  integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
-
-to-fast-properties@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
-  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
-
-to-object-path@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
-  integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
-  dependencies:
-    kind-of "^3.0.2"
-
 to-readable-stream@^1.0.0:
   version "1.0.0"
-  resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
   integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
 
-to-regex-range@^2.1.0:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
-  integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
-  dependencies:
-    is-number "^3.0.0"
-    repeat-string "^1.6.1"
-
 to-regex-range@^5.0.1:
   version "5.0.1"
-  resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
   integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
   dependencies:
     is-number "^7.0.0"
 
-to-regex@^3.0.1, to-regex@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
-  integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
-  dependencies:
-    define-property "^2.0.2"
-    extend-shallow "^3.0.2"
-    regex-not "^1.0.2"
-    safe-regex "^1.1.0"
-
-toidentifier@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
-  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
-
-tough-cookie@~2.4.3:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
-  integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
-  dependencies:
-    psl "^1.1.24"
-    punycode "^1.4.1"
-
-tough-cookie@~2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
-  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
-  dependencies:
-    psl "^1.1.28"
-    punycode "^2.1.1"
-
-tr46@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
-  integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
-  dependencies:
-    punycode "^2.1.0"
-
-trim-newlines@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
-  integrity sha1-WIeWa7WCpFA6QetST301ARgVphM=
-
 trim-newlines@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz"
-  integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
+  integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
-trim-right@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
-  integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
-
-triple-beam@^1.2.0, triple-beam@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
-  integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
-
-tsconfig-paths@^3.9.0:
-  version "3.9.0"
-  resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz"
-  integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
+tsconfig-paths@^3.11.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36"
+  integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==
   dependencies:
     "@types/json5" "^0.0.29"
     json5 "^1.0.1"
@@ -9995,39 +2777,27 @@
 
 tslib@^1.8.1, tslib@^1.9.0:
   version "1.14.1"
-  resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 tsutils@2.27.2:
   version "2.27.2"
-  resolved "https://registry.npmjs.org/tsutils/-/tsutils-2.27.2.tgz"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7"
   integrity sha512-qf6rmT84TFMuxAKez2pIfR8UCai49iQsfB7YWVjV1bKpy/d0PWT5rEOSM6La9PiHZ0k1RRZQiwVdVJfQ3BPHgg==
   dependencies:
     tslib "^1.8.1"
 
-tsutils@^3.17.1, tsutils@^3.21.0:
+tsutils@^3.21.0:
   version "3.21.0"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
   integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
   dependencies:
     tslib "^1.8.1"
 
-tunnel-agent@^0.6.0:
-  version "0.6.0"
-  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
-  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
-  dependencies:
-    safe-buffer "^5.0.1"
-
-tweetnacl@^0.14.3, tweetnacl@~0.14.0:
-  version "0.14.5"
-  resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
-  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
-
-twinkie@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/twinkie/-/twinkie-1.1.2.tgz#c301e4fc26d00d61d3d7e5be030dc6a2264271da"
-  integrity sha512-4KwhyrcrRb0WWJKMX/aT+npmMZC0h+sA//+bLhNupmuKvesrH2vEZDe6yIr48FMWKEsdA2xNdQqw/3MapZ5qXQ==
+twinkie@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/twinkie/-/twinkie-1.1.3.tgz#1a6f0cd11c59e245bc2d16c7c9fc1ec13e477229"
+  integrity sha512-8Y1U/UCtf8JC4snuV4KAo4e9nwJcKZUoMVOApihJzua4JJWeGB/2RYqAusKk3cUczJeZRGzirHpP1hkArcbA8A==
   dependencies:
     "@types/minimatch" "3.0.3"
     cheerio "1.0.0-rc.2"
@@ -10036,97 +2806,51 @@
 
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
-  resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
   integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
   dependencies:
     prelude-ls "^1.2.1"
 
-type-detect@^4.0.0:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
-  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
-
 type-fest@^0.18.0:
   version "0.18.1"
-  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
   integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
 
 type-fest@^0.20.2:
   version "0.20.2"
-  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
 type-fest@^0.21.3:
   version "0.21.3"
-  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
   integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
 
 type-fest@^0.6.0:
   version "0.6.0"
-  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
   integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
 
 type-fest@^0.8.1:
   version "0.8.1"
-  resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
   integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
 
-type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
-  version "1.6.18"
-  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
-  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
-  dependencies:
-    media-typer "0.3.0"
-    mime-types "~2.1.24"
-
 typedarray-to-buffer@^3.1.5:
   version "3.1.5"
-  resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
   integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
   dependencies:
     is-typedarray "^1.0.0"
 
-typedarray@^0.0.6:
-  version "0.0.6"
-  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-
-typescript@4.0.5:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
-  integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
-
-typescript@4.3.2:
+typescript@4.0.5, typescript@4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
   integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
 
-typical@^2.6.1:
-  version "2.6.1"
-  resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
-  integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=
-
-typical@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
-  integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
-
-ua-parser-js@^0.7.15:
-  version "0.7.28"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
-  integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
-
-uglify-js@3.4.x:
-  version "3.4.10"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
-  integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==
-  dependencies:
-    commander "~2.19.0"
-    source-map "~0.6.1"
-
-unbox-primitive@^1.0.0:
+unbox-primitive@^1.0.1:
   version "1.0.1"
-  resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
   integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
   dependencies:
     function-bind "^1.1.1"
@@ -10134,151 +2858,16 @@
     has-symbols "^1.0.2"
     which-boxed-primitive "^1.0.2"
 
-underscore@^1.8.3:
-  version "1.13.0"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.0.tgz#3ccdcbb824230fc6bf234ad0ddcd83dff4eafe5f"
-  integrity sha512-sCs4H3pCytsb5K7i072FAEC9YlSYFIbosvM0tAKAlpSSUgD7yC1iXSEGdl5XrDKQ1YUB+p/HDzYrSG2H2Vl36g==
-
-underscore@~1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
-  integrity sha1-izixDKze9jM3uLJOT/htRa6lKag=
-
-unicode-canonical-property-names-ecmascript@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
-  integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
-
-unicode-match-property-ecmascript@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
-  integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
-  dependencies:
-    unicode-canonical-property-names-ecmascript "^1.0.4"
-    unicode-property-aliases-ecmascript "^1.0.4"
-
-unicode-match-property-value-ecmascript@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
-  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
-
-unicode-property-aliases-ecmascript@^1.0.4:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
-  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
-
-union-value@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
-  integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
-  dependencies:
-    arr-union "^3.1.0"
-    get-value "^2.0.6"
-    is-extendable "^0.1.1"
-    set-value "^2.0.1"
-
-unique-stream@^2.0.2:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac"
-  integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==
-  dependencies:
-    json-stable-stringify-without-jsonify "^1.0.1"
-    through2-filter "^3.0.0"
-
-unique-string@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
-  integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
-  dependencies:
-    crypto-random-string "^1.0.0"
-
 unique-string@^2.0.0:
   version "2.0.0"
-  resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
   integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
   dependencies:
     crypto-random-string "^2.0.0"
 
-universal-user-agent@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557"
-  integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==
-  dependencies:
-    os-name "^3.1.0"
-
-universal-user-agent@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
-  integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
-
-unpipe@1.0.0, unpipe@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
-
-unset-value@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
-  integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
-  dependencies:
-    has-value "^0.3.1"
-    isobject "^3.0.0"
-
-untildify@^2.0.0, untildify@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0"
-  integrity sha1-F+soB5h/dpUunASF/DEdBqgmouA=
-  dependencies:
-    os-homedir "^1.0.0"
-
-untildify@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
-  integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==
-
-unzip-response@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe"
-  integrity sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=
-
-unzip-response@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
-  integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
-
-update-notifier@^1.0.0:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-1.0.3.tgz#8f92c515482bd6831b7c93013e70f87552c7cf5a"
-  integrity sha1-j5LFFUgr1oMbfJMBPnD4dVLHz1o=
-  dependencies:
-    boxen "^0.6.0"
-    chalk "^1.0.0"
-    configstore "^2.0.0"
-    is-npm "^1.0.0"
-    latest-version "^2.0.0"
-    lazy-req "^1.1.0"
-    semver-diff "^2.0.0"
-    xdg-basedir "^2.0.0"
-
-update-notifier@^2.2.0, update-notifier@^2.3.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
-  integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
-  dependencies:
-    boxen "^1.2.1"
-    chalk "^2.0.1"
-    configstore "^3.0.0"
-    import-lazy "^2.1.0"
-    is-ci "^1.0.10"
-    is-installed-globally "^0.1.0"
-    is-npm "^1.0.0"
-    latest-version "^3.0.0"
-    semver-diff "^2.0.0"
-    xdg-basedir "^3.0.0"
-
 update-notifier@^5.0.0:
   version "5.1.0"
-  resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
   integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==
   dependencies:
     boxen "^5.0.0"
@@ -10296,284 +2885,41 @@
     semver-diff "^3.1.1"
     xdg-basedir "^4.0.0"
 
-upper-case@^1.1.1:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
-  integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
-
 uri-js@^4.2.2:
   version "4.4.1"
-  resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
   integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
   dependencies:
     punycode "^2.1.0"
 
-urijs@^1.16.1:
-  version "1.19.6"
-  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.6.tgz#51f8cb17ca16faefb20b9a31ac60f84aa2b7c870"
-  integrity sha512-eSXsXZ2jLvGWeLYlQA3Gh36BcjF+0amo92+wHPyN1mdR8Nxf75fuEuYTd9c0a+m/vhCjRK0ESlE9YNLW+E1VEw==
-
-urix@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
-  integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
-
-url-parse-lax@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
-  integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=
-  dependencies:
-    prepend-http "^1.0.1"
-
 url-parse-lax@^3.0.0:
   version "3.0.0"
-  resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
   integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
   dependencies:
     prepend-http "^2.0.0"
 
-url-to-options@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
-  integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=
-
-use@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
-  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
-
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
-utils-merge@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-  integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-
-uuid@^2.0.1:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
-  integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
-
-uuid@^3.2.1, uuid@^3.3.2:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
-  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-
 v8-compile-cache@^2.0.3:
   version "2.3.0"
-  resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
   integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
 
-vali-date@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6"
-  integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=
-
-validate-element-name@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/validate-element-name/-/validate-element-name-2.1.1.tgz#8ff75f7da69f73e7c510588362130508b7ac644e"
-  integrity sha1-j/dffaafc+fFEFiDYhMFCLesZE4=
-  dependencies:
-    is-potential-custom-element-name "^1.0.0"
-    log-symbols "^1.0.0"
-    meow "^3.7.0"
-
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
-  resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
   integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
   dependencies:
     spdx-correct "^3.0.0"
     spdx-expression-parse "^3.0.0"
 
-vargs@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/vargs/-/vargs-0.1.0.tgz#6b6184da6520cc3204ce1b407cac26d92609ebff"
-  integrity sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=
-
-vary@^1, vary@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
-  integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
-
-verror@1.10.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
-  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
-  dependencies:
-    assert-plus "^1.0.0"
-    core-util-is "1.0.2"
-    extsprintf "^1.2.0"
-
-vinyl-file@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-3.0.0.tgz#b104d9e4409ffa325faadd520642d0a3b488b365"
-  integrity sha1-sQTZ5ECf+jJfqt1SBkLQo7SIs2U=
-  dependencies:
-    graceful-fs "^4.1.2"
-    pify "^2.3.0"
-    strip-bom-buf "^1.0.0"
-    strip-bom-stream "^2.0.0"
-    vinyl "^2.0.1"
-
-vinyl-fs@^2.4.3, vinyl-fs@^2.4.4:
-  version "2.4.4"
-  resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-2.4.4.tgz#be6ff3270cb55dfd7d3063640de81f25d7532239"
-  integrity sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=
-  dependencies:
-    duplexify "^3.2.0"
-    glob-stream "^5.3.2"
-    graceful-fs "^4.0.0"
-    gulp-sourcemaps "1.6.0"
-    is-valid-glob "^0.3.0"
-    lazystream "^1.0.0"
-    lodash.isequal "^4.0.0"
-    merge-stream "^1.0.0"
-    mkdirp "^0.5.0"
-    object-assign "^4.0.0"
-    readable-stream "^2.0.4"
-    strip-bom "^2.0.0"
-    strip-bom-stream "^1.0.0"
-    through2 "^2.0.0"
-    through2-filter "^2.0.0"
-    vali-date "^1.0.0"
-    vinyl "^1.0.0"
-
-vinyl@^1.0.0, vinyl@^1.1.1, vinyl@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
-  integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=
-  dependencies:
-    clone "^1.0.0"
-    clone-stats "^0.0.1"
-    replace-ext "0.0.1"
-
-vinyl@^2.0.1, vinyl@^2.2.0, vinyl@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974"
-  integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==
-  dependencies:
-    clone "^2.1.1"
-    clone-buffer "^1.0.0"
-    clone-stats "^1.0.0"
-    cloneable-readable "^1.0.0"
-    remove-trailing-separator "^1.0.1"
-    replace-ext "^1.0.0"
-
-vlq@^0.2.2:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
-  integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
-
-vscode-uri@=1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d"
-  integrity sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==
-
-wbuf@^1.1.0, wbuf@^1.7.2:
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
-  integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
-  dependencies:
-    minimalistic-assert "^1.0.0"
-
-wct-local@^2.1.1:
-  version "2.1.5"
-  resolved "https://registry.yarnpkg.com/wct-local/-/wct-local-2.1.5.tgz#f7986753e3ad9a35d39178a9989350523561fff1"
-  integrity sha512-eqoZhjGy4Xq2tY0uB46Grkw/ztq+/rC0ImbYKl62unFHXtOgal+kkvnxR3SLRFNM8ty9+ItgycPeH0IpTqVL+w==
-  dependencies:
-    "@types/express" "^4.0.30"
-    "@types/freeport" "^1.0.19"
-    "@types/launchpad" "^0.6.0"
-    "@types/which" "^1.3.1"
-    chalk "^2.3.0"
-    cleankill "^2.0.0"
-    freeport "^1.0.4"
-    launchpad "^0.7.0"
-    selenium-standalone "^6.7.0"
-    which "^1.0.8"
-
-wct-sauce@^2.0.2:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/wct-sauce/-/wct-sauce-2.1.0.tgz#67d0be346aabbbc28384e8d143b8d3ca7ba774c0"
-  integrity sha512-c3R4PJcbpS7Gxv2vZ4HDAqpXV6cT9peslAWMU7hHH9PMhKDPbn8RNa6E4DVL0tOmZznB+3cRmtZ6+vJ/aDwu1A==
-  dependencies:
-    chalk "^2.4.1"
-    cleankill "^2.0.0"
-    lodash "^4.17.10"
-    request "^2.85.0"
-    sauce-connect-launcher "^1.0.0"
-    temp "^0.8.1"
-    uuid "^3.2.1"
-
-wd@^1.2.0:
-  version "1.14.0"
-  resolved "https://registry.yarnpkg.com/wd/-/wd-1.14.0.tgz#1fe6450b5baef37caa135e7755292c6998ca8a90"
-  integrity sha512-X7ZfGHHYlQ5zYpRlgP16LUsvYti+Al/6fz3T/ClVyivVCpCZQpESTDdz6zbK910O5OIvujO23Ym2DBBo3XsQlA==
-  dependencies:
-    archiver "^3.0.0"
-    async "^2.0.0"
-    lodash "^4.0.0"
-    mkdirp "^0.5.1"
-    q "^1.5.1"
-    request "2.88.0"
-    vargs "^0.1.0"
-
-web-component-tester@^6.9.0:
-  version "6.9.2"
-  resolved "https://registry.yarnpkg.com/web-component-tester/-/web-component-tester-6.9.2.tgz#40a7b824f2cf3cbc4305552bdfc3357977ded48a"
-  integrity sha512-s2kB/+IE8XWcnxY1fqSpqTiiHEGHWgUWariAbiRlxmAvWSuvaCVNALHYebsZrLCNCLHKcJR8/sGv/bw0MWMvjw==
-  dependencies:
-    "@polymer/sinonjs" "^1.14.1"
-    "@polymer/test-fixture" "^0.0.3"
-    "@webcomponents/webcomponentsjs" "^1.0.7"
-    accessibility-developer-tools "^2.12.0"
-    async "^2.4.1"
-    body-parser "^1.17.2"
-    bower-config "^1.4.0"
-    chalk "^1.1.3"
-    cleankill "^2.0.0"
-    express "^4.15.3"
-    findup-sync "^2.0.0"
-    glob "^7.1.2"
-    lodash "^3.10.1"
-    multer "^1.3.0"
-    nomnom "^1.8.1"
-    polyserve "^0.27.13"
-    resolve "^1.5.0"
-    semver "^5.3.0"
-    send "^0.16.1"
-    server-destroy "^1.0.1"
-    sinon "^2.3.5"
-    sinon-chai "^2.10.0"
-    socket.io "^2.0.3"
-    stacky "^1.3.1"
-    wd "^1.2.0"
-  optionalDependencies:
-    update-notifier "^2.2.0"
-    wct-local "^2.1.1"
-    wct-sauce "^2.0.2"
-
-webidl-conversions@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
-  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
-
-whatwg-url@^6.4.0:
-  version "6.5.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
-  integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
-  dependencies:
-    lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
-
 which-boxed-primitive@^1.0.2:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
   integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
   dependencies:
     is-bigint "^1.0.1"
@@ -10582,101 +2928,28 @@
     is-string "^1.0.5"
     is-symbol "^1.0.3"
 
-which@^1.0.8, which@^1.2.12, which@^1.2.14, which@^1.2.9:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
-  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
-  dependencies:
-    isexe "^2.0.0"
-
-which@^2.0.1, which@^2.0.2:
+which@^2.0.1:
   version "2.0.2"
-  resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
   dependencies:
     isexe "^2.0.0"
 
-widest-line@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c"
-  integrity sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=
-  dependencies:
-    string-width "^1.0.1"
-
-widest-line@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
-  integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
-  dependencies:
-    string-width "^2.1.1"
-
 widest-line@^3.1.0:
   version "3.1.0"
-  resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz"
+  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
   integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
   dependencies:
     string-width "^4.0.0"
 
-windows-release@^3.1.0:
-  version "3.3.3"
-  resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999"
-  integrity sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==
-  dependencies:
-    execa "^1.0.0"
-
-winston-transport@^4.2.0, winston-transport@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
-  integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
-  dependencies:
-    readable-stream "^2.3.7"
-    triple-beam "^1.2.0"
-
-winston@^3.0.0:
-  version "3.3.3"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
-  integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
-  dependencies:
-    "@dabh/diagnostics" "^2.0.2"
-    async "^3.1.0"
-    is-stream "^2.0.0"
-    logform "^2.2.0"
-    one-time "^1.0.0"
-    readable-stream "^3.4.0"
-    stack-trace "0.0.x"
-    triple-beam "^1.3.0"
-    winston-transport "^4.4.0"
-
-with-open-file@^0.1.6:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/with-open-file/-/with-open-file-0.1.7.tgz#e2de8d974e8a8ae6e58886be4fe8e7465b58a729"
-  integrity sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==
-  dependencies:
-    p-finally "^1.0.0"
-    p-try "^2.1.0"
-    pify "^4.0.1"
-
 word-wrap@^1.2.3:
   version "1.2.3"
-  resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
+  resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
-wordwrap@^0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-  integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
-
-wordwrapjs@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e"
-  integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==
-  dependencies:
-    reduce-flatten "^1.0.1"
-    typical "^2.6.1"
-
 wrap-ansi@^7.0.0:
   version "7.0.0"
-  resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
   dependencies:
     ansi-styles "^4.0.0"
@@ -10685,30 +2958,12 @@
 
 wrappy@1:
   version "1.0.2"
-  resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-write-file-atomic@^1.1.2:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
-  integrity sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=
-  dependencies:
-    graceful-fs "^4.1.11"
-    imurmurhash "^0.1.4"
-    slide "^1.1.5"
-
-write-file-atomic@^2.0.0:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
-  integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
-  dependencies:
-    graceful-fs "^4.1.11"
-    imurmurhash "^0.1.4"
-    signal-exit "^3.0.2"
-
 write-file-atomic@^3.0.0, write-file-atomic@^3.0.3:
   version "3.0.3"
-  resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
   integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
   dependencies:
     imurmurhash "^0.1.4"
@@ -10716,189 +2971,17 @@
     signal-exit "^3.0.2"
     typedarray-to-buffer "^3.1.5"
 
-ws@~7.4.2:
-  version "7.4.4"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
-  integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
-
-xdg-basedir@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
-  integrity sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=
-  dependencies:
-    os-homedir "^1.0.0"
-
-xdg-basedir@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
-  integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
-
 xdg-basedir@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
   integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
 
-xmlbuilder@8.2.2:
-  version "8.2.2"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
-  integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=
-
-xmldom@0.1.x:
-  version "0.1.31"
-  resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
-  integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
-
-xmlhttprequest-ssl@~1.5.4:
-  version "1.5.5"
-  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
-  integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
-
-"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
-  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
-yallist@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
-  integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
-
 yallist@^4.0.0:
   version "4.0.0"
-  resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
 yargs-parser@^20.2.3:
-  version "20.2.7"
-  resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz"
-  integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
-
-yauzl@^2.10.0:
-  version "2.10.0"
-  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
-  integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
-  dependencies:
-    buffer-crc32 "~0.2.3"
-    fd-slicer "~1.1.0"
-
-yeast@0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
-  integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
-
-yeoman-environment@^1.5.2:
-  version "1.6.6"
-  resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-1.6.6.tgz#cd85fa67d156060e440d7807d7ef7cf0d2d1d671"
-  integrity sha1-zYX6Z9FWBg5EDXgH1+988NLR1nE=
-  dependencies:
-    chalk "^1.0.0"
-    debug "^2.0.0"
-    diff "^2.1.2"
-    escape-string-regexp "^1.0.2"
-    globby "^4.0.0"
-    grouped-queue "^0.3.0"
-    inquirer "^1.0.2"
-    lodash "^4.11.1"
-    log-symbols "^1.0.1"
-    mem-fs "^1.1.0"
-    text-table "^0.2.0"
-    untildify "^2.0.0"
-
-yeoman-environment@^2.0.5, yeoman-environment@^2.9.5:
-  version "2.10.3"
-  resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.10.3.tgz#9d8f42b77317414434cc0e51fb006a4bdd54688e"
-  integrity sha512-pLIhhU9z/G+kjOXmJ2bPFm3nejfbH+f1fjYRSOteEXDBrv1EoJE/e+kuHixSXfCYfTkxjYsvRaDX+1QykLCnpQ==
-  dependencies:
-    chalk "^2.4.1"
-    debug "^3.1.0"
-    diff "^3.5.0"
-    escape-string-regexp "^1.0.2"
-    execa "^4.0.0"
-    globby "^8.0.1"
-    grouped-queue "^1.1.0"
-    inquirer "^7.1.0"
-    is-scoped "^1.0.0"
-    lodash "^4.17.10"
-    log-symbols "^2.2.0"
-    mem-fs "^1.1.0"
-    mem-fs-editor "^6.0.0"
-    npm-api "^1.0.0"
-    semver "^7.1.3"
-    strip-ansi "^4.0.0"
-    text-table "^0.2.0"
-    untildify "^3.0.3"
-    yeoman-generator "^4.8.2"
-
-yeoman-generator@^3.1.1:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-3.2.0.tgz#02077d2d7ff28fedc1ed7dad7f9967fd7c3604cc"
-  integrity sha512-iR/qb2je3GdXtSfxgvOXxUW0Cp8+C6LaZaNlK2BAICzFNzwHtM10t/QBwz5Ea9nk6xVDQNj4Q889TjCXGuIv8w==
-  dependencies:
-    async "^2.6.0"
-    chalk "^2.3.0"
-    cli-table "^0.3.1"
-    cross-spawn "^6.0.5"
-    dargs "^6.0.0"
-    dateformat "^3.0.3"
-    debug "^4.1.0"
-    detect-conflict "^1.0.0"
-    error "^7.0.2"
-    find-up "^3.0.0"
-    github-username "^4.0.0"
-    istextorbinary "^2.2.1"
-    lodash "^4.17.10"
-    make-dir "^1.1.0"
-    mem-fs-editor "^5.0.0"
-    minimist "^1.2.0"
-    pretty-bytes "^5.1.0"
-    read-chunk "^3.0.0"
-    read-pkg-up "^4.0.0"
-    rimraf "^2.6.2"
-    run-async "^2.0.0"
-    shelljs "^0.8.0"
-    text-table "^0.2.0"
-    through2 "^3.0.0"
-    yeoman-environment "^2.0.5"
-
-yeoman-generator@^4.8.2:
-  version "4.13.0"
-  resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-4.13.0.tgz#a6caeed8491fceea1f84f53e31795f25888b4672"
-  integrity sha512-f2/5N5IR3M2Ozm+QocvZQudlQITv2DwI6Mcxfy7R7gTTzaKgvUpgo/pQMJ+WQKm0KN0YMWCFOZpj0xFGxevc1w==
-  dependencies:
-    async "^2.6.2"
-    chalk "^2.4.2"
-    cli-table "^0.3.1"
-    cross-spawn "^6.0.5"
-    dargs "^6.1.0"
-    dateformat "^3.0.3"
-    debug "^4.1.1"
-    diff "^4.0.1"
-    error "^7.0.2"
-    find-up "^3.0.0"
-    github-username "^3.0.0"
-    istextorbinary "^2.5.1"
-    lodash "^4.17.11"
-    make-dir "^3.0.0"
-    mem-fs-editor "^7.0.1"
-    minimist "^1.2.5"
-    pretty-bytes "^5.2.0"
-    read-chunk "^3.2.0"
-    read-pkg-up "^5.0.0"
-    rimraf "^2.6.3"
-    run-async "^2.0.0"
-    semver "^7.2.1"
-    shelljs "^0.8.4"
-    text-table "^0.2.0"
-    through2 "^3.0.1"
-  optionalDependencies:
-    grouped-queue "^1.1.0"
-    yeoman-environment "^2.9.5"
-
-zip-stream@^2.1.2:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b"
-  integrity sha512-EkXc2JGcKhO5N5aZ7TmuNo45budRaFGHOmz24wtJR7znbNqDPmdZtUauKX6et8KAVseAMBOyWJqEpXcHTBsh7Q==
-  dependencies:
-    archiver-utils "^2.1.0"
-    compress-commons "^2.1.1"
-    readable-stream "^3.4.0"
+  version "20.2.9"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==