Merge branch 'stable-3.2'

* stable-3.2:
  Set version to 3.2.3-SNAPSHOT
  Set version to 3.2.2
  Set version to 3.1.8-SNAPSHOT
  Set version to 3.1.7
  Use Mockito instead of EasyMock for X-Frame-Options header tests
  Set version to 3.0.12-SNAPSHOT
  Set version to 3.0.11
  Set X-Frame-Options header to avoid clickjacking
  PG: Skip unsupported global capabilities
  Revert "Remove documentation of obsolete gerrit.canLoadInIFrame"
  Fix typos in note-db.txt
  Document skipping of reindexing step for offline NoteDB migration
  Report end of NoteDB migration when skipping reindexing
  Clarify that index.batchThreads is relevant for offline reindexing
  Add project to output when reindexing changes in verbose mode
  Auto-flush SiteIndexer's PrintWriters
  Allow to re-index in verbose mode during NoteDB migration
  Avoid closing System.out after All-Users GC in NoteDB migration
  Honor project watches also for changes created via cherry-pick
  Report the index state after re-indexing
  Remove documentation of obsolete gerrit.canLoadInIFrame
  Remove obsolete HostPage.html
  IndexHtmlUtil: Don't log full stack trace when user is not authenticated
  Schema: Show only a single log for inexistent commits
  Schema: Refactor lambda in buildFields to a separate function
  Don't render gr-comment-list when collapsed initially
  ProjectJson: Use merge function for label value rendering
  Simplify Init for Elasticsearch
  Refactor reindexProjects in Init to be general
  Avoid auto-reindex of projects during init when unneeded
  Schema: Show only a single log for inexistent commits
  Schema: Refactor lambda in buildFields to a separate function

Change-Id: Iec0946ea9957a1f5e0898deac9ce2316bc308322
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 4045ad9..1f4fd9c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -828,6 +828,49 @@
 +
 If 0 or negative, disk storage for the cache is disabled.
 
+[[cache.name.expireAfterWrite]]cache.<name>.expireAfterWrite::
++
+Duration after which a cached value will be evicted and not
+read anymore.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
++
+Disabled by default.
+
+[[cache.name.refreshAfterWrite]]cache.<name>.refreshAfterWrite::
++
+Duration after which we asynchronously refresh the cached value.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
++
+This applies only to these caches that support refreshing:
++
+* `"projects"`: Caching project information in-memory. Defaults to 15 minutes.
+
+[[cache.refreshThreadPoolSize]]cache.refreshThreadPoolSize::
++
+Number of threads that are available to refresh cached values that became
+out of date. This applies only to these caches that support refreshing:
++
+* `"projects"`: Caching project information in-memory
++
+Refreshes will only be scheduled on this executor if the values are
+out of sync.
+The check if they are is cheap and always happens on the thread that
+inquires for a cached value.
++
+Defaults to 2.
+
 ==== [[cache_names]]Standard Caches
 
 cache `"accounts"`::
@@ -1125,22 +1168,6 @@
 +
 Default is true, enabled.
 
-[[cache.projects.checkFrequency]]cache.projects.checkFrequency::
-+
-How often project configuration should be checked for update from Git.
-Gerrit Code Review caches project access rules and configuration in
-memory, checking the refs/meta/config branch every checkFrequency
-minutes to see if a new revision should be loaded and used for future
-access. Values can be specified using standard time unit abbreviations
-('ms', 'sec', 'min', etc.).
-+
-If set to 0, checks occur every time, which may slow down operations.
-If set to 'disabled' or 'off', no check will ever be done.
-Administrators may force the cache to flush with
-link:cmd-flush-caches.html[gerrit flush-caches].
-+
-Default is 5 minutes.
-
 [[cache.projects.loadOnStartup]]cache.projects.loadOnStartup::
 +
 If the project cache should be loaded during server startup.
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index aa6e400..01cd494 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -21,7 +21,7 @@
 * A JDK for Java 8|9|10|11|...
 * Python 2 or 3
 * link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm),role=external,window=_blank]
-* Bower (`sudo npm install -g bower`)
+* Bower (`npm install -g bower`)
 * link:https://docs.bazel.build/versions/master/install.html[Bazel,role=external,window=_blank] -launched with
 link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank]
 * Maven
@@ -32,12 +32,11 @@
 [[bazel]]
 === Bazel
 
-link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] includes a
-link:https://bazel.build/[Bazel,role=external,window=_blank] version check and downloads the correct
-`bazel` version for the git project/repository. Bazelisk is the recommended
-`bazel` launcher for Gerrit. Once Bazelisk is installed locally, a `bazel`
-symlink can be created towards it. This is so that every `bazel` command
-seamlessly uses Bazelisk, which then runs the proper `bazel` binary version.
+link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] is a version
+manager for link:https://bazel.build/[Bazel,role=external,window=_blank], similar to how `nvm`
+manages `npm` versions. It takes care of downloading and installing Bazel itself, so you don't have
+to worry about using the correct version of Bazel. Bazelisk can be installed in different
+ways: link:https://docs.bazel.build/install-bazelisk.html[Install,role=external,window=_blank]
 
 [[java]]
 === Java
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 23ecd67..477641b 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -144,7 +144,7 @@
   and everyone can comment and raise concerns.
 * Design docs should stay open for a minimum of 10 calendar days so
   that everyone has a fair chance to join the review.
-* Within 14 calendar days the contributor should hear back from the
+* Within 30 calendar days the contributor should hear back from the
   link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank]
   whether the proposed feature is in scope of the project and if it can
   be accepted.
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt
index e658910..7954c54 100644
--- a/Documentation/dev-design-docs.txt
+++ b/Documentation/dev-design-docs.txt
@@ -132,7 +132,7 @@
 
 For proposed features the contributor should hear back from the
 link:dev-processes.html#steering-committee[engineering steering
-committee] within 14 calendar days whether the proposed feature is in
+committee] within 30 calendar days whether the proposed feature is in
 scope of the project and if it can be accepted.
 
 [[watch-designs]]
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index 3699a18..b7dd259 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -15,7 +15,7 @@
 * ensuring timely design reviews
 * ensuring that new features are compatible with the project vision and
   are well aligned with other features (give feedback on new
-  link:dev-design-docs.html[design docs] within 14 calendar days)
+  link:dev-design-docs.html[design docs] within 30 calendar days)
 * approving/rejecting link:dev-design-docs.html[designs], vetoing new
   features
 * assigning link:dev-roles.html#mentor[mentors] for approved features
diff --git a/Documentation/intro-gerrit-walkthrough.txt b/Documentation/intro-gerrit-walkthrough.txt
index 92732d0..6565ba4 100644
--- a/Documentation/intro-gerrit-walkthrough.txt
+++ b/Documentation/intro-gerrit-walkthrough.txt
@@ -296,6 +296,10 @@
 * Review the link:intro-project-owner.html[Project Owners guide] to learn more
   about configuring projects in Gerrit, including setting user permissions and
   configuring verification checks
+* Read through the Git and Gerrit training slides that explain concepts and
+  workflows in detail. They are meant for self-studying how Git and Gerrit work:
+** link:https://docs.google.com/presentation/d/1IQCRPHEIX-qKo7QFxsD3V62yhyGA9_5YsYXFOiBpgkk/edit?usp=sharing[Git explained: Git Concepts and Workflows]
+** link:https://docs.google.com/presentation/d/1C73UgQdzZDw0gzpaEqIC6SPujZJhqamyqO1XOHjH-uk/edit?usp=sharing[Gerrit explained: Gerrit Concepts and Workflows]
 
 GERRIT
 ------
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index bd26190..956a94d 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -279,36 +279,6 @@
 ----
 
 
-[[es6-promise]]
-es6-promise
-
-* es6-promise
-
-[[es6-promise_license]]
-----
-Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
 [[Polymer-2018]]
 Polymer-2018
 
@@ -747,70 +717,6 @@
 ----
 
 
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-fetch_license]]
-----
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
-[[moment]]
-moment
-
-* moment
-
-[[moment_license]]
-----
-Copyright (c) JS Foundation and other contributors
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 63c569a..d561596 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3221,36 +3221,6 @@
 ----
 
 
-[[es6-promise]]
-es6-promise
-
-* es6-promise
-
-[[es6-promise_license]]
-----
-Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
 [[Polymer-2018]]
 Polymer-2018
 
@@ -3689,70 +3659,6 @@
 ----
 
 
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-fetch_license]]
-----
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
-[[moment]]
-moment
-
-* moment
-
-[[moment_license]]
-----
-Copyright (c) JS Foundation and other contributors
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 7e6799be..3040348 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -64,6 +64,7 @@
 * `caches/memory_eviction_count`: Memory eviction count.
 * `caches/disk_cached`: Disk entries used by persistent 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.
 
 === Change
 
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 1ce1d61..adb5d20 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -11,34 +11,27 @@
 [[loading]]
 == Plugin loading and initialization
 
-link:js-api.html#_entry_point[Entry point] for the plugin and the loading method
-is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports,role=external,window=_blank]
-spec.
+link:js-api.html#_entry_point[Entry point] for the plugin.
 
-* The plugin provides pluginname.html, and can be a standalone file or a static
+* The plugin provides pluginname.js, and can be a standalone file or a static
   asset in a jar as a link:dev-plugins.html#deployment[Web UI plugin].
-* pluginname.html contains a `dom-module` tag with a script that uses
-  `Gerrit.install()`. There should only be single `Gerrit.install()` per file.
-* PolyGerrit imports pluginname.html along with all required resources defined in it
-  (fonts, styles, etc).
-* For standalone plugins, the entry point file is a `pluginname.html` file
+* pluginname.js contains a call to `Gerrit.install()`. There should
+  only be single `Gerrit.install()` per file.
+* PolyGerrit imports pluginname.js.
+* For standalone plugins, the entry point file is a `pluginname.js` file
   located in `gerrit-site/plugins` folder, where `pluginname` is an alphanumeric
   plugin name.
 
 Note: Code examples target modern browsers (Chrome, Firefox, Safari, Edge).
 
-Here's a recommended starter `myplugin.html`:
+Here's a recommended starter `myplugin.js`:
 
-``` html
-<dom-module id="my-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      'use strict';
+``` js
+Gerrit.install(plugin => {
+  'use strict';
 
-      // Your code here.
-    });
-  </script>
-</dom-module>
+  // Your code here.
+});
 ```
 
 [[low-level-api-concepts]]
@@ -103,34 +96,31 @@
 === Styling DOM Elements
 
 A plugin may provide Polymer's
-https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules[style
+https://polymer-library.polymer-project.org/3.0/docs/devguide/style-shadow-dom[style
 modules,role=external,window=_blank] to style individual endpoints using
 `plugin.registerStyleModule(endpointName, moduleName)`. A style must be defined
-as a standalone `<dom-module>` defined in the same .html file.
+as a standalone `<dom-module>` defined in the same .js file.
+
+See `samples/theme-plugin.js` for examples.
 
 Note: TODO: Insert link to the full styling API.
 
-``` html
-<dom-module id="my-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      plugin.registerStyleModule('change-metadata', 'some-style-module');
-    });
-  </script>
-</dom-module>
-
-<dom-module id="some-style-module">
-  <style>
+``` js
+const styleElement = document.createElement('dom-module');
+styleElement.innerHTML =
+ `<template>
+    <style>
     html {
-      --change-metadata-label-status: {
-        display: none;
-      }
-      --change-metadata-strategy: {
-        display: none;
-      }
+      --primary-text-color: red;
     }
-  </style>
-</dom-module>
+   </style>
+ </template>`;
+
+styleElement.register('some-style-module');
+
+Gerrit.install(plugin => {
+  plugin.registerStyleModule('change-metadata', 'some-style-module');
+});
 ```
 
 [[high-level-api-concepts]]
@@ -152,11 +142,11 @@
 `plugin.attributeHelper(element)`
 
 Alternative for
-link:https://www.polymer-project.org/1.0/docs/devguide/data-binding[Polymer data
+link:https://polymer-library.polymer-project.org/3.0/docs/devguide/data-binding[Polymer data
 binding,role=external,window=_blank] for plugins that don't use Polymer. Can be used to bind element
 attribute changes to callbacks.
 
-See `samples/bind-parameters.html` for examples on both Polymer data bindings
+See `samples/bind-parameters.js` for examples on both Polymer data bindings
 and `attibuteHelper` usage.
 
 === eventHelper
@@ -253,26 +243,29 @@
 
 Here's the recommended approach that uses Polymer for generating custom elements:
 
-``` html
-<dom-module id="some-plugin">
-  <script>
-    Gerrit.install(plugin => {
-      plugin.registerCustomComponent(
-        'change-view-integration', 'some-ci-module');
-    });
-  </script>
-</dom-module>
+``` js
+class SomeCiModule extends Polymer.Element {
+  static get is() {
+    return "some-ci-module";
+  }
+  static get template() {
+    return Polymer.html`
+      Sample link: <a href="http://some.com/foo">Foo</a>
+    `;
+  }
+}
 
-<dom-module id="some-ci-module">
-  <template>
-    Sample link: <a href="http://some.com/foo">Foo</a>
-  </template>
-  <script>
-    Polymer({is: 'some-ci-module'});
-  </script>
-</dom-module>
+// Register this element
+customElements.define(SomeCiModule.is, SomeCiModule);
+
+// Install the plugin
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent('change-view-integration', 'some-ci-module');
+});
 ```
 
+See `samples/` for more examples.
+
 Here's a minimal example that uses low-level DOM Hooks API for the same purpose:
 
 ``` js
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index da545d0..ad543b9 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5731,6 +5731,11 @@
 The list of commits that are being integrated into the destination
 branch by submitting the merge commit.
 
+* `/PATCHSET_LEVEL`:
++
+This file path is used exclusively for posting and indicating
+patchset-level comments, thus not relevant for other use-cases.
+
 [[fix-id]]
 === \{fix-id\}
 UUID of a suggested fix.
@@ -6226,7 +6231,7 @@
 comments may be returned for multiple patch sets.
 |`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
-The path of the file for which the inline comment was done. +
+link:#file-id[The file path] for which the inline comment was done. +
 Not set if returned in a map where the key is the file path.
 |`side`        |optional|
 The side on which the comment was added. +
@@ -6279,7 +6284,7 @@
 The URL encoded UUID of the comment if an existing draft comment should
 be updated.
 |`path`        |optional|
-The path of the file for which the inline comment should be added. +
+link:#file-id[The file path] for which the inline comment should be added. +
 Doesn't need to be set if contained in a map where the key is the file
 path.
 |`side`        |optional|
diff --git a/README.md b/README.md
index a76dac6..084f2b0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,8 @@
 [Gerrit](https://www.gerritcodereview.com) is a code review and project
 management tool for Git based projects.
 
-[![Build Status](https://gerrit-ci.gerritforge.com/job/Gerrit-master/badge/icon)](https://gerrit-ci.gerritforge.com/job/Gerrit-master/)
+[![Build Status](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-master/badge/icon)](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-master/)
+![Maven Central](https://img.shields.io/maven-central/v/com.google.gerrit/gerrit-war)
 
 ## Objective
 
diff --git a/WORKSPACE b/WORKSPACE
index 060cf31..91eef76 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -60,8 +60,8 @@
 
 http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "d0c4bb8b902c1658f42eb5563809c70a06e46015d64057d25560b0eb4bdc9007",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.5.0/rules_nodejs-1.5.0.tar.gz"],
+    sha256 = "84abf7ac4234a70924628baa9a73a5a5cbad944c4358cf9abdb4aab29c9a5b77",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.7.0/rules_nodejs-1.7.0.tar.gz"],
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -82,6 +82,7 @@
 # https://github.com/google/closure-templates/pull/155
 rules_closure_dependencies(
     omit_aopalliance = True,
+    omit_bazel_skylib = True,
     omit_javax_inject = True,
     omit_rules_cc = True,
 )
@@ -91,10 +92,10 @@
 # Golang support for PolyGerrit local dev server.
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "b34cbe1a7514f5f5487c3bfee7340a4496713ddf4f119f7a225583d6cafd793a",
+    sha256 = "a8d6b1b354d371a646d2f7927319974e0f9e52f73a2452d2b3877118169eb6bb",
     urls = [
-        "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
     ],
 )
 
@@ -106,8 +107,11 @@
 
 http_archive(
     name = "bazel_gazelle",
-    sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
-    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"],
+    sha256 = "cdb02a887a7187ea4d5a27452311a75ed8637379a1287d8eeb952138ea485f7d",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+    ],
 )
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 541e479..9d8bc57 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -39,6 +39,7 @@
     "//lib:gson",
     "//lib:guava-retrying",
     "//lib:jgit",
+    "//lib:jgit-ssh-jsch",
     "//lib:jsch",
     "//lib/commons:compress",
     "//lib/commons:lang",
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 0ef6ad5..2d62608 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -483,7 +483,6 @@
     cfg.setString("gerrit", null, "basePath", "git");
     cfg.setBoolean("sendemail", null, "enable", true);
     cfg.setInt("sendemail", null, "threadPoolSize", 0);
-    cfg.setInt("cache", "projects", "checkFrequency", 0);
     cfg.setInt("plugins", null, "checkFrequency", 0);
 
     cfg.setInt("sshd", null, "threads", 1);
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index 50f86fe..3926d47 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -35,6 +35,12 @@
   public static final String MERGE_LIST = "/MERGE_LIST";
 
   /**
+   * Magical file name which doesn't represent a file. Used specifically for patchset-level
+   * comments.
+   */
+  public static final String PATCHSET_LEVEL = "/PATCHSET_LEVEL";
+
+  /**
    * Checks if the given path represents a magic file. A magic file is a generated file that is
    * automatically included into changes. It does not exist in the commit of the patch set.
    *
@@ -42,7 +48,7 @@
    * @return {@code true} if the path represents a magic file, otherwise {@code false}.
    */
   public static boolean isMagic(String path) {
-    return COMMIT_MSG.equals(path) || MERGE_LIST.equals(path);
+    return COMMIT_MSG.equals(path) || MERGE_LIST.equals(path) || PATCHSET_LEVEL.equals(path);
   }
 
   public static Key key(PatchSet.Id patchSetId, String fileName) {
diff --git a/java/com/google/gerrit/extensions/annotations/RemoveAfter.java b/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
index aa31dd0..02f70e9 100644
--- a/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
+++ b/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.extensions.annotations;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.inject.BindingAnnotation;
 import java.lang.annotation.ElementType;
@@ -26,7 +26,7 @@
  * period we promised to users.
  */
 @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
-@Retention(SOURCE)
+@Retention(RUNTIME)
 @BindingAnnotation
 public @interface RemoveAfter {
   /**
diff --git a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
index 0698735..dd226ed 100644
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
@@ -53,6 +54,11 @@
         .thatCustom(robotCommentInfo.fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
   }
 
+  public StringSubject path() {
+    isNotNull();
+    return check("path").that(robotCommentInfo.path);
+  }
+
   public FixSuggestionInfoSubject onlyFixSuggestion() {
     return fixSuggestions().onlyElement();
   }
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 05992d4..0c3b7b0 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -246,7 +246,7 @@
   }
 
   private boolean sshdOff() {
-    return new SshAddressesModule().getListenAddresses(config).isEmpty();
+    return new SshAddressesModule().provideListenAddresses(config).isEmpty();
   }
 
   private Injector createCfgInjector() {
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 034e042e..57bec71 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -380,7 +380,7 @@
   }
 
   private boolean sshdOff() {
-    return new SshAddressesModule().getListenAddresses(config).isEmpty();
+    return new SshAddressesModule().provideListenAddresses(config).isEmpty();
   }
 
   private String myVersion() {
diff --git a/java/com/google/gerrit/server/CacheRefreshExecutor.java b/java/com/google/gerrit/server/CacheRefreshExecutor.java
new file mode 100644
index 0000000..1a377c3
--- /dev/null
+++ b/java/com/google/gerrit/server/CacheRefreshExecutor.java
@@ -0,0 +1,28 @@
+// 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.
+
+package com.google.gerrit.server;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on the global {@link java.util.concurrent.ThreadPoolExecutor} used to refresh outdated
+ * values in caches.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface CacheRefreshExecutor {}
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index dd48b93..32edadb 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -35,34 +35,38 @@
 @Singleton
 public class ChangeMessagesUtil {
   public static final String AUTOGENERATED_TAG_PREFIX = "autogenerated:";
+  public static final String AUTOGENERATED_BY_GERRIT_TAG_PREFIX =
+      AUTOGENERATED_TAG_PREFIX + "gerrit:";
 
-  public static final String TAG_ABANDON = AUTOGENERATED_TAG_PREFIX + "gerrit:abandon";
+  public static final String TAG_ABANDON = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "abandon";
   public static final String TAG_CHERRY_PICK_CHANGE =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:cherryPickChange";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "cherryPickChange";
   public static final String TAG_DELETE_ASSIGNEE =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:deleteAssignee";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteAssignee";
   public static final String TAG_DELETE_REVIEWER =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:deleteReviewer";
-  public static final String TAG_DELETE_VOTE = AUTOGENERATED_TAG_PREFIX + "gerrit:deleteVote";
-  public static final String TAG_MERGED = AUTOGENERATED_TAG_PREFIX + "gerrit:merged";
-  public static final String TAG_MOVE = AUTOGENERATED_TAG_PREFIX + "gerrit:move";
-  public static final String TAG_RESTORE = AUTOGENERATED_TAG_PREFIX + "gerrit:restore";
-  public static final String TAG_REVERT = AUTOGENERATED_TAG_PREFIX + "gerrit:revert";
-  public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_TAG_PREFIX + "gerrit:setAssignee";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteReviewer";
+  public static final String TAG_DELETE_VOTE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteVote";
+  public static final String TAG_MERGED = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "merged";
+  public static final String TAG_MOVE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "move";
+  public static final String TAG_RESTORE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "restore";
+  public static final String TAG_REVERT = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "revert";
+  public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setAssignee";
   public static final String TAG_UPDATE_ATTENTION_SET =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:updateAttentionSet";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "updateAttentionSet";
   public static final String TAG_SET_DESCRIPTION =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:setPsDescription";
-  public static final String TAG_SET_HASHTAGS = AUTOGENERATED_TAG_PREFIX + "gerrit:setHashtag";
-  public static final String TAG_SET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:setPrivate";
-  public static final String TAG_SET_READY = AUTOGENERATED_TAG_PREFIX + "gerrit:setReadyForReview";
-  public static final String TAG_SET_TOPIC = AUTOGENERATED_TAG_PREFIX + "gerrit:setTopic";
-  public static final String TAG_SET_WIP = AUTOGENERATED_TAG_PREFIX + "gerrit:setWorkInProgress";
-  public static final String TAG_UNSET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:unsetPrivate";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setPsDescription";
+  public static final String TAG_SET_HASHTAGS = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setHashtag";
+  public static final String TAG_SET_PRIVATE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setPrivate";
+  public static final String TAG_SET_READY =
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setReadyForReview";
+  public static final String TAG_SET_TOPIC = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setTopic";
+  public static final String TAG_SET_WIP = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setWorkInProgress";
+  public static final String TAG_UNSET_PRIVATE =
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "unsetPrivate";
   public static final String TAG_UPLOADED_PATCH_SET =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:newPatchSet";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "newPatchSet";
   public static final String TAG_UPLOADED_WIP_PATCH_SET =
-      AUTOGENERATED_TAG_PREFIX + "gerrit:newWipPatchSet";
+      AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "newWipPatchSet";
 
   public static ChangeMessage newMessage(ChangeContext ctx, String body, @Nullable String tag) {
     return newMessage(ctx.getChange().currentPatchSetId(), ctx.getUser(), ctx.getWhen(), body, tag);
@@ -122,6 +126,10 @@
     return tag != null && tag.startsWith(AUTOGENERATED_TAG_PREFIX);
   }
 
+  public static boolean isAutogeneratedByGerrit(@Nullable String tag) {
+    return tag != null && tag.startsWith(AUTOGENERATED_BY_GERRIT_TAG_PREFIX);
+  }
+
   public static ChangeMessageInfo createChangeMessageInfo(
       ChangeMessage message, AccountLoader accountLoader) {
     PatchSet.Id patchNum = message.getPatchSetId();
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index e9ba72d..e8b44aa 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -242,7 +242,9 @@
    * @param changeMessages list of change messages
    */
   public static void linkCommentsToChangeMessages(
-      List<? extends CommentInfo> comments, List<ChangeMessage> changeMessages) {
+      List<? extends CommentInfo> comments,
+      List<ChangeMessage> changeMessages,
+      boolean skipAutoGeneratedMessages) {
     ArrayList<ChangeMessage> sortedChangeMessages =
         changeMessages.stream()
             .sorted(comparing(ChangeMessage::getWrittenOn))
@@ -257,7 +259,7 @@
       // message in timestamp
       while (cmItr < sortedChangeMessages.size()) {
         ChangeMessage cm = sortedChangeMessages.get(cmItr);
-        if (isAfter(comment, cm) || skipChangeMessage(cm)) {
+        if (isAfter(comment, cm) || (skipAutoGeneratedMessages && isAutoGenerated(cm))) {
           cmItr += 1;
         } else {
           break;
@@ -269,8 +271,10 @@
     }
   }
 
-  private static boolean skipChangeMessage(ChangeMessage cm) {
-    return ChangeMessagesUtil.isAutogenerated(cm.getTag());
+  private static boolean isAutoGenerated(ChangeMessage cm) {
+    // Ignore Gerrit auto-generated messages, allowing to link against human change messages that
+    // have an auto-generated tag
+    return ChangeMessagesUtil.isAutogeneratedByGerrit(cm.getTag());
   }
 
   private static boolean isAfter(CommentInfo c, ChangeMessage cm) {
diff --git a/java/com/google/gerrit/server/PublishCommentsOp.java b/java/com/google/gerrit/server/PublishCommentsOp.java
index df57629..745755b 100644
--- a/java/com/google/gerrit/server/PublishCommentsOp.java
+++ b/java/com/google/gerrit/server/PublishCommentsOp.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.change.EmailReviewComments;
@@ -31,6 +32,7 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.CommentsRejectedException;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoView;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -114,7 +116,16 @@
     PatchSet ps = psUtil.get(changeNotes, psId);
     NotifyResolver.Result notify = ctx.getNotify(changeNotes.getChangeId());
     if (notify.shouldNotify()) {
-      email.create(notify, changeNotes, ps, user, message, comments, null, labelDelta).sendAsync();
+      RepoView repoView;
+      try {
+        repoView = ctx.getRepoView();
+      } catch (IOException ex) {
+        throw new StorageException(
+            String.format("Repository %s not found", ctx.getProject().get()), ex);
+      }
+      email
+          .create(notify, changeNotes, ps, user, message, comments, null, labelDelta, repoView)
+          .sendAsync();
     }
     commentAdded.fire(
         changeNotes.getChange(),
diff --git a/java/com/google/gerrit/server/account/StoredPreferences.java b/java/com/google/gerrit/server/account/StoredPreferences.java
index 1b3ff40..573c619 100644
--- a/java/com/google/gerrit/server/account/StoredPreferences.java
+++ b/java/com/google/gerrit/server/account/StoredPreferences.java
@@ -15,19 +15,13 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.skipField;
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
 import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -36,13 +30,12 @@
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.gerrit.server.config.VersionedDefaultPreferences;
 import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import java.io.IOException;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -76,8 +69,6 @@
  * <p>The preferences are lazily parsed.
  */
 public class StoredPreferences {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   public static final String PREFERENCES_CONFIG = "preferences.config";
 
   private final Account.Id accountId;
@@ -141,7 +132,7 @@
           UserConfigSections.GENERAL,
           null,
           mergedGeneralPreferencesInput,
-          parseDefaultGeneralPreferences(defaultCfg, null));
+          PreferencesParserUtil.parseDefaultGeneralPreferences(defaultCfg, null));
       setChangeTable(cfg, mergedGeneralPreferencesInput.changeTable);
       setMy(cfg, mergedGeneralPreferencesInput.my);
 
@@ -158,7 +149,7 @@
           UserConfigSections.DIFF,
           null,
           mergedDiffPreferencesInput,
-          parseDefaultDiffPreferences(defaultCfg, null));
+          PreferencesParserUtil.parseDefaultDiffPreferences(defaultCfg, null));
 
       // evict the cached diff preferences
       this.diffPreferences = null;
@@ -173,7 +164,7 @@
           UserConfigSections.EDIT,
           null,
           mergedEditPreferencesInput,
-          parseDefaultEditPreferences(defaultCfg, null));
+          PreferencesParserUtil.parseDefaultEditPreferences(defaultCfg, null));
 
       // evict the cached edit preferences
       this.editPreferences = null;
@@ -189,7 +180,7 @@
 
   private GeneralPreferencesInfo parseGeneralPreferences(@Nullable GeneralPreferencesInfo input) {
     try {
-      return parseGeneralPreferences(cfg, defaultCfg, input);
+      return PreferencesParserUtil.parseGeneralPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -203,7 +194,7 @@
 
   private DiffPreferencesInfo parseDiffPreferences(@Nullable DiffPreferencesInfo input) {
     try {
-      return parseDiffPreferences(cfg, defaultCfg, input);
+      return PreferencesParserUtil.parseDiffPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -216,7 +207,7 @@
 
   private EditPreferencesInfo parseEditPreferences(@Nullable EditPreferencesInfo input) {
     try {
-      return parseEditPreferences(cfg, defaultCfg, input);
+      return PreferencesParserUtil.parseEditPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -227,218 +218,6 @@
     }
   }
 
-  /**
-   * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
-   * the server's default configs and {@code cfg} for the user's config. These configs are then
-   * overlaid to inherit values (default -> user -> input (if provided).
-   */
-  public static GeneralPreferencesInfo parseGeneralPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
-      throws ConfigInvalidException {
-    GeneralPreferencesInfo r =
-        loadSection(
-            cfg,
-            UserConfigSections.GENERAL,
-            null,
-            new GeneralPreferencesInfo(),
-            defaultCfg != null
-                ? parseDefaultGeneralPreferences(defaultCfg, input)
-                : GeneralPreferencesInfo.defaults(),
-            input);
-    if (input != null) {
-      r.changeTable = input.changeTable;
-      r.my = input.my;
-    } else {
-      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
-      r.my = parseMyMenus(cfg, defaultCfg);
-    }
-    return r;
-  }
-
-  /**
-   * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
-   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
-   * to inherit values (default -> user -> input (if provided).
-   */
-  public static DiffPreferencesInfo parseDiffPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
-      throws ConfigInvalidException {
-    return loadSection(
-        cfg,
-        UserConfigSections.DIFF,
-        null,
-        new DiffPreferencesInfo(),
-        defaultCfg != null
-            ? parseDefaultDiffPreferences(defaultCfg, input)
-            : DiffPreferencesInfo.defaults(),
-        input);
-  }
-
-  /**
-   * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
-   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
-   * to inherit values (default -> user -> input (if provided).
-   */
-  public static EditPreferencesInfo parseEditPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
-      throws ConfigInvalidException {
-    return loadSection(
-        cfg,
-        UserConfigSections.EDIT,
-        null,
-        new EditPreferencesInfo(),
-        defaultCfg != null
-            ? parseDefaultEditPreferences(defaultCfg, input)
-            : EditPreferencesInfo.defaults(),
-        input);
-  }
-
-  private static GeneralPreferencesInfo parseDefaultGeneralPreferences(
-      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
-    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.GENERAL,
-        null,
-        allUserPrefs,
-        GeneralPreferencesInfo.defaults(),
-        input);
-    return updateGeneralPreferencesDefaults(allUserPrefs);
-  }
-
-  private static DiffPreferencesInfo parseDefaultDiffPreferences(
-      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
-    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.DIFF,
-        null,
-        allUserPrefs,
-        DiffPreferencesInfo.defaults(),
-        input);
-    return updateDiffPreferencesDefaults(allUserPrefs);
-  }
-
-  private static EditPreferencesInfo parseDefaultEditPreferences(
-      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
-    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.EDIT,
-        null,
-        allUserPrefs,
-        EditPreferencesInfo.defaults(),
-        input);
-    return updateEditPreferencesDefaults(allUserPrefs);
-  }
-
-  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
-      GeneralPreferencesInfo input) {
-    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
-      return GeneralPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
-    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
-      return DiffPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
-    EditPreferencesInfo result = EditPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
-      return EditPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
-    List<String> changeTable = changeTable(cfg);
-    if (changeTable == null && defaultCfg != null) {
-      changeTable = changeTable(defaultCfg);
-    }
-    return changeTable;
-  }
-
-  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
-    List<MenuItem> my = my(cfg);
-    if (my.isEmpty() && defaultCfg != null) {
-      my = my(defaultCfg);
-    }
-    if (my.isEmpty()) {
-      my.add(new MenuItem("Dashboard", "#/dashboard/self", null));
-      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
-      my.add(new MenuItem("Edits", "#/q/has:edit", null));
-      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
-      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
-      my.add(new MenuItem("Groups", "#/settings/#Groups", null));
-    }
-    return my;
-  }
-
-  public static GeneralPreferencesInfo readDefaultGeneralPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseGeneralPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
-
-  public static DiffPreferencesInfo readDefaultDiffPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseDiffPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
-
-  public static EditPreferencesInfo readDefaultEditPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseEditPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
-
-  static Config readDefaultConfig(AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
-    defaultPrefs.load(allUsersName, allUsersRepo);
-    return defaultPrefs.getConfig();
-  }
-
   public static GeneralPreferencesInfo updateDefaultGeneralPreferences(
       MetaDataUpdate md, GeneralPreferencesInfo input) throws IOException, ConfigInvalidException {
     VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
@@ -453,7 +232,7 @@
     setChangeTable(defaultPrefs.getConfig(), input.changeTable);
     defaultPrefs.commit(md);
 
-    return parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
+    return PreferencesParserUtil.parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
   }
 
   public static DiffPreferencesInfo updateDefaultDiffPreferences(
@@ -468,7 +247,7 @@
         DiffPreferencesInfo.defaults());
     defaultPrefs.commit(md);
 
-    return parseDiffPreferences(defaultPrefs.getConfig(), null, null);
+    return PreferencesParserUtil.parseDiffPreferences(defaultPrefs.getConfig(), null, null);
   }
 
   public static EditPreferencesInfo updateDefaultEditPreferences(
@@ -483,11 +262,24 @@
         EditPreferencesInfo.defaults());
     defaultPrefs.commit(md);
 
-    return parseEditPreferences(defaultPrefs.getConfig(), null, null);
+    return PreferencesParserUtil.parseEditPreferences(defaultPrefs.getConfig(), null, null);
   }
 
-  private static List<String> changeTable(Config cfg) {
-    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  public static void validateMy(List<MenuItem> my) throws BadRequestException {
+    if (my == null) {
+      return;
+    }
+    for (MenuItem item : my) {
+      checkRequiredMenuItemField(item.name, "name");
+      checkRequiredMenuItemField(item.url, "URL");
+    }
+  }
+
+  static Config readDefaultConfig(AllUsersName allUsersName, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(allUsersName, allUsersRepo);
+    return defaultPrefs.getConfig();
   }
 
   private static void setChangeTable(Config cfg, List<String> changeTable) {
@@ -497,21 +289,6 @@
     }
   }
 
-  private static List<MenuItem> my(Config cfg) {
-    List<MenuItem> my = new ArrayList<>();
-    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
-      String url = my(cfg, subsection, KEY_URL, "#/");
-      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
-      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
-    }
-    return my;
-  }
-
-  private static String my(Config cfg, String subsection, String key, String defaultValue) {
-    String val = cfg.getString(UserConfigSections.MY, subsection, key);
-    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
-  }
-
   private static void setMy(Config cfg, List<MenuItem> my) {
     if (my != null) {
       unsetSection(cfg, UserConfigSections.MY);
@@ -526,16 +303,6 @@
     }
   }
 
-  public static void validateMy(List<MenuItem> my) throws BadRequestException {
-    if (my == null) {
-      return;
-    }
-    for (MenuItem item : my) {
-      checkRequiredMenuItemField(item.name, "name");
-      checkRequiredMenuItemField(item.url, "URL");
-    }
-  }
-
   private static void checkRequiredMenuItemField(String value, String name)
       throws BadRequestException {
     if (isNullOrEmpty(value)) {
diff --git a/java/com/google/gerrit/server/cache/CacheBinding.java b/java/com/google/gerrit/server/cache/CacheBinding.java
index 9d90d073..99db64e 100644
--- a/java/com/google/gerrit/server/cache/CacheBinding.java
+++ b/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -29,6 +29,13 @@
   /** Set the time an element lives after last access before being expired. */
   CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
 
+  /**
+   * Set the time that an element will be refreshed after. Elements older than this but younger than
+   * {@link #expireAfterWrite(Duration)} will still be returned, but on access a task is queued to
+   * refresh their value asynchronously.
+   */
+  CacheBinding<K, V> refreshAfterWrite(Duration duration);
+
   /** Populate the cache with items from the CacheLoader. */
   CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
 
diff --git a/java/com/google/gerrit/server/cache/CacheDef.java b/java/com/google/gerrit/server/cache/CacheDef.java
index d0c633e..31a453e 100644
--- a/java/com/google/gerrit/server/cache/CacheDef.java
+++ b/java/com/google/gerrit/server/cache/CacheDef.java
@@ -51,6 +51,9 @@
   Duration expireFromMemoryAfterAccess();
 
   @Nullable
+  Duration refreshAfterWrite();
+
+  @Nullable
   Weigher<K, V> weigher();
 
   @Nullable
diff --git a/java/com/google/gerrit/server/cache/CacheProvider.java b/java/com/google/gerrit/server/cache/CacheProvider.java
index fe4244c..2dd9e1f 100644
--- a/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -38,6 +38,7 @@
   private long maximumWeight;
   private Duration expireAfterWrite;
   private Duration expireFromMemoryAfterAccess;
+  private Duration refreshAfterWrite;
   private Provider<CacheLoader<K, V>> loader;
   private Provider<Weigher<K, V>> weigher;
 
@@ -90,6 +91,13 @@
   }
 
   @Override
+  public CacheBinding<K, V> refreshAfterWrite(Duration duration) {
+    checkNotFrozen();
+    refreshAfterWrite = duration;
+    return this;
+  }
+
+  @Override
   public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> impl) {
     checkNotFrozen();
     loader = module.bindCacheLoader(this, impl);
@@ -151,6 +159,11 @@
   }
 
   @Override
+  public Duration refreshAfterWrite() {
+    return refreshAfterWrite;
+  }
+
+  @Override
   @Nullable
   public Weigher<K, V> weigher() {
     return weigher != null ? weigher.get() : null;
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index 5d9ce60..aa62745 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -43,6 +43,11 @@
   }
 
   @Override
+  public Duration refreshAfterWrite() {
+    return source.refreshAfterWrite();
+  }
+
+  @Override
   public Weigher<K, V> weigher() {
     Weigher<K, V> weigher = source.weigher();
     if (weigher == null) {
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 8f7e360..82615a4 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -237,6 +237,7 @@
         def.valueSerializer(),
         def.version(),
         maxSize,
-        def.expireAfterWrite());
+        def.expireAfterWrite(),
+        def.expireFromMemoryAfterAccess());
   }
 }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index ef4e44c..7a53600 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -23,6 +23,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.BloomFilter;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
@@ -40,6 +43,7 @@
 import java.sql.Statement;
 import java.sql.Timestamp;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Calendar;
 import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -122,7 +126,12 @@
   @Override
   public V get(K key) throws ExecutionException {
     if (mem instanceof LoadingCache) {
-      return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+      LoadingCache<K, ValueHolder<V>> asLoadingCache = (LoadingCache<K, ValueHolder<V>>) mem;
+      ValueHolder<V> valueHolder = asLoadingCache.get(key);
+      if (store.needsRefresh(valueHolder.created)) {
+        asLoadingCache.refresh(key);
+      }
+      return valueHolder.value;
     }
     throw new UnsupportedOperationException();
   }
@@ -139,8 +148,8 @@
                 }
               }
 
-              ValueHolder<V> h = new ValueHolder<>(valueLoader.call());
-              h.created = TimeUtil.nowMs();
+              ValueHolder<V> h =
+                  new ValueHolder<>(valueLoader.call(), Instant.ofEpochMilli(TimeUtil.nowMs()));
               executor.execute(() -> store.put(key, h));
               return h;
             })
@@ -149,8 +158,7 @@
 
   @Override
   public void put(K key, V val) {
-    final ValueHolder<V> h = new ValueHolder<>(val);
-    h.created = TimeUtil.nowMs();
+    final ValueHolder<V> h = new ValueHolder<>(val, Instant.ofEpochMilli(TimeUtil.nowMs()));
     mem.put(key, h);
     executor.execute(() -> store.put(key, h));
   }
@@ -217,11 +225,12 @@
 
   static class ValueHolder<V> {
     final V value;
-    long created;
+    final Instant created;
     volatile boolean clean;
 
-    ValueHolder(V value) {
+    ValueHolder(V value, Instant created) {
       this.value = value;
+      this.created = created;
     }
   }
 
@@ -248,12 +257,34 @@
           }
         }
 
-        final ValueHolder<V> h = new ValueHolder<>(loader.load(key));
-        h.created = TimeUtil.nowMs();
+        final ValueHolder<V> h =
+            new ValueHolder<>(loader.load(key), Instant.ofEpochMilli(TimeUtil.nowMs()));
         executor.execute(() -> store.put(key, h));
         return h;
       }
     }
+
+    @Override
+    public ListenableFuture<ValueHolder<V>> reload(K key, ValueHolder<V> oldValue)
+        throws Exception {
+      ListenableFuture<V> reloadedValue = loader.reload(key, oldValue.value);
+      Futures.addCallback(
+          reloadedValue,
+          new FutureCallback<V>() {
+            @Override
+            public void onSuccess(V result) {
+              store.put(key, new ValueHolder<>(result, TimeUtil.now()));
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+              logger.atWarning().withCause(t).log("Unable to reload cache value");
+            }
+          },
+          executor);
+
+      return Futures.transform(reloadedValue, v -> new ValueHolder<>(v, TimeUtil.now()), executor);
+    }
   }
 
   static class SqlStore<K, V> {
@@ -263,6 +294,7 @@
     private final int version;
     private final long maxSize;
     @Nullable private final Duration expireAfterWrite;
+    @Nullable private final Duration refreshAfterWrite;
     private final BlockingQueue<SqlHandle> handles;
     private final AtomicLong hitCount = new AtomicLong();
     private final AtomicLong missCount = new AtomicLong();
@@ -276,13 +308,15 @@
         CacheSerializer<V> valueSerializer,
         int version,
         long maxSize,
-        @Nullable Duration expireAfterWrite) {
+        @Nullable Duration expireAfterWrite,
+        @Nullable Duration refreshAfterWrite) {
       this.url = jdbcUrl;
       this.keyType = createKeyType(keyType, keySerializer);
       this.valueSerializer = valueSerializer;
       this.version = version;
       this.maxSize = maxSize;
       this.expireAfterWrite = expireAfterWrite;
+      this.refreshAfterWrite = refreshAfterWrite;
 
       int cores = Runtime.getRuntime().availableProcessors();
       int keep = Math.min(cores, 16);
@@ -394,14 +428,14 @@
           }
 
           Timestamp created = r.getTimestamp(2);
-          if (expired(created)) {
+          if (expired(created.toInstant())) {
             invalidate(key);
             missCount.incrementAndGet();
             return null;
           }
 
           V val = valueSerializer.deserialize(r.getBytes(1));
-          ValueHolder<V> h = new ValueHolder<>(val);
+          ValueHolder<V> h = new ValueHolder<>(val, created.toInstant());
           h.clean = true;
           hitCount.incrementAndGet();
           touch(c, key);
@@ -429,14 +463,22 @@
       return false;
     }
 
-    private boolean expired(Timestamp created) {
+    private boolean expired(Instant created) {
       if (expireAfterWrite == null) {
         return false;
       }
-      Duration age = Duration.between(created.toInstant(), TimeUtil.now());
+      Duration age = Duration.between(created, TimeUtil.now());
       return age.compareTo(expireAfterWrite) > 0;
     }
 
+    private boolean needsRefresh(Instant created) {
+      if (refreshAfterWrite == null) {
+        return false;
+      }
+      Duration age = Duration.between(created, TimeUtil.now());
+      return age.compareTo(refreshAfterWrite) > 0;
+    }
+
     private void touch(SqlHandle c, K key) throws IOException, SQLException {
       if (c.touch == null) {
         c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=? AND version=?");
@@ -474,7 +516,7 @@
           keyType.set(c.put, 1, key);
           c.put.setBytes(2, valueSerializer.serialize(holder.value));
           c.put.setInt(3, version);
-          c.put.setTimestamp(4, new Timestamp(holder.created));
+          c.put.setTimestamp(4, Timestamp.from(holder.created));
           c.put.setTimestamp(5, TimeUtil.nowTs());
           c.put.executeUpdate();
           holder.clean = true;
@@ -560,7 +602,7 @@
             while (maxSize < used && r.next()) {
               K key = keyType.get(r, 1);
               Timestamp created = r.getTimestamp(3);
-              if (mem.getIfPresent(key) != null && !expired(created)) {
+              if (mem.getIfPresent(key) != null && !expired(created.toInstant())) {
                 touch(c, key);
               } else {
                 invalidate(c, key);
diff --git a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index 9906b3d..23caca7 100644
--- a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -105,6 +105,21 @@
       builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
     }
 
+    Duration refreshAfterWrite = def.refreshAfterWrite();
+    if (has(def.configKey(), "refreshAfterWrite")) {
+      builder.refreshAfterWrite(
+          ConfigUtil.getTimeUnit(
+              cfg,
+              "cache",
+              def.configKey(),
+              "refreshAfterWrite",
+              toSeconds(refreshAfterWrite),
+              SECONDS),
+          SECONDS);
+    } else if (refreshAfterWrite != null) {
+      builder.refreshAfterWrite(refreshAfterWrite.toNanos(), NANOSECONDS);
+    }
+
     return builder;
   }
 
@@ -141,6 +156,21 @@
       builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
     }
 
+    Duration refreshAfterWrite = def.refreshAfterWrite();
+    if (has(def.configKey(), "refreshAfterWrite")) {
+      builder.expireAfterAccess(
+          ConfigUtil.getTimeUnit(
+              cfg,
+              "cache",
+              def.configKey(),
+              "refreshAfterWrite",
+              toSeconds(refreshAfterWrite),
+              SECONDS),
+          SECONDS);
+    } else if (refreshAfterWrite != null) {
+      builder.refreshAfterWrite(refreshAfterWrite.toNanos(), NANOSECONDS);
+    }
+
     return builder;
   }
 
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index eb6e8d7..9e228d9 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.extensions.events.ChangeAbandoned;
 import com.google.gerrit.server.mail.send.AbandonedSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.ReplyToChangeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -42,6 +43,7 @@
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
   private final ChangeAbandoned changeAbandoned;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final String msgTxt;
   private final AccountState accountState;
@@ -61,12 +63,14 @@
       ChangeMessagesUtil cmUtil,
       PatchSetUtil psUtil,
       ChangeAbandoned changeAbandoned,
+      MessageIdGenerator messageIdGenerator,
       @Assisted @Nullable AccountState accountState,
       @Assisted @Nullable String msgTxt) {
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.cmUtil = cmUtil;
     this.psUtil = psUtil;
     this.changeAbandoned = changeAbandoned;
+    this.messageIdGenerator = messageIdGenerator;
 
     this.accountState = accountState;
     this.msgTxt = Strings.nullToEmpty(msgTxt);
@@ -116,6 +120,7 @@
       }
       cm.setChangeMessage(message.getMessage(), ctx.getWhen());
       cm.setNotify(notify);
+      cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
       cm.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/AddReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
index 2778bdd..ae3851e 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.send.AddReviewerSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Collection;
@@ -37,13 +38,16 @@
 
   private final AddReviewerSender.Factory addReviewerSenderFactory;
   private final ExecutorService sendEmailsExecutor;
+  private final MessageIdGenerator messageIdGenerator;
 
   @Inject
   AddReviewersEmail(
       AddReviewerSender.Factory addReviewerSenderFactory,
-      @SendEmailExecutor ExecutorService sendEmailsExecutor) {
+      @SendEmailExecutor ExecutorService sendEmailsExecutor,
+      MessageIdGenerator messageIdGenerator) {
     this.addReviewerSenderFactory = addReviewerSenderFactory;
     this.sendEmailsExecutor = sendEmailsExecutor;
+    this.messageIdGenerator = messageIdGenerator;
   }
 
   public void emailReviewersAsync(
@@ -86,6 +90,9 @@
                 cm.addReviewersByEmail(immutableAddedByEmail);
                 cm.addExtraCC(immutableToCopy);
                 cm.addExtraCCByEmail(immutableCopiedByEmail);
+                cm.setMessageId(
+                    messageIdGenerator.fromChangeUpdate(
+                        change.getProject(), change.currentPatchSetId()));
                 cm.send();
               } catch (Exception err) {
                 logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index bbb94ea..467c4a2 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -59,6 +59,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.send.CreateChangeSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -108,6 +109,7 @@
   private final RevisionCreated revisionCreated;
   private final CommentAdded commentAdded;
   private final ReviewerAdder reviewerAdder;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final Change.Id changeId;
   private final PatchSet.Id psId;
@@ -156,6 +158,7 @@
       CommentAdded commentAdded,
       RevisionCreated revisionCreated,
       ReviewerAdder reviewerAdder,
+      MessageIdGenerator messageIdGenerator,
       @Assisted Change.Id changeId,
       @Assisted ObjectId commitId,
       @Assisted String refName) {
@@ -171,6 +174,7 @@
     this.revisionCreated = revisionCreated;
     this.commentAdded = commentAdded;
     this.reviewerAdder = reviewerAdder;
+    this.messageIdGenerator = messageIdGenerator;
 
     this.changeId = changeId;
     this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
@@ -475,6 +479,8 @@
                 cm.addExtraCC(reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
                 cm.addExtraCCByEmail(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCsByEmail));
+                cm.setMessageId(
+                    messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
                 cm.send();
               } catch (Exception e) {
                 logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 8c4f275..8f97b68 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -140,7 +140,6 @@
           COMMIT_FOOTERS,
           CURRENT_ACTIONS,
           CURRENT_COMMIT,
-          DETAILED_LABELS, // may need to load ChangeNotes to check remove reviewer permissions
           MESSAGES);
 
   @Singleton
@@ -722,7 +721,10 @@
     // testRemoveReviewer check for a specific reviewer in the loop saving potentially many
     // permission checks.
     boolean canRemoveAnyReviewer =
-        permissionBackendForChange(userProvider.get(), cd).test(ChangePermission.REMOVE_REVIEWER);
+        permissionBackend
+            .user(userProvider.get())
+            .change(cd)
+            .test(ChangePermission.REMOVE_REVIEWER);
     for (LabelInfo label : labels) {
       if (label.all == null) {
         continue;
@@ -817,16 +819,4 @@
     }
     return map;
   }
-
-  /**
-   * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
-   *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
-   *     lazyload}.
-   */
-  private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd) {
-    PermissionBackend.WithUser withUser = permissionBackend.user(user);
-    return lazyLoad
-        ? withUser.change(cd)
-        : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
-  }
 }
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index 3bc9324..e52375f 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
@@ -36,6 +37,8 @@
   }
 
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
+  private final MessageIdGenerator messageIdGenerator;
+
   private final Address reviewer;
 
   private ChangeMessage changeMessage;
@@ -43,8 +46,11 @@
 
   @Inject
   DeleteReviewerByEmailOp(
-      DeleteReviewerSender.Factory deleteReviewerSenderFactory, @Assisted Address reviewer) {
+      DeleteReviewerSender.Factory deleteReviewerSenderFactory,
+      MessageIdGenerator messageIdGenerator,
+      @Assisted Address reviewer) {
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
+    this.messageIdGenerator = messageIdGenerator;
     this.reviewer = reviewer;
   }
 
@@ -79,6 +85,8 @@
       cm.addReviewersByEmail(Collections.singleton(reviewer));
       cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
       cm.setNotify(notify);
+      cm.setMessageId(
+          messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
       cm.send();
     } catch (Exception err) {
       logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index b70b059..5a9fb99 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.extensions.events.ReviewerDeleted;
 import com.google.gerrit.server.mail.send.DeleteReviewerSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -45,14 +46,13 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoView;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 public class DeleteReviewerOp implements BatchUpdateOp {
@@ -71,6 +71,7 @@
   private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final AccountState reviewer;
   private final DeleteReviewerInput input;
@@ -92,6 +93,7 @@
       DeleteReviewerSender.Factory deleteReviewerSenderFactory,
       RemoveReviewerControl removeReviewerControl,
       ProjectCache projectCache,
+      MessageIdGenerator messageIdGenerator,
       @Assisted AccountState reviewerAccount,
       @Assisted DeleteReviewerInput input) {
     this.approvalsUtil = approvalsUtil;
@@ -103,6 +105,7 @@
     this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
+    this.messageIdGenerator = messageIdGenerator;
     this.reviewer = reviewerAccount;
     this.input = input;
   }
@@ -134,12 +137,10 @@
     msg.append("Removed reviewer " + reviewer.account().fullName());
     StringBuilder removedVotesMsg = new StringBuilder();
     removedVotesMsg.append(" with the following votes:\n\n");
-    List<PatchSetApproval> del = new ArrayList<>();
     boolean votesRemoved = false;
     for (PatchSetApproval a : approvals(ctx, reviewerId)) {
       // Check if removing this vote is OK
       removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
-      del.add(a);
       if (a.patchSetId().equals(currPs.id()) && a.value() != 0) {
         oldApprovals.put(a.label(), a.value());
         removedVotesMsg
@@ -181,7 +182,7 @@
     }
     try {
       if (notify.shouldNotify()) {
-        emailReviewers(ctx.getProject(), currChange, changeMessage, notify);
+        emailReviewers(ctx.getProject(), currChange, changeMessage, notify, ctx.getRepoView());
       }
     } catch (Exception err) {
       logger.atSevere().withCause(err).log("Cannot email update for change %s", currChange.getId());
@@ -215,7 +216,8 @@
       Project.NameKey projectName,
       Change change,
       ChangeMessage changeMessage,
-      NotifyResolver.Result notify)
+      NotifyResolver.Result notify,
+      RepoView repoView)
       throws EmailException {
     Account.Id userId = user.get().getAccountId();
     if (userId.equals(reviewer.account().id())) {
@@ -227,6 +229,7 @@
     cm.addReviewers(Collections.singleton(reviewer.account().id()));
     cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
     cm.setNotify(notify);
+    cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
     cm.send();
   }
 }
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index f7e45e7..f1eff6f 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -25,8 +25,10 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.send.CommentSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.update.RepoView;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -65,13 +67,15 @@
         ChangeMessage message,
         List<Comment> comments,
         String patchSetComment,
-        List<LabelVote> labels);
+        List<LabelVote> labels,
+        RepoView repoView);
   }
 
   private final ExecutorService sendEmailsExecutor;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final CommentSender.Factory commentSenderFactory;
   private final ThreadLocalRequestContext requestContext;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final NotifyResolver.Result notify;
   private final ChangeNotes notes;
@@ -81,6 +85,7 @@
   private final List<Comment> comments;
   private final String patchSetComment;
   private final List<LabelVote> labels;
+  private final RepoView repoView;
 
   @Inject
   EmailReviewComments(
@@ -88,6 +93,7 @@
       PatchSetInfoFactory patchSetInfoFactory,
       CommentSender.Factory commentSenderFactory,
       ThreadLocalRequestContext requestContext,
+      MessageIdGenerator messageIdGenerator,
       @Assisted NotifyResolver.Result notify,
       @Assisted ChangeNotes notes,
       @Assisted PatchSet patchSet,
@@ -95,11 +101,13 @@
       @Assisted ChangeMessage message,
       @Assisted List<Comment> comments,
       @Nullable @Assisted String patchSetComment,
-      @Assisted List<LabelVote> labels) {
+      @Assisted List<LabelVote> labels,
+      @Assisted RepoView repoView) {
     this.sendEmailsExecutor = executor;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.commentSenderFactory = commentSenderFactory;
     this.requestContext = requestContext;
+    this.messageIdGenerator = messageIdGenerator;
     this.notify = notify;
     this.notes = notes;
     this.patchSet = patchSet;
@@ -108,6 +116,7 @@
     this.comments = COMMENT_ORDER.sortedCopy(comments);
     this.patchSetComment = patchSetComment;
     this.labels = labels;
+    this.repoView = repoView;
   }
 
   public void sendAsync() {
@@ -127,6 +136,7 @@
       cm.setPatchSetComment(patchSetComment);
       cm.setLabels(labels);
       cm.setNotify(notify);
+      cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, patchSet.id()));
       cm.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.id());
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index c6f4969..2db17d6 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -131,7 +131,7 @@
 
     Map<String, Short> labels = null;
     Set<LabelPermission.WithValue> can =
-        permissionBackendForChange(filterApprovalsBy, cd).testLabels(toCheck.values());
+        permissionBackend.absentUser(filterApprovalsBy).change(cd).testLabels(toCheck.values());
     SetMultimap<String, String> permitted = LinkedHashMultimap.create();
     for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
@@ -452,7 +452,7 @@
 
     LabelTypes labelTypes = cd.getLabelTypes();
     for (Account.Id accountId : allUsers) {
-      PermissionBackend.ForChange perm = permissionBackendForChange(accountId, cd);
+      PermissionBackend.ForChange perm = permissionBackend.absentUser(accountId).change(cd);
       Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
       for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
         LabelType lt = labelTypes.byLabel(e.getKey());
@@ -492,18 +492,6 @@
     }
   }
 
-  /**
-   * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
-   *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
-   *     lazyload}.
-   */
-  private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd) {
-    PermissionBackend.WithUser withUser = permissionBackend.absentUser(user);
-    return lazyLoad
-        ? withUser.change(cd)
-        : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
-  }
-
   private List<SubmitRecord> submitRecords(ChangeData cd) {
     return cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
   }
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 988d178..d4d74a3 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -79,6 +80,7 @@
   private final ChangeMessagesUtil cmUtil;
   private final PatchSetUtil psUtil;
   private final WorkInProgressStateChanged wipStateChanged;
+  private final MessageIdGenerator messageIdGenerator;
 
   // Assisted-injected fields.
   private final PatchSet.Id psId;
@@ -120,6 +122,7 @@
       RevisionCreated revisionCreated,
       ProjectCache projectCache,
       WorkInProgressStateChanged wipStateChanged,
+      MessageIdGenerator messageIdGenerator,
       @Assisted ChangeNotes notes,
       @Assisted PatchSet.Id psId,
       @Assisted ObjectId commitId) {
@@ -133,6 +136,7 @@
     this.revisionCreated = revisionCreated;
     this.projectCache = projectCache;
     this.wipStateChanged = wipStateChanged;
+    this.messageIdGenerator = messageIdGenerator;
 
     this.origNotes = notes;
     this.psId = psId;
@@ -291,6 +295,7 @@
         cm.addReviewers(oldReviewers.byState(REVIEWER));
         cm.addExtraCC(oldReviewers.byState(CC));
         cm.setNotify(notify);
+        cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 001a532..414107f 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -31,7 +31,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
@@ -60,7 +59,6 @@
 import com.google.gerrit.server.account.GpgApiAdapter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -107,8 +105,6 @@
   private final AnonymousUser anonymous;
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
-  private final ChangeNotes.Factory notesFactory;
-  private final boolean lazyLoad;
 
   @Inject
   RevisionJson(
@@ -128,7 +124,6 @@
       ChangeKindCache changeKindCache,
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
-      ChangeNotes.Factory notesFactory,
       @Assisted Iterable<ListChangesOption> options) {
     this.userProvider = userProvider;
     this.anonymous = anonymous;
@@ -145,10 +140,8 @@
     this.changeResourceFactory = changeResourceFactory;
     this.changeKindCache = changeKindCache;
     this.permissionBackend = permissionBackend;
-    this.notesFactory = notesFactory;
     this.repoManager = repoManager;
     this.options = ImmutableSet.copyOf(options);
-    this.lazyLoad = containsAnyOf(this.options, ChangeJson.REQUIRE_LAZY_LOAD);
   }
 
   /**
@@ -346,22 +339,9 @@
     return options.contains(option);
   }
 
-  /**
-   * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
-   *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
-   *     lazyload}.
-   */
-  private PermissionBackend.ForChange permissionBackendForChange(
-      PermissionBackend.WithUser withUser, ChangeData cd) {
-    return lazyLoad
-        ? withUser.change(cd)
-        : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
-  }
-
   private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException {
     try {
-      permissionBackendForChange(permissionBackend.user(anonymous), cd)
-          .check(ChangePermission.READ);
+      permissionBackend.user(anonymous).change(cd).check(ChangePermission.READ);
     } catch (AuthException ae) {
       return false;
     }
@@ -382,9 +362,4 @@
   private RevWalk newRevWalk(@Nullable Repository repo) {
     return repo != null ? new RevWalk(repo) : null;
   }
-
-  private static boolean containsAnyOf(
-      ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
-    return !Sets.intersection(toFind, set).isEmpty();
-  }
 }
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index 9848150..74536aa 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.AssigneeChanged;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.SetAssigneeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -50,6 +51,7 @@
   private final SetAssigneeSender.Factory setAssigneeSenderFactory;
   private final Provider<IdentifiedUser> user;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final MessageIdGenerator messageIdGenerator;
 
   private Change change;
   private IdentifiedUser oldAssignee;
@@ -62,6 +64,7 @@
       SetAssigneeSender.Factory setAssigneeSenderFactory,
       Provider<IdentifiedUser> user,
       IdentifiedUser.GenericFactory userFactory,
+      MessageIdGenerator messageIdGenerator,
       @Assisted IdentifiedUser newAssignee) {
     this.cmUtil = cmUtil;
     this.validationListeners = validationListeners;
@@ -69,6 +72,7 @@
     this.setAssigneeSenderFactory = setAssigneeSenderFactory;
     this.user = user;
     this.userFactory = userFactory;
+    this.messageIdGenerator = messageIdGenerator;
     this.newAssignee = requireNonNull(newAssignee, "assignee");
   }
 
@@ -122,6 +126,8 @@
           setAssigneeSenderFactory.create(
               change.getProject(), change.getId(), newAssignee.getAccountId());
       cm.setFrom(user.get().getAccountId());
+      cm.setMessageId(
+          messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
       cm.send();
     } catch (Exception err) {
       logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 283cff8..f0ebb80 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.InputWithMessage;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -30,8 +31,10 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoView;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
 
 /* Set work in progress or ready for review state on a change */
 public class WorkInProgressOp implements BatchUpdateOp {
@@ -131,6 +134,13 @@
         || !sendEmail) {
       return;
     }
+    RepoView repoView;
+    try {
+      repoView = ctx.getRepoView();
+    } catch (IOException ex) {
+      throw new StorageException(
+          String.format("Repository %s not found", ctx.getProject().get()), ex);
+    }
     email
         .create(
             notify,
@@ -140,7 +150,8 @@
             cmsg,
             ImmutableList.of(),
             cmsg.getMessage(),
-            ImmutableList.of())
+            ImmutableList.of(),
+            repoView)
         .sendAsync();
   }
 }
diff --git a/java/com/google/gerrit/server/config/CachedPreferences.java b/java/com/google/gerrit/server/config/CachedPreferences.java
index f4dcd10..388f58a 100644
--- a/java/com/google/gerrit/server/config/CachedPreferences.java
+++ b/java/com/google/gerrit/server/config/CachedPreferences.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.server.account.StoredPreferences;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -52,7 +51,7 @@
   public static GeneralPreferencesInfo general(
       Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
     try {
-      return StoredPreferences.parseGeneralPreferences(
+      return PreferencesParserUtil.parseGeneralPreferences(
           userPreferences.asConfig(), configOrNull(defaultPreferences), null);
     } catch (ConfigInvalidException e) {
       return GeneralPreferencesInfo.defaults();
@@ -62,7 +61,7 @@
   public static EditPreferencesInfo edit(
       Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
     try {
-      return StoredPreferences.parseEditPreferences(
+      return PreferencesParserUtil.parseEditPreferences(
           userPreferences.asConfig(), configOrNull(defaultPreferences), null);
     } catch (ConfigInvalidException e) {
       return EditPreferencesInfo.defaults();
@@ -72,7 +71,7 @@
   public static DiffPreferencesInfo diff(
       Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
     try {
-      return StoredPreferences.parseDiffPreferences(
+      return PreferencesParserUtil.parseDiffPreferences(
           userPreferences.asConfig(), configOrNull(defaultPreferences), null);
     } catch (ConfigInvalidException e) {
       return DiffPreferencesInfo.defaults();
diff --git a/java/com/google/gerrit/server/config/PreferencesParserUtil.java b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
new file mode 100644
index 0000000..69d75be
--- /dev/null
+++ b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
@@ -0,0 +1,266 @@
+// 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.
+
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.MenuItem;
+import com.google.gerrit.server.git.UserConfigSections;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/** Helper to read default or user preferences from Git-style config files. */
+public class PreferencesParserUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private PreferencesParserUtil() {}
+
+  /**
+   * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
+   * the server's default configs and {@code cfg} for the user's config. These configs are then
+   * overlaid to inherit values (default -> user -> input (if provided).
+   */
+  public static GeneralPreferencesInfo parseGeneralPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
+      throws ConfigInvalidException {
+    GeneralPreferencesInfo r =
+        loadSection(
+            cfg,
+            UserConfigSections.GENERAL,
+            null,
+            new GeneralPreferencesInfo(),
+            defaultCfg != null
+                ? parseDefaultGeneralPreferences(defaultCfg, input)
+                : GeneralPreferencesInfo.defaults(),
+            input);
+    if (input != null) {
+      r.changeTable = input.changeTable;
+      r.my = input.my;
+    } else {
+      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
+      r.my = parseMyMenus(cfg, defaultCfg);
+    }
+    return r;
+  }
+
+  /**
+   * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
+   * the server's default configs. These configs are then overlaid to inherit values (default ->
+   * input (if provided).
+   */
+  public static GeneralPreferencesInfo parseDefaultGeneralPreferences(
+      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
+    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.GENERAL,
+        null,
+        allUserPrefs,
+        GeneralPreferencesInfo.defaults(),
+        input);
+    return updateGeneralPreferencesDefaults(allUserPrefs);
+  }
+
+  /**
+   * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
+   * to inherit values (default -> user -> input (if provided).
+   */
+  public static DiffPreferencesInfo parseDiffPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.DIFF,
+        null,
+        new DiffPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultDiffPreferences(defaultCfg, input)
+            : DiffPreferencesInfo.defaults(),
+        input);
+  }
+
+  /**
+   * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs. These configs are then overlaid to inherit values (default -> input
+   * (if provided).
+   */
+  public static DiffPreferencesInfo parseDefaultDiffPreferences(
+      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
+    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.DIFF,
+        null,
+        allUserPrefs,
+        DiffPreferencesInfo.defaults(),
+        input);
+    return updateDiffPreferencesDefaults(allUserPrefs);
+  }
+
+  /**
+   * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
+   * to inherit values (default -> user -> input (if provided).
+   */
+  public static EditPreferencesInfo parseEditPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.EDIT,
+        null,
+        new EditPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultEditPreferences(defaultCfg, input)
+            : EditPreferencesInfo.defaults(),
+        input);
+  }
+
+  /**
+   * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs. These configs are then overlaid to inherit values (default -> input
+   * (if provided).
+   */
+  public static EditPreferencesInfo parseDefaultEditPreferences(
+      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
+    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.EDIT,
+        null,
+        allUserPrefs,
+        EditPreferencesInfo.defaults(),
+        input);
+    return updateEditPreferencesDefaults(allUserPrefs);
+  }
+
+  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
+    List<String> changeTable = changeTable(cfg);
+    if (changeTable == null && defaultCfg != null) {
+      changeTable = changeTable(defaultCfg);
+    }
+    return changeTable;
+  }
+
+  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
+    List<MenuItem> my = my(cfg);
+    if (my.isEmpty() && defaultCfg != null) {
+      my = my(defaultCfg);
+    }
+    if (my.isEmpty()) {
+      my.add(new MenuItem("Dashboard", "#/dashboard/self", null));
+      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
+      my.add(new MenuItem("Edits", "#/q/has:edit", null));
+      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
+      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
+      my.add(new MenuItem("Groups", "#/settings/#Groups", null));
+    }
+    return my;
+  }
+
+  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
+      GeneralPreferencesInfo input) {
+    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
+      return GeneralPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
+    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
+      return DiffPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
+    EditPreferencesInfo result = EditPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
+      return EditPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static List<String> changeTable(Config cfg) {
+    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  }
+
+  private static List<MenuItem> my(Config cfg) {
+    List<MenuItem> my = new ArrayList<>();
+    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
+      String url = my(cfg, subsection, KEY_URL, "#/");
+      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
+      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
+    }
+    return my;
+  }
+
+  private static String my(Config cfg, String subsection, String key, String defaultValue) {
+    String val = cfg.getString(UserConfigSections.MY, subsection, key);
+    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
+  }
+}
diff --git a/java/com/google/gerrit/server/config/SysExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
index e7f4540..ea45b12 100644
--- a/java/com/google/gerrit/server/config/SysExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -14,7 +14,11 @@
 
 package com.google.gerrit.server.config;
 
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.server.CacheRefreshExecutor;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.AbstractModule;
@@ -24,7 +28,7 @@
 import org.eclipse.jgit.lib.Config;
 
 /**
- * Module providing the {@link ReceiveCommitsExecutor}.
+ * Module providing different executors.
  *
  * <p>This module is intended to be installed at the top level when creating a {@code sysInjector}
  * in {@code Daemon} or similar, not nested in another module. This ensures the module can be
@@ -37,7 +41,7 @@
   @Provides
   @Singleton
   @ReceiveCommitsExecutor
-  public ExecutorService createReceiveCommitsExecutor(
+  public ExecutorService provideReceiveCommitsExecutor(
       @GerritServerConfig Config config, WorkQueue queues) {
     int poolSize =
         config.getInt(
@@ -48,11 +52,11 @@
   @Provides
   @Singleton
   @SendEmailExecutor
-  public ExecutorService createSendEmailExecutor(
+  public ExecutorService provideSendEmailExecutor(
       @GerritServerConfig Config config, WorkQueue queues) {
     int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
     if (poolSize == 0) {
-      return MoreExecutors.newDirectExecutorService();
+      return newDirectExecutorService();
     }
     return queues.createQueue(poolSize, "SendEmail", true);
   }
@@ -60,11 +64,24 @@
   @Provides
   @Singleton
   @FanOutExecutor
-  public ExecutorService createFanOutExecutor(@GerritServerConfig Config config, WorkQueue queues) {
+  public ExecutorService provideFanOutExecutor(
+      @GerritServerConfig Config config, WorkQueue queues) {
     int poolSize = config.getInt("execution", null, "fanOutThreadPoolSize", 25);
     if (poolSize == 0) {
-      return MoreExecutors.newDirectExecutorService();
+      return newDirectExecutorService();
     }
     return queues.createQueue(poolSize, "FanOut");
   }
+
+  @Provides
+  @Singleton
+  @CacheRefreshExecutor
+  public ListeningExecutorService provideCacheRefreshExecutor(
+      @GerritServerConfig Config config, WorkQueue queues) {
+    int poolSize = config.getInt("cache", null, "refreshThreadPoolSize", 2);
+    if (poolSize == 0) {
+      return newDirectExecutorService();
+    }
+    return MoreExecutors.listeningDecorator(queues.createQueue(poolSize, "CacheRefresh"));
+  }
 }
diff --git a/java/com/google/gerrit/server/config/UrlFormatter.java b/java/com/google/gerrit/server/config/UrlFormatter.java
index 6b2510e..d3f90e5 100644
--- a/java/com/google/gerrit/server/config/UrlFormatter.java
+++ b/java/com/google/gerrit/server/config/UrlFormatter.java
@@ -47,6 +47,16 @@
     return getWebUrl().map(url -> url + "c/" + project.get() + "/+/" + id.get());
   }
 
+  /** Returns the URL for viewing the comment tab view of a change. */
+  default Optional<String> getCommentsTabView(Change change) {
+    return getChangeViewUrl(change.getProject(), change.getId()).map(url -> url + "?tab=comments");
+  }
+
+  /** Returns the URL for viewing the findings tab view of a change. */
+  default Optional<String> getFindingsTabView(Change change) {
+    return getChangeViewUrl(change.getProject(), change.getId()).map(url -> url + "?tab=findings");
+  }
+
   /** Returns the URL for viewing a file in a given patch set of a change. */
   default Optional<String> getPatchFileView(Change change, int patchsetId, String filename) {
     return getChangeViewUrl(change.getProject(), change.getId())
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index 2eb46f1..2d5e708 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -21,7 +21,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.vladsch.flexmark.Extension;
 import com.vladsch.flexmark.ast.Block;
 import com.vladsch.flexmark.ast.Heading;
 import com.vladsch.flexmark.ast.Node;
@@ -36,7 +35,6 @@
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.nio.charset.Charset;
-import java.util.ArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.commons.lang.StringEscapeUtils;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -95,11 +93,6 @@
                 options, MarkdownFormatterHeader.HeadingExtension.create())
             .toMutable();
 
-    ArrayList<Extension> extensions = new ArrayList<>();
-    for (Extension extension : optionsExt.get(com.vladsch.flexmark.parser.Parser.EXTENSIONS)) {
-      extensions.add(extension);
-    }
-
     return optionsExt;
   }
 
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index df53133..329530c 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.server.change.ChangeMessages;
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.extensions.events.ChangeReverted;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.RevertedSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -86,6 +87,7 @@
   private final ChangeMessagesUtil cmUtil;
   private final ChangeReverted changeReverted;
   private final BatchUpdate.Factory updateFactory;
+  private final MessageIdGenerator messageIdGenerator;
 
   @Inject
   CommitUtil(
@@ -98,7 +100,8 @@
       RevertedSender.Factory revertedSenderFactory,
       ChangeMessagesUtil cmUtil,
       ChangeReverted changeReverted,
-      BatchUpdate.Factory updateFactory) {
+      BatchUpdate.Factory updateFactory,
+      MessageIdGenerator messageIdGenerator) {
     this.repoManager = repoManager;
     this.serverIdent = serverIdent;
     this.seq = seq;
@@ -109,6 +112,7 @@
     this.cmUtil = cmUtil;
     this.changeReverted = changeReverted;
     this.updateFactory = updateFactory;
+    this.messageIdGenerator = messageIdGenerator;
   }
 
   public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
@@ -308,6 +312,8 @@
         RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
         cm.setNotify(ctx.getNotify(change.getId()));
+        cm.setMessageId(
+            messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index b272cba..89d6bd3 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.extensions.events.ChangeMerged;
 import com.google.gerrit.server.mail.send.MergedSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -70,6 +71,7 @@
   private final PatchSetUtil psUtil;
   private final ExecutorService sendEmailExecutor;
   private final ChangeMerged changeMerged;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final PatchSet.Id psId;
   private final SubmissionId submissionId;
@@ -90,6 +92,7 @@
       PatchSetUtil psUtil,
       @SendEmailExecutor ExecutorService sendEmailExecutor,
       ChangeMerged changeMerged,
+      MessageIdGenerator messageIdGenerator,
       @Assisted RequestScopePropagator requestScopePropagator,
       @Assisted PatchSet.Id psId,
       @Assisted SubmissionId submissionId,
@@ -101,6 +104,7 @@
     this.psUtil = psUtil;
     this.sendEmailExecutor = sendEmailExecutor;
     this.changeMerged = changeMerged;
+    this.messageIdGenerator = messageIdGenerator;
     this.requestScopePropagator = requestScopePropagator;
     this.submissionId = submissionId;
     this.psId = psId;
@@ -189,6 +193,8 @@
                           mergedSenderFactory.create(ctx.getProject(), psId.changeId());
                       cm.setFrom(ctx.getAccountId());
                       cm.setPatchSet(patchSet, info);
+                      cm.setMessageId(
+                          messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
                       cm.send();
                     } catch (Exception e) {
                       logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 196fc61..fed6541 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -165,7 +165,7 @@
         List<CachedChange> result = new ArrayList<>(cds.size());
         for (ChangeData cd : cds) {
           result.add(
-              new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.getReviewers()));
+              new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.reviewers()));
         }
         return Collections.unmodifiableList(result);
       }
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index f2a0ff1..4b08040 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -606,9 +606,12 @@
     @Override
     public void run() {
       if (running.compareAndSet(false, true)) {
+        String oldThreadName = Thread.currentThread().getName();
         try {
+          Thread.currentThread().setName(oldThreadName + "[" + task.toString() + "]");
           task.run();
         } finally {
+          Thread.currentThread().setName(oldThreadName);
           if (isPeriodic()) {
             running.set(false);
           } else {
@@ -681,5 +684,10 @@
     public boolean hasCustomizedPrint() {
       return runnable.hasCustomizedPrint();
     }
+
+    @Override
+    public String toString() {
+      return runnable.toString();
+    }
   }
 }
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 0d762c7..2177485 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -417,6 +417,16 @@
     metrics.latencyPerPush.record(pushType, deltaNanos, NANOSECONDS);
   }
 
+  /**
+   * Sends all messages which have been collected while processing the push to the client.
+   *
+   * @see ReceiveCommits#sendMessages()
+   */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public void sendMessages() {
+    receiveCommits.sendMessages();
+  }
+
   public ReceivePack getReceivePack() {
     return receivePack;
   }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index a845e53..404fa3c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -737,7 +737,7 @@
       Set<BranchNameKey> branches = new HashSet<>();
       for (ReceiveCommand c : cmds) {
         // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that
-        // should happen in this loop are things that can't happen within one BatchUpdate because
+        // should happen in this loops are things that can't happen within one BatchUpdate because
         // they involve kicking off an additional BatchUpdate.
         if (c.getResult() != OK) {
           continue;
@@ -3238,6 +3238,13 @@
                       ObjectInserter ins = repo.newObjectInserter();
                       ObjectReader reader = ins.newReader();
                       RevWalk rw = new RevWalk(reader)) {
+                    if (ObjectId.zeroId().equals(cmd.getOldId())) {
+                      // The user is creating a new branch. The branch can't contain any changes, so
+                      // auto-closing doesn't apply. Exiting here early to spare any further,
+                      // potentially expensive computation that loop over all commits.
+                      return null;
+                    }
+
                     bu.setRepository(repo, rw, ins);
                     // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
 
@@ -3247,9 +3254,7 @@
                     rw.reset();
                     rw.sort(RevSort.REVERSE);
                     rw.markStart(newTip);
-                    if (!ObjectId.zeroId().equals(cmd.getOldId())) {
-                      rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
-                    }
+                    rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
 
                     Map<Change.Key, ChangeNotes> byKey = null;
                     List<ReplaceRequest> replaceAndClose = new ArrayList<>();
@@ -3261,6 +3266,8 @@
                     for (RevCommit c; (c = rw.next()) != null; ) {
                       rw.parseBody(c);
 
+                      // Check if change refs point to this commit. Usually there are 0-1 change
+                      // refs pointing to this commit.
                       for (Ref ref :
                           receivePackRefCache.tipsFromObjectId(c.copy(), RefNames.REFS_CHANGES)) {
                         PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
@@ -3360,6 +3367,8 @@
         logger.atSevere().withCause(e).log("Can't insert patchset");
       } catch (UpdateException e) {
         logger.atSevere().withCause(e).log("Failed to auto-close changes");
+      } finally {
+        logger.atFine().log("Done auto-closing changes");
       }
     }
   }
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 0baecf5..f5c69e0 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -61,6 +61,7 @@
 import com.google.gerrit.server.git.MergedByPushOp;
 import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -128,6 +129,8 @@
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ProjectCache projectCache;
   private final ReviewerAdder reviewerAdder;
+  private final Change change;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final ProjectState projectState;
   private final BranchNameKey dest;
@@ -140,7 +143,6 @@
   private final PatchSetInfo info;
   private final MagicBranchInput magicBranch;
   private final PushCertificate pushCertificate;
-  private final Change change;
   private List<String> groups;
 
   private final Map<String, Short> approvals = new HashMap<>();
@@ -172,6 +174,7 @@
       @SendEmailExecutor ExecutorService sendEmailExecutor,
       ReviewerAdder reviewerAdder,
       Change change,
+      MessageIdGenerator messageIdGenerator,
       @Assisted ProjectState projectState,
       @Assisted BranchNameKey dest,
       @Assisted boolean checkMergedInto,
@@ -197,6 +200,8 @@
     this.projectCache = projectCache;
     this.sendEmailExecutor = sendEmailExecutor;
     this.reviewerAdder = reviewerAdder;
+    this.change = change;
+    this.messageIdGenerator = messageIdGenerator;
 
     this.projectState = projectState;
     this.dest = dest;
@@ -210,7 +215,6 @@
     this.groups = groups;
     this.magicBranch = magicBranch;
     this.pushCertificate = pushCertificate;
-    this.change = change;
   }
 
   @Override
@@ -533,6 +537,7 @@
                     oldRecipients.getCcOnly().stream(),
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs).stream())
                 .collect(toImmutableSet()));
+        cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSetId));
         // TODO(dborowitz): Support byEmail
         cm.send();
       } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 7535f51..923ba68 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -45,6 +45,9 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.validators.ValidationMessage.Type;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -230,7 +233,17 @@
     List<CommitValidationMessage> messages = new ArrayList<>();
     try {
       for (CommitValidationListener commitValidator : validators) {
-        messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+        try (TraceTimer traceTimer =
+            TraceContext.newTimer(
+                "Running CommitValidationListener",
+                Metadata.builder()
+                    .className(commitValidator.getClass().getSimpleName())
+                    .projectName(receiveEvent.getProjectNameKey().get())
+                    .branchName(receiveEvent.getBranchNameKey().branch())
+                    .commit(receiveEvent.commit.name())
+                    .build())) {
+          messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+        }
       }
     } catch (CommitValidationException e) {
       logger.atFine().withCause(e).log(
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 3bb4770..1a4a335 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -31,116 +31,126 @@
 /** Metadata that is provided to {@link PerformanceLogger}s as context for performance records. */
 @AutoValue
 public abstract class Metadata {
-  // The numeric ID of an account.
+  /** The numeric ID of an account. */
   public abstract Optional<Integer> accountId();
 
-  // The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
-  // PLUGIN_UPDATE).
+  /**
+   * The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
+   * PLUGIN_UPDATE).
+   */
   public abstract Optional<String> actionType();
 
-  // An authentication domain name.
+  /** An authentication domain name. */
   public abstract Optional<String> authDomainName();
 
-  // The name of a branch.
+  /** The name of a branch. */
   public abstract Optional<String> branchName();
 
-  // Key of an entity in a cache.
+  /** Key of an entity in a cache. */
   public abstract Optional<String> cacheKey();
 
-  // The name of a cache.
+  /** The name of a cache. */
   public abstract Optional<String> cacheName();
 
-  // The name of the implementation class.
+  /** The name of the implementation class. */
   public abstract Optional<String> className();
 
-  // The numeric ID of a change.
+  /** The numeric ID of a change. */
   public abstract Optional<Integer> changeId();
 
-  // The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
+  /**
+   * The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
+   */
   public abstract Optional<String> changeIdType();
 
-  // The cause of an error.
+  /** The cause of an error. */
   public abstract Optional<String> cause();
 
-  // The type of an event.
+  /** The SHA1 of a commit. */
+  public abstract Optional<String> commit();
+
+  /** The type of an event. */
   public abstract Optional<String> eventType();
 
-  // The value of the @Export annotation which was used to register a plugin extension.
+  /** The value of the @Export annotation which was used to register a plugin extension. */
   public abstract Optional<String> exportValue();
 
-  // Path of a file in a repository.
+  /** Path of a file in a repository. */
   public abstract Optional<String> filePath();
 
-  // Garbage collector name.
+  /** Garbage collector name. */
   public abstract Optional<String> garbageCollectorName();
 
-  // Git operation (CLONE, FETCH).
+  /** Git operation (CLONE, FETCH). */
   public abstract Optional<String> gitOperation();
 
-  // The numeric ID of an internal group.
+  /** The numeric ID of an internal group. */
   public abstract Optional<Integer> groupId();
 
-  // The name of a group.
+  /** The name of a group. */
   public abstract Optional<String> groupName();
 
-  // The UUID of a group.
+  /** The UUID of a group. */
   public abstract Optional<String> groupUuid();
 
-  // HTTP status response code.
+  /** HTTP status response code. */
   public abstract Optional<Integer> httpStatus();
 
-  // The name of a secondary index.
+  /** The name of a secondary index. */
   public abstract Optional<String> indexName();
 
-  // The version of a secondary index.
+  /** The version of a secondary index. */
   public abstract Optional<Integer> indexVersion();
 
-  // The name of the implementation method.
+  /** The name of the implementation method. */
   public abstract Optional<String> methodName();
 
-  // One or more resources
+  /** One or more resources */
   public abstract Optional<Boolean> multiple();
 
-  // The name of an operation that is performed.
+  /** The name of an operation that is performed. */
   public abstract Optional<String> operationName();
 
-  // Partial or full computation
+  /** Partial or full computation */
   public abstract Optional<Boolean> partial();
 
-  // Path of a metadata file in NoteDb.
+  /** If a value is still current or not */
+  public abstract Optional<Boolean> outdated();
+
+  /** Path of a metadata file in NoteDb. */
   public abstract Optional<String> noteDbFilePath();
 
-  // Name of a metadata ref in NoteDb.
+  /** Name of a metadata ref in NoteDb. */
   public abstract Optional<String> noteDbRefName();
 
-  // Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS).
+  /** Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS). */
   public abstract Optional<String> noteDbSequenceType();
 
-  // The ID of a patch set.
+  /** The ID of a patch set. */
   public abstract Optional<Integer> patchSetId();
 
-  // Plugin metadata that doesn't fit into any other category.
+  /** Plugin metadata that doesn't fit into any other category. */
   public abstract ImmutableList<PluginMetadata> pluginMetadata();
 
-  // The name of a plugin.
+  /** The name of a plugin. */
   public abstract Optional<String> pluginName();
 
-  // The name of a Gerrit project (aka Git repository).
+  /** The name of a Gerrit project (aka Git repository). */
   public abstract Optional<String> projectName();
 
-  // The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE).
+  /** The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE). */
   public abstract Optional<String> pushType();
 
-  // The number of resources that is processed.
+  /** The number of resources that is processed. */
   public abstract Optional<Integer> resourceCount();
 
-  // The name of a REST view.
+  /** The name of a REST view. */
   public abstract Optional<String> restViewName();
 
-  // The SHA1 of Git commit.
+  /** The SHA1 of Git commit. */
   public abstract Optional<String> revision();
 
-  // The username of an account.
+  /** The username of an account. */
   public abstract Optional<String> username();
 
   /**
@@ -275,6 +285,8 @@
 
     public abstract Builder cause(@Nullable String cause);
 
+    public abstract Builder commit(@Nullable String commit);
+
     public abstract Builder eventType(@Nullable String eventType);
 
     public abstract Builder exportValue(@Nullable String exportValue);
@@ -305,6 +317,8 @@
 
     public abstract Builder partial(boolean partial);
 
+    public abstract Builder outdated(boolean outdated);
+
     public abstract Builder noteDbFilePath(@Nullable String noteDbFilePath);
 
     public abstract Builder noteDbRefName(@Nullable String noteDbRefName);
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 9c3dd02..f004e4b 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -57,6 +57,7 @@
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.mail.MailFilter;
 import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -115,6 +116,7 @@
   private final AccountCache accountCache;
   private final DynamicItem<UrlFormatter> urlFormatter;
   private final PluginSetContext<CommentValidator> commentValidators;
+  private final MessageIdGenerator messageIdGenerator;
 
   @Inject
   public MailProcessor(
@@ -133,7 +135,8 @@
       CommentAdded commentAdded,
       AccountCache accountCache,
       DynamicItem<UrlFormatter> urlFormatter,
-      PluginSetContext<CommentValidator> commentValidators) {
+      PluginSetContext<CommentValidator> commentValidators,
+      MessageIdGenerator messageIdGenerator) {
     this.emails = emails;
     this.emailRejectionSender = emailRejectionSender;
     this.retryHelper = retryHelper;
@@ -150,6 +153,7 @@
     this.accountCache = accountCache;
     this.urlFormatter = urlFormatter;
     this.commentValidators = commentValidators;
+    this.messageIdGenerator = messageIdGenerator;
   }
 
   /**
@@ -222,6 +226,7 @@
     try {
       InboundEmailRejectionSender em =
           emailRejectionSender.create(message.from(), message.id(), reason);
+      em.setMessageId(messageIdGenerator.fromMailMessage(message));
       em.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot send email to warn for an error");
@@ -366,7 +371,8 @@
               changeMessage,
               comments,
               patchSetComment,
-              ImmutableList.of())
+              ImmutableList.of(),
+              ctx.getRepoView())
           .sendAsync();
       // Get previous approvals from this user
       Map<String, Short> approvals = new HashMap<>();
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 3b7b2aa..e02f02a 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -35,11 +35,16 @@
   private final IdentifiedUser user;
   private final AccountSshKey sshKey;
   private final List<String> gpgKeys;
+  private final MessageIdGenerator messageIdGenerator;
 
   @AssistedInject
   public AddKeySender(
-      EmailArguments args, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
+      EmailArguments args,
+      MessageIdGenerator messageIdGenerator,
+      @Assisted IdentifiedUser user,
+      @Assisted AccountSshKey sshKey) {
     super(args, "addkey");
+    this.messageIdGenerator = messageIdGenerator;
     this.user = user;
     this.sshKey = sshKey;
     this.gpgKeys = null;
@@ -47,8 +52,12 @@
 
   @AssistedInject
   public AddKeySender(
-      EmailArguments args, @Assisted IdentifiedUser user, @Assisted List<String> gpgKeys) {
+      EmailArguments args,
+      MessageIdGenerator messageIdGenerator,
+      @Assisted IdentifiedUser user,
+      @Assisted List<String> gpgKeys) {
     super(args, "addkey");
+    this.messageIdGenerator = messageIdGenerator;
     this.user = user;
     this.sshKey = null;
     this.gpgKeys = gpgKeys;
@@ -58,6 +67,7 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", String.format("[Gerrit Code Review] New %s Keys Added", getKeyType()));
+    setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
     add(RecipientType.TO, Address.create(getEmail()));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 48d342e..f3cccf2 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -88,6 +88,16 @@
           .orElse(null);
     }
 
+    /** @return 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. */
+    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]]".
      */
@@ -96,6 +106,8 @@
         return "Commit Message";
       } else if (Patch.MERGE_LIST.equals(filename)) {
         return "Merge List";
+      } else if (Patch.PATCHSET_LEVEL.equals(filename)) {
+        return "Patchset";
       } else {
         return "File " + filename;
       }
@@ -379,7 +391,9 @@
 
     for (CommentSender.FileCommentGroup group : getGroupedInlineComments(repo)) {
       Map<String, Object> groupData = new HashMap<>();
-      groupData.put("link", group.getFileLink());
+      if (!group.filename.equals(Patch.PATCHSET_LEVEL)) {
+        groupData.put("link", group.getFileLink());
+      }
       groupData.put("title", group.getTitle());
       groupData.put("patchSetId", group.patchSetId);
 
@@ -407,7 +421,14 @@
         commentData.put("startLine", startLine);
 
         // Set the comment link.
-        if (comment.lineNbr == 0) {
+
+        if (comment.key.filename.equals(Patch.PATCHSET_LEVEL)) {
+          if (comment instanceof RobotComment) {
+            commentData.put("link", group.getFindingsTabLink());
+          } else {
+            commentData.put("link", group.getCommentsTabLink());
+          }
+        } else if (comment.lineNbr == 0) {
           commentData.put("link", group.getFileLink());
         } else {
           commentData.put("link", group.getCommentLink(comment.side, startLine));
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index 3df7f05..ce336ff 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -38,11 +38,16 @@
   private final IdentifiedUser user;
   private final AccountSshKey sshKey;
   private final List<String> gpgKeyFingerprints;
+  private final MessageIdGenerator messageIdGenerator;
 
   @AssistedInject
   public DeleteKeySender(
-      EmailArguments args, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
+      EmailArguments args,
+      MessageIdGenerator messageIdGenerator,
+      @Assisted IdentifiedUser user,
+      @Assisted AccountSshKey sshKey) {
     super(args, "deletekey");
+    this.messageIdGenerator = messageIdGenerator;
     this.user = user;
     this.gpgKeyFingerprints = Collections.emptyList();
     this.sshKey = sshKey;
@@ -51,9 +56,11 @@
   @AssistedInject
   public DeleteKeySender(
       EmailArguments args,
+      MessageIdGenerator messageIdGenerator,
       @Assisted IdentifiedUser user,
       @Assisted List<String> gpgKeyFingerprints) {
     super(args, "deletekey");
+    this.messageIdGenerator = messageIdGenerator;
     this.user = user;
     this.gpgKeyFingerprints = gpgKeyFingerprints;
     this.sshKey = null;
@@ -63,6 +70,7 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", String.format("[Gerrit Code Review] %s Keys Deleted", getKeyType()));
+    setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
     add(RecipientType.TO, Address.create(getEmail()));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index cec2bb5..bca5338 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -29,11 +30,16 @@
 
   private final IdentifiedUser user;
   private final String operation;
+  private final MessageIdGenerator messageIdGenerator;
 
   @AssistedInject
   public HttpPasswordUpdateSender(
-      EmailArguments args, @Assisted IdentifiedUser user, @Assisted String operation) {
+      EmailArguments args,
+      MessageIdGenerator messageIdGenerator,
+      @Assisted IdentifiedUser user,
+      @Assisted String operation) {
     super(args, "HttpPasswordUpdate");
+    this.messageIdGenerator = messageIdGenerator;
     this.user = user;
     this.operation = operation;
   }
@@ -42,6 +48,9 @@
   protected void init() throws EmailException {
     super.init();
     setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
+    setMessageId(
+        messageIdGenerator.fromReasonAccountIdAndTimestamp(
+            "HTTP_password_change", user.getAccountId(), TimeUtil.now()));
     add(RecipientType.TO, Address.create(getEmail()));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
new file mode 100644
index 0000000..3a411dc
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
@@ -0,0 +1,126 @@
+// 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.
+
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.RepoView;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/** A generator class that creates a {@link MessageId} */
+public class MessageIdGenerator {
+  private final GitRepositoryManager repositoryManager;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  public MessageIdGenerator(GitRepositoryManager repositoryManager, AllUsersName allUsersName) {
+    this.repositoryManager = repositoryManager;
+    this.allUsersName = allUsersName;
+  }
+
+  /**
+   * A unique id used which is a part of the header of all emails sent through by Gerrit. All of the
+   * emails are sent via {@link OutgoingEmail#send()}.
+   */
+  @AutoValue
+  public abstract static class MessageId {
+    public abstract String id();
+  }
+
+  /**
+   * 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) {
+    String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
+    Optional<ObjectId> metaSha1;
+    try {
+      metaSha1 = repoView.getRef(metaRef);
+    } catch (IOException ex) {
+      throw new StorageException("unable to extract info for Message-Id", ex);
+    }
+    return metaSha1
+        .map(optional -> new AutoValue_MessageIdGenerator_MessageId(optional.getName()))
+        .orElseThrow(() -> new IllegalStateException(metaRef + " doesn't exist"));
+  }
+
+  public MessageId fromChangeUpdate(Project.NameKey project, PatchSet.Id patchsetId) {
+    String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
+    Ref ref = getRef(metaRef, project);
+    checkState(ref != null, metaRef + " must exist");
+    return new AutoValue_MessageIdGenerator_MessageId(ref.getObjectId().getName());
+  }
+
+  /**
+   * @param accountId Create a {@link MessageId} as a result of an account update.
+   * @return MessageId that depends on the account id.
+   */
+  public MessageId fromAccountUpdate(Account.Id accountId) {
+    String userRef = RefNames.refsUsers(accountId);
+    Ref ref = getRef(userRef, allUsersName);
+    checkState(ref != null, userRef + " must exist");
+    return new AutoValue_MessageIdGenerator_MessageId(ref.getObjectId().getName());
+  }
+
+  /**
+   * Create a {@link MessageId} from a mail message.
+   *
+   * @param mailMessage The message that was sent but was rejected.
+   * @return MessageId that depends on the MailMessage that was rejected.
+   */
+  public MessageId fromMailMessage(MailMessage mailMessage) {
+    return new AutoValue_MessageIdGenerator_MessageId(mailMessage.id() + "-REJECTION");
+  }
+
+  /**
+   * 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(
+      String reason, Account.Id accountId, Instant timestamp) {
+    return new AutoValue_MessageIdGenerator_MessageId(
+        reason + "-" + accountId.toString() + "-" + timestamp.toString());
+  }
+
+  private Ref getRef(String userRef, Project.NameKey project) {
+    try (Repository repository = repositoryManager.openRepository(project)) {
+      return repository.getRefDatabase().findRef(userRef);
+    } catch (IOException ex) {
+      throw new StorageException("unable to extract info for Message-Id", ex);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index 83c3a94..d81dca4 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -57,7 +57,6 @@
     super.init();
 
     String threadId = getChangeMessageThreadId();
-    setHeader("Message-ID", threadId);
     setHeader("References", threadId);
 
     switch (notify.handling()) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index b35bbec..8f63177 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -67,6 +67,7 @@
   private Address smtpFromAddress;
   private StringBuilder textBody;
   private StringBuilder htmlBody;
+  private MessageIdGenerator.MessageId messageId;
   protected Map<String, Object> soyContext;
   protected Map<String, Object> soyContextEmailData;
   protected List<String> footers;
@@ -88,6 +89,10 @@
     this.notify = requireNonNull(notify);
   }
 
+  public void setMessageId(MessageIdGenerator.MessageId messageId) {
+    this.messageId = messageId;
+  }
+
   /**
    * Format and enqueue the message for delivery.
    *
@@ -108,6 +113,9 @@
     }
 
     init();
+    if (messageId == null) {
+      throw new IllegalStateException("All emails must have a messageId");
+    }
     if (useHtml()) {
       appendHtml(soyHtmlTemplate("HeaderHtml"));
     }
@@ -201,31 +209,21 @@
         va.htmlBody = null;
       }
 
-      for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
-        try {
-          validator.validateOutgoingEmail(va);
-        } catch (ValidationException e) {
-          logger.atFine().log(
-              "Not sending '%s': Rejected by outgoing email validator: %s",
-              messageClass, e.getMessage());
-          return;
-        }
-      }
-
       Set<Address> intersection = Sets.intersection(va.smtpRcptTo, smtpRcptToPlaintextOnly);
       if (!intersection.isEmpty()) {
         logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection);
       }
-
       if (!va.smtpRcptTo.isEmpty()) {
         // Send multipart message
+        addMessageId(va, "-HTML");
+        if (!validateEmail(va)) return;
         logger.atFine().log(
             "Sending multipart '%s' from %s to %s",
             messageClass, va.smtpFromAddress, va.smtpRcptTo);
         args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody);
       }
-
       if (!smtpRcptToPlaintextOnly.isEmpty()) {
+        addMessageId(va, "-PLAIN");
         // Send plaintext message
         Map<String, EmailHeader> shallowCopy = new HashMap<>();
         shallowCopy.putAll(headers);
@@ -238,6 +236,7 @@
           to.add(a);
           shallowCopy.put(FieldName.TO, to);
         }
+        if (!validateEmail(va)) return;
         logger.atFine().log(
             "Sending plaintext '%s' from %s to %s",
             messageClass, va.smtpFromAddress, smtpRcptToPlaintextOnly);
@@ -246,6 +245,29 @@
     }
   }
 
+  private boolean validateEmail(OutgoingEmailValidationListener.Args va) {
+    for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
+      try {
+        validator.validateOutgoingEmail(va);
+      } catch (ValidationException e) {
+        logger.atFine().log(
+            "Not sending '%s': Rejected by outgoing email validator: %s",
+            messageClass, e.getMessage());
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // All message ids must start with < and end with >. Also, they must have @domain and no spaces.
+  private void addMessageId(OutgoingEmailValidationListener.Args va, String suffix) {
+    if (messageId != null) {
+      String message = "<" + messageId.id() + suffix + "@" + getGerritHost() + ">";
+      message = message.replaceAll("\\s", "");
+      va.headers.put(FieldName.MESSAGE_ID, new EmailHeader.String(message));
+    }
+  }
+
   /** Format the message body by calling {@link #appendText(String)}. */
   protected abstract void format() throws EmailException;
 
@@ -262,7 +284,6 @@
     headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
     headers.put(FieldName.TO, new EmailHeader.AddressList());
     headers.put(FieldName.CC, new EmailHeader.AddressList());
-    setHeader(FieldName.MESSAGE_ID, "");
     setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
 
     for (RecipientType recipientType : notify.accounts().keySet()) {
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 7136d2b..8e6606e 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -110,7 +110,7 @@
       ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Date when) {
     checkUserType(u);
     if (u instanceof IdentifiedUser) {
-      return noteUtil.newIdent(u.asIdentifiedUser().getAccount().id(), when, serverIdent);
+      return noteUtil.newAccountIdIdent(u.asIdentifiedUser().getAccount().id(), when, serverIdent);
     } else if (u instanceof InternalUser) {
       return serverIdent;
     }
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 4b538f3..07b58f2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -19,7 +19,6 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
@@ -37,7 +36,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -191,7 +189,6 @@
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
       throws ConfigInvalidException, IOException {
     RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
-    Set<ObjectId> updatedCommits = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
 
     for (Comment c : put) {
@@ -207,7 +204,6 @@
     Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
     boolean touchedAnyRevs = false;
     for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
-      updatedCommits.add(e.getKey());
       ObjectId id = e.getKey();
       byte[] data = e.getValue().build(noteUtil.getChangeNoteJson());
       if (!Arrays.equals(data, e.getValue().baseRaw)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 86b6ed7..15f187a 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -69,16 +69,38 @@
     return changeNoteJson;
   }
 
-  public PersonIdent newIdent(Account.Id accountId, Date when, PersonIdent serverIdent) {
-    return new PersonIdent(
-        getUsername(accountId), getEmailAddress(accountId), when, serverIdent.getTimeZone());
+  /**
+   * Generates a user identifier that contains the account ID, but not the user's name or email
+   * address.
+   *
+   * @return The passed in {@link StringBuilder} instance to which the identifier has been appended.
+   */
+  StringBuilder appendAccountIdIdentString(StringBuilder stringBuilder, Account.Id accountId) {
+    return stringBuilder
+        .append(getAccountIdAsUsername(accountId))
+        .append(" <")
+        .append(getAccountIdAsEmailAddress(accountId))
+        .append('>');
   }
 
-  private static String getUsername(Account.Id accountId) {
+  /**
+   * Returns a {@link PersonIdent} that contains the account ID, but not the user's name or email
+   * address.
+   */
+  public PersonIdent newAccountIdIdent(Account.Id accountId, Date when, PersonIdent serverIdent) {
+    return new PersonIdent(
+        getAccountIdAsUsername(accountId),
+        getAccountIdAsEmailAddress(accountId),
+        when,
+        serverIdent.getTimeZone());
+  }
+
+  /** Returns the string {@code "Gerrit User " + accountId}, to pseudonymize user names. */
+  public static String getAccountIdAsUsername(Account.Id accountId) {
     return "Gerrit User " + accountId.toString();
   }
 
-  private String getEmailAddress(Account.Id accountId) {
+  private String getAccountIdAsEmailAddress(Account.Id accountId) {
     return accountId.get() + "@" + serverId;
   }
 
@@ -198,21 +220,10 @@
   }
 
   String attentionSetUpdateToJson(AttentionSetUpdate attentionSetUpdate) {
-    PersonIdent personIdent =
-        new PersonIdent(
-            getUsername(attentionSetUpdate.account()),
-            getEmailAddress(attentionSetUpdate.account()));
     StringBuilder stringBuilder = new StringBuilder();
-    appendIdentString(stringBuilder, personIdent.getName(), personIdent.getEmailAddress());
+    appendAccountIdIdentString(stringBuilder, attentionSetUpdate.account());
     return gson.toJson(
         new AttentionStatusInNoteDb(
             stringBuilder.toString(), attentionSetUpdate.operation(), attentionSetUpdate.reason()));
   }
-
-  static void appendIdentString(StringBuilder stringBuilder, String name, String emailAddress) {
-    PersonIdent.appendSanitized(stringBuilder, name);
-    stringBuilder.append(" <");
-    PersonIdent.appendSanitized(stringBuilder, emailAddress);
-    stringBuilder.append('>');
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 63f4e5d..41b1b94 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -602,7 +602,7 @@
     if (assignee != null) {
       if (assignee.isPresent()) {
         addFooter(msg, FOOTER_ASSIGNEE);
-        addIdent(msg, assignee.get()).append('\n');
+        noteUtil.appendAccountIdIdentString(msg, assignee.get()).append('\n');
       } else {
         addFooter(msg, FOOTER_ASSIGNEE).append('\n');
       }
@@ -623,7 +623,7 @@
 
     for (Map.Entry<Account.Id, ReviewerStateInternal> e : reviewers.entrySet()) {
       addFooter(msg, e.getValue().getFooterKey());
-      addIdent(msg, e.getKey()).append('\n');
+      noteUtil.appendAccountIdIdentString(msg, e.getKey()).append('\n');
     }
 
     for (Map.Entry<Address, ReviewerStateInternal> e : reviewersByEmail.entrySet()) {
@@ -640,7 +640,7 @@
       }
       Account.Id id = c.getColumnKey();
       if (!id.equals(getAccountId())) {
-        addIdent(msg.append(' '), id);
+        noteUtil.appendAccountIdIdentString(msg.append(' '), id);
       }
       msg.append('\n');
     }
@@ -666,7 +666,7 @@
                 .append(label.label);
             if (label.appliedBy != null) {
               msg.append(": ");
-              addIdent(msg, label.appliedBy);
+              noteUtil.appendAccountIdIdentString(msg, label.appliedBy);
             }
             msg.append('\n');
           }
@@ -677,7 +677,7 @@
 
     if (!Objects.equals(accountId, realAccountId)) {
       addFooter(msg, FOOTER_REAL_USER);
-      addIdent(msg, realAccountId).append('\n');
+      noteUtil.appendAccountIdIdentString(msg, realAccountId).append('\n');
     }
 
     if (isPrivate != null) {
@@ -799,10 +799,4 @@
   private static boolean isIllegalTopic(String topic) {
     return (topic != null && topic.contains("\""));
   }
-
-  private StringBuilder addIdent(StringBuilder sb, Account.Id accountId) {
-    PersonIdent ident = noteUtil.newIdent(accountId, when, serverIdent);
-    ChangeNoteUtil.appendIdentString(sb, ident.getName(), ident.getEmailAddress());
-    return sb;
-  }
 }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 2d1a04a..ca97a1a 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.logging.TraceContext.newTimer;
 
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MultimapBuilder;
@@ -31,6 +32,8 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.update.ChainedReceiveCommands;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -307,8 +310,15 @@
       // we may have stale draft comments. Doing it in this order allows stale
       // comments to be filtered out by ChangeNotes, reflecting the fact that
       // comments can only go from DRAFT to PUBLISHED, not vice versa.
-      BatchRefUpdate result = execute(changeRepo, dryrun, pushCert);
-      execute(allUsersRepo, dryrun, null);
+      BatchRefUpdate result;
+      try (TraceContext.TraceTimer ignored =
+          newTimer("NoteDbUpdateManager#updateRepo", Metadata.empty())) {
+        result = execute(changeRepo, dryrun, pushCert);
+      }
+      try (TraceContext.TraceTimer ignored =
+          newTimer("NoteDbUpdateManager#updateAllUsersSync", Metadata.empty())) {
+        execute(allUsersRepo, dryrun, null);
+      }
       if (!dryrun) {
         // Only execute the asynchronous operation if we are not in dry-run mode: The dry run would
         // have to run synchronous to be of any value at all. For the removal of draft comments from
diff --git a/java/com/google/gerrit/server/patch/DiffExecutorModule.java b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
index eb6a280..b9e644f 100644
--- a/java/com/google/gerrit/server/patch/DiffExecutorModule.java
+++ b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
@@ -31,7 +31,7 @@
   @Provides
   @Singleton
   @DiffExecutor
-  public ExecutorService createDiffExecutor() {
+  public ExecutorService provideDiffExecutor() {
     return new LoggingContextAwareExecutorService(
         Executors.newCachedThreadPool(
             new ThreadFactoryBuilder().setNameFormat("Diff-%d").setDaemon(true).build()));
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index c9e45ba..28f61d3 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -45,23 +45,12 @@
 public class PatchList implements Serializable {
   private static final long serialVersionUID = PatchListKey.serialVersionUID;
 
-  private static final Comparator<PatchListEntry> PATCH_CMP =
-      Comparator.comparing(PatchListEntry::getNewName, PatchList::comparePaths);
-
   @VisibleForTesting
-  static int comparePaths(String a, String b) {
-    int m1 = Patch.isMagic(a) ? (a.equals(Patch.MERGE_LIST) ? 2 : 1) : 3;
-    int m2 = Patch.isMagic(b) ? (b.equals(Patch.MERGE_LIST) ? 2 : 1) : 3;
+  static final Comparator<String> FILE_PATH_CMP =
+      Comparator.comparing(Patch::isMagic).reversed().thenComparing(Comparator.naturalOrder());
 
-    if (m1 != m2) {
-      return m1 - m2;
-    } else if (m1 < 3) {
-      return 0;
-    }
-
-    // m1 == m2 == 3: normal names.
-    return a.compareTo(b);
-  }
+  private static final Comparator<PatchListEntry> PATCH_CMP =
+      Comparator.comparing(PatchListEntry::getNewName, FILE_PATH_CMP);
 
   @Nullable private transient ObjectId oldId;
   private transient ObjectId newId;
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 143547b..9f216c0 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -19,21 +19,16 @@
 
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.Map;
@@ -41,39 +36,16 @@
 
 /** Access control management for a user accessing a single change. */
 class ChangeControl {
-  @Singleton
-  static class Factory {
-    private final ChangeData.Factory changeDataFactory;
-    private final ChangeNotes.Factory notesFactory;
-
-    @Inject
-    Factory(ChangeData.Factory changeDataFactory, ChangeNotes.Factory notesFactory) {
-      this.changeDataFactory = changeDataFactory;
-      this.notesFactory = notesFactory;
-    }
-
-    ChangeControl create(RefControl refControl, Project.NameKey project, Change.Id changeId) {
-      return create(refControl, notesFactory.create(project, changeId));
-    }
-
-    ChangeControl create(RefControl refControl, ChangeNotes notes) {
-      return new ChangeControl(changeDataFactory, refControl, notes);
-    }
-  }
-
-  private final ChangeData.Factory changeDataFactory;
   private final RefControl refControl;
-  private final ChangeNotes notes;
+  private final ChangeData changeData;
 
-  private ChangeControl(
-      ChangeData.Factory changeDataFactory, RefControl refControl, ChangeNotes notes) {
-    this.changeDataFactory = changeDataFactory;
+  ChangeControl(RefControl refControl, ChangeData changeData) {
     this.refControl = refControl;
-    this.notes = notes;
+    this.changeData = changeData;
   }
 
-  ForChange asForChange(@Nullable ChangeData cd) {
-    return new ForChangeImpl(cd);
+  ForChange asForChange() {
+    return new ForChangeImpl();
   }
 
   private CurrentUser getUser() {
@@ -85,7 +57,7 @@
   }
 
   private Change getChange() {
-    return notes.getChange();
+    return changeData.change();
   }
 
   /** Can this user see this change? */
@@ -224,19 +196,13 @@
   }
 
   private class ForChangeImpl extends ForChange {
-    private ChangeData cd;
     private Map<String, PermissionRange> labels;
     private String resourcePath;
 
-    ForChangeImpl(@Nullable ChangeData cd) {
-      this.cd = cd;
-    }
+    private ForChangeImpl() {}
 
     private ChangeData changeData() {
-      if (cd == null) {
-        cd = changeDataFactory.create(notes);
-      }
-      return cd;
+      return changeData;
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
index f3a3c78..3f84dff 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
@@ -31,7 +31,6 @@
       // TODO(hiesel) Hide ProjectControl, RefControl, ChangeControl related bindings.
       factory(ProjectControl.Factory.class);
       factory(DefaultRefFilter.Factory.class);
-      bind(ChangeControl.Factory.class);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index e92ada1..37de0d1 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -448,12 +448,11 @@
     try {
       Map<Change.Id, BranchNameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(project)) {
-        ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
         if (!projectState.statePermitsRead()) {
           continue;
         }
         try {
-          permissionBackendForProject.indexedChange(cd, notes).check(ChangePermission.READ);
+          permissionBackendForProject.change(cd).check(ChangePermission.READ);
           visibleChanges.put(cd.getId(), cd.change().getDest());
         } catch (AuthException e) {
           // Do nothing.
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 2344781..749ca6b 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -173,11 +173,6 @@
     }
 
     @Override
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return new FailedChange(message, cause);
-    }
-
-    @Override
     public void check(RefPermission perm) throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 653c3b5f..23145ba 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -173,15 +173,6 @@
       return ref(notes.getChange().getDest()).change(notes);
     }
 
-    /**
-     * Returns an instance scoped for the change loaded from index, and its destination ref and
-     * project. This method should only be used when database access is harmful and potentially
-     * stale data from the index is acceptable.
-     */
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return ref(notes.getChange().getDest()).indexedChange(cd, notes);
-    }
-
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(GlobalOrPluginPermission perm)
         throws AuthException, PermissionBackendException;
@@ -289,15 +280,6 @@
       return ref(notes.getChange().getDest().branch()).change(notes);
     }
 
-    /**
-     * Returns an instance scoped for the change loaded from index, and its destination ref and
-     * project. This method should only be used when database access is harmful and potentially
-     * stale data from the index is acceptable.
-     */
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return ref(notes.getChange().getDest().branch()).indexedChange(cd, notes);
-    }
-
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(CoreOrPluginProjectPermission perm)
         throws AuthException, PermissionBackendException;
@@ -386,12 +368,6 @@
     /** Returns an instance scoped to change. */
     public abstract ForChange change(ChangeNotes notes);
 
-    /**
-     * @return instance scoped to change loaded from index. This method should only be used when
-     *     database access is harmful and potentially stale data from the index is acceptable.
-     */
-    public abstract ForChange indexedChange(ChangeData cd, ChangeNotes notes);
-
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
 
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 145e0b6..e6d66ee 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -70,9 +70,9 @@
   private final PermissionBackend permissionBackend;
   private final CurrentUser user;
   private final ProjectState state;
-  private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
   private final DefaultRefFilter.Factory refFilterFactory;
+  private final ChangeData.Factory changeDataFactory;
 
   private List<SectionMatcher> allSections;
   private Map<String, RefControl> refControls;
@@ -83,17 +83,17 @@
       @GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       PermissionCollection.Factory permissionFilter,
-      ChangeControl.Factory changeControlFactory,
       PermissionBackend permissionBackend,
       DefaultRefFilter.Factory refFilterFactory,
+      ChangeData.Factory changeDataFactory,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps) {
-    this.changeControlFactory = changeControlFactory;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
     this.permissionBackend = permissionBackend;
     this.refFilterFactory = refFilterFactory;
+    this.changeDataFactory = changeDataFactory;
     user = who;
     state = ps;
   }
@@ -102,13 +102,8 @@
     return new ForProjectImpl();
   }
 
-  ChangeControl controlFor(Change change) {
-    return changeControlFactory.create(
-        controlForRef(change.getDest()), change.getProject(), change.getId());
-  }
-
-  ChangeControl controlFor(ChangeNotes notes) {
-    return changeControlFactory.create(controlForRef(notes.getChange().getDest()), notes);
+  ChangeControl controlFor(ChangeData cd) {
+    return new ChangeControl(controlForRef(cd.change().getDest()), cd);
   }
 
   RefControl controlForRef(BranchNameKey ref) {
@@ -122,7 +117,7 @@
     RefControl ctl = refControls.get(refName);
     if (ctl == null) {
       PermissionCollection relevant = permissionFilter.filter(access(), refName, user);
-      ctl = new RefControl(this, refName, relevant);
+      ctl = new RefControl(changeDataFactory, this, refName, relevant);
       refControls.put(refName, ctl);
     }
     return ctl;
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 7c5d6bd..5081116 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -43,6 +43,7 @@
 class RefControl {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final ChangeData.Factory changeDataFactory;
   private final ProjectControl projectControl;
   private final String refName;
 
@@ -58,7 +59,12 @@
   private Boolean canForgeCommitter;
   private Boolean isVisible;
 
-  RefControl(ProjectControl projectControl, String ref, PermissionCollection relevant) {
+  RefControl(
+      ChangeData.Factory changeDataFactory,
+      ProjectControl projectControl,
+      String ref,
+      PermissionCollection relevant) {
+    this.changeDataFactory = changeDataFactory;
     this.projectControl = projectControl;
     this.refName = ref;
     this.relevant = relevant;
@@ -444,7 +450,7 @@
     @Override
     public ForChange change(ChangeData cd) {
       try {
-        return getProjectControl().controlFor(cd.notes()).asForChange(cd);
+        return getProjectControl().controlFor(cd).asForChange();
       } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
@@ -459,12 +465,9 @@
           "expected change in project %s, not %s",
           project,
           change.getProject());
-      return getProjectControl().controlFor(notes).asForChange(null);
-    }
-
-    @Override
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return getProjectControl().controlFor(notes).asForChange(cd);
+      // Having ChangeNotes means it's OK to load values from NoteDb if needed.
+      // ChangeData.Factory will allow lazyLoading
+      return getProjectControl().controlFor(changeDataFactory.create(notes)).asForChange();
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/project/ProjectCacheClock.java b/java/com/google/gerrit/server/project/ProjectCacheClock.java
deleted file mode 100644
index eb451fd..0000000
--- a/java/com/google/gerrit/server/project/ProjectCacheClock.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2012 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.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import org.eclipse.jgit.lib.Config;
-
-/** Ticks periodically to force refresh events for {@link ProjectCacheImpl}. */
-@Singleton
-public class ProjectCacheClock implements LifecycleListener {
-  private final Config serverConfig;
-
-  private final AtomicLong generation = new AtomicLong();
-
-  private ScheduledExecutorService executor;
-
-  @Inject
-  public ProjectCacheClock(@GerritServerConfig Config serverConfig) {
-    this.serverConfig = serverConfig;
-  }
-
-  @Override
-  public void start() {
-    long checkFrequencyMillis = checkFrequency(serverConfig);
-
-    if (checkFrequencyMillis == Long.MAX_VALUE) {
-      // Start with generation 1 (to avoid magic 0 below).
-      // Do not begin background thread, disabling the clock.
-      generation.set(1);
-    } else if (10 < checkFrequencyMillis) {
-      // Start with generation 1 (to avoid magic 0 below).
-      generation.set(1);
-      executor =
-          new LoggingContextAwareScheduledExecutorService(
-              Executors.newScheduledThreadPool(
-                  1,
-                  new ThreadFactoryBuilder()
-                      .setNameFormat("ProjectCacheClock-%d")
-                      .setDaemon(true)
-                      .setPriority(Thread.MIN_PRIORITY)
-                      .build()));
-      @SuppressWarnings("unused") // Runnable already handles errors
-      Future<?> possiblyIgnoredError =
-          executor.scheduleAtFixedRate(
-              generation::incrementAndGet,
-              checkFrequencyMillis,
-              checkFrequencyMillis,
-              TimeUnit.MILLISECONDS);
-    } else {
-      // Magic generation 0 triggers ProjectState to always
-      // check on each needsRefresh() request we make to it.
-      generation.set(0);
-    }
-  }
-
-  @Override
-  public void stop() {
-    if (executor != null) {
-      executor.shutdown();
-    }
-  }
-
-  long read() {
-    return generation.get();
-  }
-
-  private static long checkFrequency(Config serverConfig) {
-    String freq = serverConfig.getString("cache", "projects", "checkFrequency");
-    if (freq != null && ("disabled".equalsIgnoreCase(freq) || "off".equalsIgnoreCase(freq))) {
-      return Long.MAX_VALUE;
-    }
-    return TimeUnit.MILLISECONDS.convert(
-        ConfigUtil.getTimeUnit(
-            serverConfig, "cache", "projects", "checkFrequency", 5, TimeUnit.MINUTES),
-        TimeUnit.MINUTES);
-  }
-}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 9d09eeb..a5c07e5 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -24,16 +24,23 @@
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.project.ProjectIndexer;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.Counter2;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.server.CacheRefreshExecutor;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
@@ -48,6 +55,7 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -55,6 +63,7 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
 /** Cache of project information, including access rights. */
@@ -70,7 +79,10 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(CACHE_NAME, String.class, ProjectState.class).loader(Loader.class);
+        cache(CACHE_NAME, Project.NameKey.class, ProjectState.class)
+            .loader(Loader.class)
+            .refreshAfterWrite(Duration.ofMinutes(15))
+            .expireAfterWrite(Duration.ofHours(1));
 
         cache(CACHE_LIST, ListKey.class, new TypeLiteral<ImmutableSortedSet<Project.NameKey>>() {})
             .maximumWeight(1)
@@ -84,7 +96,6 @@
               @Override
               protected void configure() {
                 listener().to(ProjectCacheWarmer.class);
-                listener().to(ProjectCacheClock.class);
               }
             });
       }
@@ -93,10 +104,9 @@
 
   private final AllProjectsName allProjectsName;
   private final AllUsersName allUsersName;
-  private final LoadingCache<String, ProjectState> byName;
+  private final LoadingCache<Project.NameKey, ProjectState> byName;
   private final LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list;
   private final Lock listLock;
-  private final ProjectCacheClock clock;
   private final Provider<ProjectIndexer> indexer;
   private final Timer0 guessRelevantGroupsLatency;
 
@@ -104,9 +114,8 @@
   ProjectCacheImpl(
       final AllProjectsName allProjectsName,
       final AllUsersName allUsersName,
-      @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
+      @Named(CACHE_NAME) LoadingCache<Project.NameKey, ProjectState> byName,
       @Named(CACHE_LIST) LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list,
-      ProjectCacheClock clock,
       Provider<ProjectIndexer> indexer,
       MetricMaker metricMaker) {
     this.allProjectsName = allProjectsName;
@@ -114,7 +123,6 @@
     this.byName = byName;
     this.list = list;
     this.listLock = new ReentrantLock(true /* fair */);
-    this.clock = clock;
     this.indexer = indexer;
 
     this.guessRelevantGroupsLatency =
@@ -142,13 +150,8 @@
     }
 
     try {
-      ProjectState state = byName.get(projectName.get());
-      if (state != null && state.needsRefresh(clock.read())) {
-        byName.invalidate(projectName.get());
-        state = byName.get(projectName.get());
-      }
-      return Optional.of(state);
-    } catch (Exception e) {
+      return Optional.of(byName.get(projectName));
+    } catch (ExecutionException e) {
       if ((e.getCause() instanceof RepositoryNotFoundException)) {
         logger.atFine().log("Cannot find project %s", projectName.get());
         return Optional.empty();
@@ -167,7 +170,7 @@
   public void evict(Project.NameKey p) {
     if (p != null) {
       logger.atFine().log("Evict project '%s'", p.get());
-      byName.invalidate(p.get());
+      byName.invalidate(p);
     }
     indexer.get().index(p);
   }
@@ -222,7 +225,7 @@
   public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
     try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
       return all().stream()
-          .map(n -> byName.getIfPresent(n.get()))
+          .map(n -> byName.getIfPresent(n))
           .filter(Objects::nonNull)
           .flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
           // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
@@ -245,41 +248,67 @@
     }
   }
 
-  static class Loader extends CacheLoader<String, ProjectState> {
+  @Singleton
+  static class Loader extends CacheLoader<Project.NameKey, ProjectState> {
     private final ProjectState.Factory projectStateFactory;
     private final GitRepositoryManager mgr;
-    private final ProjectCacheClock clock;
     private final ProjectConfig.Factory projectConfigFactory;
+    private final ListeningExecutorService cacheRefreshExecutor;
+    private final Counter2<String, Boolean> refreshCounter;
 
     @Inject
     Loader(
         ProjectState.Factory psf,
         GitRepositoryManager g,
-        ProjectCacheClock clock,
-        ProjectConfig.Factory projectConfigFactory) {
+        ProjectConfig.Factory projectConfigFactory,
+        @CacheRefreshExecutor ListeningExecutorService cacheRefreshExecutor,
+        MetricMaker metricMaker) {
       projectStateFactory = psf;
       mgr = g;
-      this.clock = clock;
       this.projectConfigFactory = projectConfigFactory;
+      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());
     }
 
     @Override
-    public ProjectState load(String projectName) throws Exception {
+    public ProjectState load(Project.NameKey key) throws Exception {
       try (TraceTimer timer =
           TraceContext.newTimer(
-              "Loading project", Metadata.builder().projectName(projectName).build())) {
-        long now = clock.read();
-        Project.NameKey key = Project.nameKey(projectName);
+              "Loading project", Metadata.builder().projectName(key.get()).build())) {
         try (Repository git = mgr.openRepository(key)) {
           ProjectConfig cfg = projectConfigFactory.create(key);
           cfg.load(key, git);
-
-          ProjectState state = projectStateFactory.create(cfg);
-          state.initLastCheck(now);
-          return state;
+          return projectStateFactory.create(cfg);
         }
       }
     }
+
+    @Override
+    public ListenableFuture<ProjectState> reload(Project.NameKey key, ProjectState oldState)
+        throws Exception {
+      try (TraceTimer timer =
+          TraceContext.newTimer(
+              "Reload project", Metadata.builder().projectName(key.get()).build())) {
+        try (Repository git = mgr.openRepository(key)) {
+          Ref configRef = git.exactRef(RefNames.REFS_CONFIG);
+          if (configRef != null
+              && configRef.getObjectId().equals(oldState.getConfig().getRevision())) {
+            refreshCounter.increment(CACHE_NAME, false);
+            return Futures.immediateFuture(oldState);
+          }
+        }
+
+        // Repository is not thread safe, so we have to open it on the thread that does the loading.
+        // Just invoke the loader on the other thread.
+        refreshCounter.increment(CACHE_NAME, true);
+        return cacheRefreshExecutor.submit(() -> load(key));
+      }
+    }
   }
 
   static class ListKey {
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index e52f344..efadcc8 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -59,7 +58,6 @@
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
 /**
@@ -86,9 +84,6 @@
   private final long globalMaxObjectSizeLimit;
   private final boolean inheritProjectMaxObjectSizeLimit;
 
-  /** Last system time the configuration's revision was examined. */
-  private volatile long lastCheckGeneration;
-
   /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
   private volatile List<SectionMatcher> localAccessSections;
 
@@ -140,33 +135,6 @@
     }
   }
 
-  void initLastCheck(long generation) {
-    lastCheckGeneration = generation;
-  }
-
-  boolean needsRefresh(long generation) {
-    if (generation <= 0) {
-      return isRevisionOutOfDate();
-    }
-    if (lastCheckGeneration != generation) {
-      lastCheckGeneration = generation;
-      return isRevisionOutOfDate();
-    }
-    return false;
-  }
-
-  private boolean isRevisionOutOfDate() {
-    try (Repository git = gitMgr.openRepository(getNameKey())) {
-      Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG);
-      if (ref == null || ref.getObjectId() == null) {
-        return true;
-      }
-      return !ref.getObjectId().equals(config.getRevision());
-    } catch (IOException gone) {
-      return true;
-    }
-  }
-
   /**
    * @return cached computation of all global capabilities. This should only be invoked on the state
    *     from {@link ProjectCache#getAllProjects()}. Null on any other project.
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 69f1a4e..01d2c31 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -675,7 +675,9 @@
   public ReviewerSet reviewers() {
     if (reviewers == null) {
       if (!lazyLoad) {
-        return ReviewerSet.empty();
+        // We are not allowed to load values from NoteDb. Reviewers were not populated with values
+        // from the index. However, we need these values for permission checks.
+        throw new IllegalStateException("reviewers not populated");
       }
       reviewers = approvalsUtil.getReviewers(notes(), approvals().values());
     }
@@ -686,10 +688,6 @@
     this.reviewers = reviewers;
   }
 
-  public ReviewerSet getReviewers() {
-    return reviewers;
-  }
-
   public ReviewerByEmailSet reviewersByEmail() {
     if (reviewersByEmail == null) {
       if (!lazyLoad) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 40a3a07..c6bcd60 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -73,7 +73,6 @@
       return false;
     }
 
-    ChangeNotes notes = notesFactory.createFromIndexedChange(change);
     Optional<ProjectState> projectState = projectCache.get(cd.project());
     if (!projectState.isPresent()) {
       logger.atFine().log("Filter out change %s of non-existing project %s", cd, cd.project());
@@ -92,7 +91,7 @@
                     .filter(u -> u instanceof SingleGroupUser || u instanceof InternalUser)
                     .orElseGet(anonymousUserProvider::get));
     try {
-      withUser.indexedChange(cd, notes).check(ChangePermission.READ);
+      withUser.change(cd).check(ChangePermission.READ);
     } catch (PermissionBackendException e) {
       Throwable cause = e.getCause();
       if (cause instanceof RepositoryNotFoundException) {
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index fee5eab..89b931f 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -80,6 +81,7 @@
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
   private final PutPreferred putPreferred;
   private final OutgoingEmailValidator validator;
+  private final MessageIdGenerator messageIdGenerator;
   private final boolean isDevMode;
 
   @Inject
@@ -91,7 +93,8 @@
       AccountManager accountManager,
       RegisterNewEmailSender.Factory registerNewEmailFactory,
       PutPreferred putPreferred,
-      OutgoingEmailValidator validator) {
+      OutgoingEmailValidator validator,
+      MessageIdGenerator messageIdGenerator) {
     this.self = self;
     this.realm = realm;
     this.permissionBackend = permissionBackend;
@@ -100,6 +103,7 @@
     this.putPreferred = putPreferred;
     this.validator = validator;
     this.isDevMode = authConfig.getAuthType() == DEVELOPMENT_BECOME_ANY_ACCOUNT;
+    this.messageIdGenerator = messageIdGenerator;
   }
 
   @Override
@@ -161,6 +165,7 @@
         if (!sender.isAllowed()) {
           throw new MethodNotAllowedException("Not allowed to add email address " + email);
         }
+        sender.setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
         sender.send();
         info.pendingConfirmation = true;
       } catch (EmailException | RuntimeException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 5b7245d..42032f7 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.common.base.Strings;
@@ -73,6 +74,9 @@
       throw new BadRequestException("path must be non-empty");
     } else if (in.message == null || in.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
+    } else if (in.path.equals(PATCHSET_LEVEL)
+        && (in.side != null || in.range != null || in.line != null)) {
+      throw new BadRequestException("patchset-level comments can't have side, range, or line");
     } else if (in.line != null && in.line < 0) {
       throw new BadRequestException("line must be >= 0");
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 4c39763..bfe7177 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.change.VoteResource;
 import com.google.gerrit.server.extensions.events.VoteDeleted;
 import com.google.gerrit.server.mail.send.DeleteVoteSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.ReplyToChangeSender;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -77,6 +78,7 @@
   private final NotifyResolver notifyResolver;
   private final RemoveReviewerControl removeReviewerControl;
   private final ProjectCache projectCache;
+  private final MessageIdGenerator messageIdGenerator;
 
   @Inject
   DeleteVote(
@@ -89,7 +91,8 @@
       DeleteVoteSender.Factory deleteVoteSenderFactory,
       NotifyResolver notifyResolver,
       RemoveReviewerControl removeReviewerControl,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      MessageIdGenerator messageIdGenerator) {
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.psUtil = psUtil;
@@ -100,6 +103,7 @@
     this.notifyResolver = notifyResolver;
     this.removeReviewerControl = removeReviewerControl;
     this.projectCache = projectCache;
+    this.messageIdGenerator = messageIdGenerator;
   }
 
   @Override
@@ -229,6 +233,8 @@
           cm.setFrom(user.getAccountId());
           cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
           cm.setNotify(notify);
+          cm.setMessageId(
+              messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
           cm.send();
         }
       } catch (Exception e) {
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
index e544509..409b0d5 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -73,7 +73,7 @@
       throws PermissionBackendException {
     ImmutableList<CommentInfo> commentInfos = getCommentFormatter().formatAsList(comments);
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, true);
     return commentInfos;
   }
 
@@ -83,7 +83,7 @@
     List<CommentInfo> commentInfos =
         commentInfosMap.values().stream().flatMap(List::stream).collect(toList());
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, true);
     return commentInfosMap;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index 0ed7d60..d841183 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -63,7 +63,7 @@
     List<RobotCommentInfo> commentInfos =
         robotCommentsMap.values().stream().flatMap(List::stream).collect(toList());
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
     return Response.ok(robotCommentsMap);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index 742eaca..25f4005 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -64,7 +64,7 @@
       Iterable<RobotComment> comments, RevisionResource rsrc) throws PermissionBackendException {
     ImmutableList<RobotCommentInfo> commentInfos = getCommentFormatter().formatAsList(comments);
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
     return commentInfos;
   }
 
@@ -74,7 +74,7 @@
     List<RobotCommentInfo> commentInfos =
         commentInfosMap.values().stream().flatMap(List::stream).collect(toList());
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
     return commentInfosMap;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 7008bb9..a16d4f9 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
@@ -48,11 +49,11 @@
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.FixReplacement;
 import com.google.gerrit.entities.FixSuggestion;
-import com.google.gerrit.entities.LabelId;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.RobotComment;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -606,6 +607,7 @@
         ensureLineIsNonNegative(comment.line, path);
         ensureCommentNotOnMagicFilesOfAutoMerge(path, comment);
         ensureRangeIsValid(path, comment.range);
+        ensureValidPatchsetLevelComment(path, comment);
       }
     }
   }
@@ -644,6 +646,14 @@
     }
   }
 
+  private static <T extends CommentInput> void ensureValidPatchsetLevelComment(
+      String path, T comment) throws BadRequestException {
+    if (path.equals(PATCHSET_LEVEL)
+        && (comment.side != null || comment.range != null || comment.line != null)) {
+      throw new BadRequestException("Patchset-level comments can't have side, range, or line");
+    }
+  }
+
   private void checkRobotComments(
       RevisionResource revision, Map<String, List<RobotCommentInput>> in)
       throws BadRequestException, PatchListNotAvailableException {
@@ -703,7 +713,7 @@
     ensureReplacementsArePresent(commentPath, fixReplacementInfos);
 
     for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) {
-      ensureReplacementPathIsSet(commentPath, fixReplacementInfo.path);
+      ensureReplacementPathIsSetAndNotPatchsetLevel(commentPath, fixReplacementInfo.path);
       ensureRangeIsSet(commentPath, fixReplacementInfo.range);
       ensureRangeIsValid(commentPath, fixReplacementInfo.range);
       ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement);
@@ -727,14 +737,20 @@
     }
   }
 
-  private static void ensureReplacementPathIsSet(String commentPath, String replacementPath)
-      throws BadRequestException {
+  private static void ensureReplacementPathIsSetAndNotPatchsetLevel(
+      String commentPath, String replacementPath) throws BadRequestException {
     if (replacementPath == null) {
       throw new BadRequestException(
           String.format(
               "A file path must be given for the replacement of the robot comment on %s",
               commentPath));
     }
+    if (replacementPath.equals(PATCHSET_LEVEL)) {
+      throw new BadRequestException(
+          String.format(
+              "A file path must not be %s for the replacement of the robot comment on %s",
+              PATCHSET_LEVEL, commentPath));
+    }
   }
 
   private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException {
@@ -888,9 +904,23 @@
       }
       NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
       if (notify.shouldNotify()) {
-        email
-            .create(notify, notes, ps, user, message, comments, in.message, labelDelta)
-            .sendAsync();
+        try {
+          email
+              .create(
+                  notify,
+                  notes,
+                  ps,
+                  user,
+                  message,
+                  comments,
+                  in.message,
+                  labelDelta,
+                  ctx.getRepoView())
+              .sendAsync();
+        } catch (IOException ex) {
+          throw new StorageException(
+              String.format("Repository %s not found", ctx.getProject().get()), ex);
+        }
       }
       commentAdded.fire(
           notes.getChange(),
@@ -1255,8 +1285,6 @@
         return false;
       }
 
-      forceCallerAsReviewer(projectState, ctx, current, ups, del);
-
       return !del.isEmpty() || !ups.isEmpty();
     }
 
@@ -1327,41 +1355,6 @@
       }
     }
 
-    private void forceCallerAsReviewer(
-        ProjectState projectState,
-        ChangeContext ctx,
-        Map<String, PatchSetApproval> current,
-        List<PatchSetApproval> ups,
-        List<PatchSetApproval> del) {
-      if (current.isEmpty() && ups.isEmpty()) {
-        // TODO Find another way to link reviewers to changes.
-        if (del.isEmpty()) {
-          // If no existing label is being set to 0, hack in the caller
-          // as a reviewer by picking the first server-wide LabelType.
-          List<LabelType> labelTypes = projectState.getLabelTypes(ctx.getNotes()).getLabelTypes();
-          if (labelTypes.isEmpty()) {
-            logger.atWarning().log(
-                "no label type found for project %s, change %s",
-                projectState.getName(), ctx.getChange().getChangeId());
-            return;
-          }
-
-          LabelId labelId = labelTypes.get(0).getLabelId();
-          ups.add(
-              ApprovalsUtil.newApproval(psId, user, labelId, 0, ctx.getWhen())
-                  .tag(Optional.ofNullable(in.tag))
-                  .granted(ctx.getWhen())
-                  .build());
-        } else {
-          // Pick a random label that is about to be deleted and keep it.
-          Iterator<PatchSetApproval> i = del.iterator();
-          ups.add(i.next().toBuilder().value(0).granted(ctx.getWhen()).build());
-          i.remove();
-        }
-      }
-      ctx.getUpdate(ctx.getChange().currentPatchSetId()).putReviewer(user.getAccountId(), REVIEWER);
-    }
-
     private Map<String, PatchSetApproval> scanLabels(
         ProjectState projectState, ChangeContext ctx, List<PatchSetApproval> del)
         throws IOException {
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 63cd7a3..ea58365 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.gerrit.entities.Comment;
@@ -79,6 +80,9 @@
       throw new BadRequestException("id must match URL");
     } else if (in.line != null && in.line < 0) {
       throw new BadRequestException("line must be >= 0");
+    } else if (in.path.equals(PATCHSET_LEVEL)
+        && (in.side != null || in.range != null || in.line != null)) {
+      throw new BadRequestException("patchset-level comments can't have side, range, or line");
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
       throw new BadRequestException("range endLine must be on the same line as the comment");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index a72192e..d1506b7 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.extensions.events.ChangeRestored;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.ReplyToChangeSender;
 import com.google.gerrit.server.mail.send.RestoredSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -65,6 +66,7 @@
   private final PatchSetUtil psUtil;
   private final ChangeRestored changeRestored;
   private final ProjectCache projectCache;
+  private final MessageIdGenerator messageIdGenerator;
 
   @Inject
   Restore(
@@ -74,7 +76,8 @@
       ChangeMessagesUtil cmUtil,
       PatchSetUtil psUtil,
       ChangeRestored changeRestored,
-      ProjectCache projectCache) {
+      ProjectCache projectCache,
+      MessageIdGenerator messageIdGenerator) {
     this.updateFactory = updateFactory;
     this.restoredSenderFactory = restoredSenderFactory;
     this.json = json;
@@ -82,6 +85,7 @@
     this.psUtil = psUtil;
     this.changeRestored = changeRestored;
     this.projectCache = projectCache;
+    this.messageIdGenerator = messageIdGenerator;
   }
 
   @Override
@@ -149,6 +153,8 @@
         ReplyToChangeSender cm = restoredSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
         cm.setChangeMessage(message.getMessage(), ctx.getWhen());
+        cm.setMessageId(
+            messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
         cm.send();
       } catch (Exception e) {
         logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 88db66e..e2b898f 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.server.extensions.events.ChangeReverted;
 import com.google.gerrit.server.git.CommitUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
 import com.google.gerrit.server.mail.send.RevertedSender;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.Sequences;
@@ -126,6 +127,7 @@
   private final BatchUpdate.Factory updateFactory;
   private final ChangeResource.Factory changeResourceFactory;
   private final GetRelated getRelated;
+  private final MessageIdGenerator messageIdGenerator;
 
   private CherryPickInput cherryPickInput;
   private List<ChangeInfo> results;
@@ -154,7 +156,8 @@
       NotifyResolver notifyResolver,
       BatchUpdate.Factory updateFactory,
       ChangeResource.Factory changeResourceFactory,
-      GetRelated getRelated) {
+      GetRelated getRelated,
+      MessageIdGenerator messageIdGenerator) {
     this.queryProvider = queryProvider;
     this.user = user;
     this.permissionBackend = permissionBackend;
@@ -175,6 +178,7 @@
     this.updateFactory = updateFactory;
     this.changeResourceFactory = changeResourceFactory;
     this.getRelated = getRelated;
+    this.messageIdGenerator = messageIdGenerator;
     results = new ArrayList<>();
     cherryPickInput = null;
   }
@@ -601,6 +605,8 @@
         RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
         cm.setFrom(ctx.getAccountId());
         cm.setNotify(ctx.getNotify(change.getId()));
+        cm.setMessageId(
+            messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
         cm.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
index 83b0262..8185281 100644
--- a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.DefaultPreferencesCache;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +41,7 @@
   public Response<DiffPreferencesInfo> apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
     return Response.ok(
-        StoredPreferences.parseDiffPreferences(
+        PreferencesParserUtil.parseDiffPreferences(
             defaultPreferenceCache.get().asConfig(), null, null));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
index 95fc10e..bb9e483 100644
--- a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.DefaultPreferencesCache;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -40,7 +40,7 @@
   public Response<EditPreferencesInfo> apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
     return Response.ok(
-        StoredPreferences.parseEditPreferences(
+        PreferencesParserUtil.parseEditPreferences(
             defaultPreferenceCache.get().asConfig(), null, null));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetPreferences.java b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
index 8a28d55..288055b 100644
--- a/java/com/google/gerrit/server/restapi/config/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.DefaultPreferencesCache;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -38,7 +38,7 @@
   public Response<GeneralPreferencesInfo> apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
     return Response.ok(
-        StoredPreferences.parseGeneralPreferences(
+        PreferencesParserUtil.parseGeneralPreferences(
             defaultPreferenceCache.get().asConfig(), null, null));
   }
 }
diff --git a/java/com/google/gerrit/server/ssh/SshAddressesModule.java b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
index 0a6bcac..1159e06 100644
--- a/java/com/google/gerrit/server/ssh/SshAddressesModule.java
+++ b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -40,7 +40,7 @@
   @Provides
   @Singleton
   @SshListenAddresses
-  public List<SocketAddress> getListenAddresses(@GerritServerConfig Config cfg) {
+  public List<SocketAddress> provideListenAddresses(@GerritServerConfig Config cfg) {
     List<SocketAddress> listen = Lists.newArrayListWithExpectedSize(2);
     String[] want = cfg.getStringList("sshd", null, "listenaddress");
     if (want == null || want.length == 0) {
@@ -71,7 +71,7 @@
   @Provides
   @Singleton
   @SshAdvertisedAddresses
-  List<String> getAdvertisedAddresses(
+  List<String> provideAdvertisedAddresses(
       @GerritServerConfig Config cfg, @SshListenAddresses List<SocketAddress> listen) {
     String[] want = cfg.getStringList("sshd", null, "advertisedaddress");
     if (want.length > 0) {
diff --git a/java/com/google/gerrit/server/submit/BranchTips.java b/java/com/google/gerrit/server/submit/BranchTips.java
new file mode 100644
index 0000000..d42517c
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/BranchTips.java
@@ -0,0 +1,62 @@
+// 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.
+
+package com.google.gerrit.server.submit;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Ref;
+
+/**
+ * Current branch tips, taking into account commits created during the submit process as well as
+ * submodule updates produced by this class.
+ */
+class BranchTips {
+
+  private final Map<BranchNameKey, CodeReviewCommit> branchTips = new HashMap<>();
+
+  /**
+   * Returns current tip of the branch, taking into account commits created during the submit
+   * process or submodule updates.
+   *
+   * @param branch branch
+   * @param repo repository to look for the branch if not cached
+   * @return the current tip. Empty if the branch doesn't exist in the repository
+   * @throws IOException Cannot access the underlying storage
+   */
+  Optional<CodeReviewCommit> getTip(BranchNameKey branch, OpenRepo repo) throws IOException {
+    CodeReviewCommit currentCommit;
+    if (branchTips.containsKey(branch)) {
+      currentCommit = branchTips.get(branch);
+    } else {
+      Ref r = repo.repo.exactRef(branch.branch());
+      if (r == null) {
+        return Optional.empty();
+      }
+      currentCommit = repo.rw.parseCommit(r.getObjectId());
+      branchTips.put(branch, currentCommit);
+    }
+
+    return Optional.of(currentCommit);
+  }
+
+  void put(BranchNameKey branch, CodeReviewCommit c) {
+    branchTips.put(branch, c);
+  }
+}
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index c94d49e..c433ee6 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -24,6 +24,8 @@
 import com.google.gerrit.server.change.NotifyResolver;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.send.MergedSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.update.RepoView;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
@@ -38,20 +40,23 @@
   interface Factory {
     EmailMerge create(
         Project.NameKey project,
-        Change.Id changeId,
+        Change change,
         Account.Id submitter,
-        NotifyResolver.Result notify);
+        NotifyResolver.Result notify,
+        RepoView repoView);
   }
 
   private final ExecutorService sendEmailsExecutor;
   private final MergedSender.Factory mergedSenderFactory;
   private final ThreadLocalRequestContext requestContext;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final MessageIdGenerator messageIdGenerator;
 
   private final Project.NameKey project;
-  private final Change.Id changeId;
+  private final Change change;
   private final Account.Id submitter;
   private final NotifyResolver.Result notify;
+  private final RepoView repoView;
 
   @Inject
   EmailMerge(
@@ -59,18 +64,22 @@
       MergedSender.Factory mergedSenderFactory,
       ThreadLocalRequestContext requestContext,
       IdentifiedUser.GenericFactory identifiedUserFactory,
+      MessageIdGenerator messageIdGenerator,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id changeId,
+      @Assisted Change change,
       @Assisted @Nullable Account.Id submitter,
-      @Assisted NotifyResolver.Result notify) {
+      @Assisted NotifyResolver.Result notify,
+      @Assisted RepoView repoView) {
     this.sendEmailsExecutor = executor;
     this.mergedSenderFactory = mergedSenderFactory;
     this.requestContext = requestContext;
     this.identifiedUserFactory = identifiedUserFactory;
+    this.messageIdGenerator = messageIdGenerator;
     this.project = project;
-    this.changeId = changeId;
+    this.change = change;
     this.submitter = submitter;
     this.notify = notify;
+    this.repoView = repoView;
   }
 
   void sendAsync() {
@@ -82,14 +91,15 @@
   public void run() {
     RequestContext old = requestContext.setContext(this);
     try {
-      MergedSender cm = mergedSenderFactory.create(project, changeId);
+      MergedSender cm = mergedSenderFactory.create(project, change.getId());
       if (submitter != null) {
         cm.setFrom(submitter);
       }
       cm.setNotify(notify);
+      cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
       cm.send();
     } catch (Exception e) {
-      logger.atSevere().withCause(e).log("Cannot email merged notification for %s", changeId);
+      logger.atSevere().withCause(e).log("Cannot email merged notification for %s", change.getId());
     } finally {
       requestContext.setContext(old);
     }
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 33c3584..edc3725 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.BooleanProjectConfig;
@@ -62,20 +63,42 @@
     } catch (IOException | StorageException e) {
       throw new StorageException("Commit sorting failed", e);
     }
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
-    boolean first = true;
 
+    // We cannot rebase merge commits. This is why we integrate merge changes into the target branch
+    // the same way as if MERGE_IF_NECESSARY was the submit strategy. This means if needed we create
+    // a merge commit that integrates the merge change into the target branch.
+    // If we integrate a change series that consists out of a normal change and a merge change,
+    // where the merge change depends on the normal change, we must skip rebasing the normal change,
+    // because it already gets integrated by merging the merge change. If the rebasing of the normal
+    // change is not skipped, it would appear twice in the history after the submit is done (once
+    // through its rebased commit, and once through its original commit which is a parent of the
+    // merge change that was merged into the target branch. To skip the rebasing of the normal
+    // change, we call MergeUtil#reduceToMinimalMerge, as it excludes commits which will be
+    // implicitly integrated by merging the series. Then we use the MergeIfNecessaryOp to integrate
+    // the whole series.
+    // If on the other hand, we integrate a change series that consists out of a merge change and a
+    // normal change, where the normal change depends on the merge change, we can first integrate
+    // the merge change by a merge and then integrate the normal change by a rebase. In this case we
+    // do not want to call MergeUtil#reduceToMinimalMerge as we are not intending to integrate the
+    // whole series by a merge, but rather do the integration of the commits one by one.
+    boolean foundNonMerge = false;
     for (CodeReviewCommit c : sorted) {
       if (c.getParentCount() > 1) {
-        // Since there is a merge commit, sort and prune again using
-        // MERGE_IF_NECESSARY semantics to avoid creating duplicate
-        // commits.
-        //
+        if (!foundNonMerge) {
+          // found a merge change, but it doesn't depend on a normal change, this means we are not
+          // required to merge the whole series at once
+          continue;
+        }
+        // found a merge commit that depends on a normal change, this means we are required to merge
+        // the whole series at once
         sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
-        break;
+        return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toList());
       }
+      foundNonMerge = true;
     }
 
+    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    boolean first = true;
     while (!sorted.isEmpty()) {
       CodeReviewCommit n = sorted.remove(0);
       if (first && args.mergeTip.getInitialTip() == null) {
@@ -87,7 +110,7 @@
       } else if (n.getParentCount() == 1) {
         ops.add(new RebaseOneOp(n));
       } else {
-        ops.add(new RebaseMultipleParentsOp(n));
+        ops.add(new MergeIfNecessaryOp(n));
       }
       first = false;
     }
@@ -254,8 +277,8 @@
     }
   }
 
-  private class RebaseMultipleParentsOp extends SubmitStrategyOp {
-    private RebaseMultipleParentsOp(CodeReviewCommit toMerge) {
+  private class MergeIfNecessaryOp extends SubmitStrategyOp {
+    private MergeIfNecessaryOp(CodeReviewCommit toMerge) {
       super(RebaseSubmitStrategy.this.args, toMerge);
     }
 
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index a4141be..5558c74 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GroupCollector;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
@@ -398,7 +399,7 @@
     Optional<Account> account =
         args.accountCache.get(submitter.accountId()).map(AccountState::account);
     if (account.isPresent() && account.get().fullName() != null) {
-      return " by " + account.get().fullName();
+      return " by " + ChangeNoteUtil.getAccountIdAsUsername(account.get().id());
     }
     return "";
   }
@@ -500,7 +501,12 @@
     // have failed fast in one of the other steps.
     try {
       args.mergedSenderFactory
-          .create(ctx.getProject(), getId(), submitter.accountId(), ctx.getNotify(getId()))
+          .create(
+              ctx.getProject(),
+              toMerge.change(),
+              submitter.accountId(),
+              ctx.getNotify(getId()),
+              ctx.getRepoView())
           .sendAsync();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
@@ -549,7 +555,7 @@
 
     // Modify the commit with gitlink update
     try {
-      return args.submoduleOp.composeGitlinksCommit(args.destBranch, commit);
+      return args.submoduleOp.amendGitlinksCommit(args.destBranch, commit);
     } catch (IOException e) {
       throw new StorageException(
           String.format("cannot update gitlink for the commit at branch %s", args.destBranch), e);
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index b48076194..6854e90 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -58,6 +58,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.dircache.DirCache;
@@ -82,9 +83,11 @@
   /** Only used for branches without code review changes */
   public class GitlinkOp implements RepoOnlyOp {
     private final BranchNameKey branch;
+    private final BranchTips currentBranchTips;
 
-    GitlinkOp(BranchNameKey branch) {
+    GitlinkOp(BranchNameKey branch, BranchTips branchTips) {
       this.branch = branch;
+      this.currentBranchTips = branchTips;
     }
 
     @Override
@@ -92,7 +95,7 @@
       CodeReviewCommit c = composeGitlinksCommit(branch);
       if (c != null) {
         ctx.addRefUpdate(c.getParent(0), c, branch.branch());
-        addBranchTip(branch, c);
+        currentBranchTips.put(branch, c);
       }
     }
   }
@@ -137,12 +140,6 @@
   private final ImmutableSet<BranchNameKey> updatedBranches;
 
   /**
-   * Current branch tips, taking into account commits created during the submit process as well as
-   * submodule updates produced by this class.
-   */
-  private final Map<BranchNameKey, CodeReviewCommit> branchTips;
-
-  /**
    * Branches in a superproject that contain submodule subscriptions, plus branches in submodules
    * which are subscribed to by some superproject.
    */
@@ -154,12 +151,16 @@
   /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
   private final SetMultimap<BranchNameKey, SubmoduleSubscription> targets;
 
+  private final BranchTips branchTips = new BranchTips();
   /**
    * Multimap of superproject name to all branch names within that superproject which have submodule
    * subscriptions.
    */
   private final SetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
 
+  /** All branches subscribed by other projects. */
+  private final Set<BranchNameKey> subscribedBranches;
+
   private SubmoduleOp(
       GitModules.Factory gitmodulesFactory,
       PersonIdent myIdent,
@@ -182,9 +183,9 @@
     this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
     this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
     this.affectedBranches = new HashSet<>();
-    this.branchTips = new HashMap<>();
     this.branchGitModules = new HashMap<>();
     this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build();
+    this.subscribedBranches = new HashSet<>();
     this.sortedBranches = calculateSubscriptionMaps();
   }
 
@@ -197,6 +198,7 @@
    *   <li>{@link #affectedBranches}
    *   <li>{@link #targets}
    *   <li>{@link #branchesByProject}
+   *   <li>{@link #subscribedBranches}
    * </ul>
    *
    * @return the ordered set to be stored in {@link #sortedBranches}.
@@ -269,6 +271,7 @@
         branchesByProject.put(superBranch.project(), superBranch);
         affectedBranches.add(superBranch);
         affectedBranches.add(sub.getSubmodule());
+        subscribedBranches.add(sub.getSubmodule());
       }
     } catch (IOException e) {
       throw new StorageException("Cannot find superprojects for " + current, e);
@@ -358,8 +361,7 @@
     return ret;
   }
 
-  @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
-  public Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
+  private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
       BranchNameKey srcBranch) throws IOException {
     logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
     Collection<SubmoduleSubscription> ret = new ArrayList<>();
@@ -397,6 +399,11 @@
     return ret;
   }
 
+  @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
+  public boolean hasSuperproject(BranchNameKey branch) {
+    return subscribedBranches.contains(branch);
+  }
+
   public void updateSuperProjects() throws RestApiException {
     ImmutableSet<Project.NameKey> projects = getProjectsInOrder();
     if (projects == null) {
@@ -432,18 +439,13 @@
       throw new StorageException("Cannot access superproject", e);
     }
 
-    CodeReviewCommit currentCommit;
-    if (branchTips.containsKey(subscriber)) {
-      currentCommit = branchTips.get(subscriber);
-    } else {
-      Ref r = or.repo.exactRef(subscriber.branch());
-      if (r == null) {
-        throw new SubmoduleConflictException(
-            "The branch was probably deleted from the subscriber repository");
-      }
-      currentCommit = or.rw.parseCommit(r.getObjectId());
-      addBranchTip(subscriber, currentCommit);
-    }
+    CodeReviewCommit currentCommit =
+        branchTips
+            .getTip(subscriber, or)
+            .orElseThrow(
+                () ->
+                    new SubmoduleConflictException(
+                        "The branch was probably deleted from the subscriber repository"));
 
     StringBuilder msgbuf = new StringBuilder();
     PersonIdent author = null;
@@ -493,7 +495,7 @@
   }
 
   /** Amend an existing commit with gitlink updates */
-  CodeReviewCommit composeGitlinksCommit(BranchNameKey subscriber, CodeReviewCommit currentCommit)
+  CodeReviewCommit amendGitlinksCommit(BranchNameKey subscriber, CodeReviewCommit currentCommit)
       throws IOException, SubmoduleConflictException {
     OpenRepo or;
     try {
@@ -574,25 +576,14 @@
       }
     }
 
-    final CodeReviewCommit newCommit;
-    if (branchTips.containsKey(s.getSubmodule())) {
-      // This submodule's branch was updated as part of this specific submit batch: update the
-      // gitlink to point to the new commit from the batch.
-      newCommit = branchTips.get(s.getSubmodule());
-    } else {
-      // For whatever reason, this submodule was not updated as part of this submit batch, but the
-      // superproject is still subscribed to this branch. Re-read the ref to see if anything has
-      // changed since the last time the gitlink was updated, and roll that update into the same
-      // commit as all other submodule updates.
-      Ref ref = subOr.repo.getRefDatabase().exactRef(s.getSubmodule().branch());
-      if (ref == null) {
-        ed.add(new DeletePath(s.getPath()));
-        return null;
-      }
-      newCommit = subOr.rw.parseCommit(ref.getObjectId());
-      addBranchTip(s.getSubmodule(), newCommit);
+    Optional<CodeReviewCommit> maybeNewCommit = branchTips.getTip(s.getSubmodule(), subOr);
+    if (!maybeNewCommit.isPresent()) {
+      // This submodule branch is neither in the submit set nor in the repository itself
+      ed.add(new DeletePath(s.getPath()));
+      return null;
     }
 
+    CodeReviewCommit newCommit = maybeNewCommit.get();
     if (Objects.equals(newCommit, oldCommit)) {
       // gitlink have already been updated for this submodule
       return null;
@@ -737,6 +728,6 @@
   }
 
   void addOp(BatchUpdate bu, BranchNameKey branch) {
-    bu.addRepoOnlyOp(new GitlinkOp(branch));
+    bu.addRepoOnlyOp(new GitlinkOp(branch, branchTips));
   }
 }
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 8800463..a682d33 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.testing;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.base.Strings;
-import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
 import com.google.gerrit.extensions.client.AuthType;
@@ -30,6 +31,7 @@
 import com.google.gerrit.index.project.ProjectSchemaDefinitions;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.CacheRefreshExecutor;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -209,7 +211,7 @@
           @Singleton
           @DiffExecutor
           public ExecutorService createDiffExecutor() {
-            return MoreExecutors.newDirectExecutorService();
+            return newDirectExecutorService();
           }
         });
     install(new DefaultMemoryCacheModule());
@@ -277,7 +279,7 @@
   @Singleton
   @SendEmailExecutor
   public ExecutorService createSendEmailExecutor() {
-    return MoreExecutors.newDirectExecutorService();
+    return newDirectExecutorService();
   }
 
   @Provides
@@ -287,6 +289,13 @@
     return queues.createQueue(2, "FanOut");
   }
 
+  @Provides
+  @Singleton
+  @CacheRefreshExecutor
+  public ListeningExecutorService createCacheRefreshExecutor() {
+    return newDirectExecutorService();
+  }
+
   private Module luceneIndexModule() {
     return indexModule("com.google.gerrit.lucene.LuceneIndexModule");
   }
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
index 5ce6d13..bd859db 100644
--- a/java/com/google/gerrit/testing/TestCommentHelper.java
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.Collection;
@@ -116,7 +117,6 @@
     RobotCommentInput in = new RobotCommentInput();
     in.robotId = "happyRobot";
     in.robotRunId = "1";
-    in.line = 1;
     in.message = "nit: trailing whitespace";
     in.path = path;
     return in;
@@ -144,6 +144,7 @@
     reviewInput.robotComments =
         Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
     reviewInput.message = message;
+    reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
     gApi.changes().id(targetChangeId).current().review(reviewInput);
   }
 }
diff --git a/java/com/google/gerrit/truth/MapSubject.java b/java/com/google/gerrit/truth/MapSubject.java
index 8217920..95a0e0c 100644
--- a/java/com/google/gerrit/truth/MapSubject.java
+++ b/java/com/google/gerrit/truth/MapSubject.java
@@ -1,18 +1,16 @@
-/*
- * 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.
- */
+// 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.
 
 package com.google.gerrit.truth;
 
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
new file mode 100644
index 0000000..8e08b1c
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -0,0 +1,138 @@
+// 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.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.EmailHeader;
+import com.google.gerrit.testing.FakeEmailSender;
+import java.net.URL;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class OutgoingEmailIT extends AbstractDaemonTest {
+
+  @Test
+  public void messageIdHeaderFromChangeUpdate() throws Exception {
+    Repository repository = repoManager.openRepository(project);
+    PushOneCommit.Result result = createChange();
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email();
+    gApi.changes().id(result.getChangeId()).addReviewer(addReviewerInput);
+    sender.clear();
+
+    gApi.changes().id(result.getChangeId()).abandon();
+    assertThat(getMessageId(sender))
+        .isEqualTo(
+            withPrefixAndSuffixForMessageId(
+                repository
+                        .getRefDatabase()
+                        .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+                        .getObjectId()
+                        .getName()
+                    + "-HTML"));
+    sender.clear();
+
+    gApi.changes().id(result.getChangeId()).restore();
+    assertThat(getMessageId(sender))
+        .isEqualTo(
+            withPrefixAndSuffixForMessageId(
+                repository
+                        .getRefDatabase()
+                        .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+                        .getObjectId()
+                        .getName()
+                    + "-HTML"));
+  }
+
+  @Test
+  @GerritConfig(
+      name = "auth.registerEmailPrivateKey",
+      value = "HsOc6l_2lhS9G7sE_RsnS7Z6GJjdRDX14co=")
+  public void messageIdHeaderFromAccountUpdate() throws Exception {
+    Repository allUsersRepo = repoManager.openRepository(allUsers);
+    String email = "new.email@example.com";
+    EmailInput input = new EmailInput();
+    input.email = email;
+    sender.clear();
+    gApi.accounts().self().addEmail(input);
+
+    assertThat(sender.getMessages()).hasSize(1);
+    FakeEmailSender.Message m = sender.getMessages().get(0);
+    assertThat(m.rcpt()).containsExactly(Address.create(email));
+
+    assertThat(getMessageId(sender))
+        .isEqualTo(
+            withPrefixAndSuffixForMessageId(
+                allUsersRepo
+                        .getRefDatabase()
+                        .exactRef(RefNames.refsUsers(admin.id()))
+                        .getObjectId()
+                        .getName()
+                    + "-HTML"));
+  }
+
+  @Test
+  public void messageIdHeaderFromPasswordUpdate() throws Exception {
+    sender.clear();
+    String newPassword = gApi.accounts().self().generateHttpPassword();
+    assertThat(newPassword).isNotNull();
+    assertThat(getMessageId(sender))
+        .containsMatch("<HTTP_password_change-" + admin.id().toString() + ".*@.*>");
+  }
+
+  @Test
+  public void htmlAndPlainTextSuffixAddedToMessageId() throws Exception {
+    PushOneCommit.Result result = createChange();
+    GeneralPreferencesInfo generalPreferencesInfo = new GeneralPreferencesInfo();
+    generalPreferencesInfo.emailFormat = GeneralPreferencesInfo.EmailFormat.PLAINTEXT;
+    gApi.accounts().id(user.id().get()).setPreferences(generalPreferencesInfo);
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email();
+    gApi.changes().id(result.getChangeId()).addReviewer(addReviewerInput);
+    sender.clear();
+
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+    assertThat(getMessageId(sender)).contains("-PLAIN");
+    sender.clear();
+
+    generalPreferencesInfo.emailFormat = GeneralPreferencesInfo.EmailFormat.HTML_PLAINTEXT;
+    gApi.accounts().id(user.id().get()).setPreferences(generalPreferencesInfo);
+    sender.clear();
+
+    gApi.changes().id(result.getChangeId()).current().review(ReviewInput.reject());
+    assertThat(getMessageId(sender)).contains("-HTML");
+  }
+
+  private static String getMessageId(FakeEmailSender sender) {
+    return ((EmailHeader.String)
+            (Iterables.getOnlyElement(sender.getMessages()).headers().get("Message-ID")))
+        .getString();
+  }
+
+  // Each message-id must start with '<' and end with '>'. Also, it must contain no spaces and it
+  // must contain a '@'.
+  private String withPrefixAndSuffixForMessageId(String id) throws Exception {
+    return "<" + id + "@" + new URL(canonicalWebUrl.get()).getHost() + ">";
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
new file mode 100644
index 0000000..a8fd834
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -0,0 +1,81 @@
+// 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.
+
+package com.google.gerrit.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.time.Instant;
+import javax.inject.Inject;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class MessageIdGeneratorIT extends AbstractDaemonTest {
+  @Inject private MessageIdGenerator messageIdGenerator;
+
+  @Test
+  public void fromAccountUpdate() throws Exception {
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      String messageId = messageIdGenerator.fromAccountUpdate(admin.id()).id();
+      String sha1 =
+          repo.getRefDatabase().findRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
+      assertThat(sha1).isEqualTo(messageId);
+    }
+  }
+
+  @Test
+  public void fromChangeUpdate() throws Exception {
+    try (Repository repo = repoManager.openRepository(project)) {
+      PushOneCommit.Result result = createChange();
+      PatchSet.Id patchsetId = result.getChange().currentPatchSet().id();
+      String messageId = messageIdGenerator.fromChangeUpdate(project, patchsetId).id();
+      String sha1 =
+          repo.getRefDatabase()
+              .findRef(String.format("%smeta", patchsetId.changeId().toRefPrefix()))
+              .getObjectId()
+              .getName();
+      assertThat(sha1).isEqualTo(messageId);
+    }
+  }
+
+  @Test
+  public void fromMailMessage() throws Exception {
+    String id = "unique-id";
+    MailMessage mailMessage =
+        MailMessage.builder()
+            .id(id)
+            .from(Address.create("email@email.com"))
+            .dateReceived(Instant.EPOCH)
+            .subject("subject")
+            .build();
+    assertThat(messageIdGenerator.fromMailMessage(mailMessage).id()).isEqualTo(id + "-REJECTION");
+  }
+
+  @Test
+  public void fromReasonAccountIdAndTimestamp() throws Exception {
+    String reason = "reason";
+    Instant timestamp = TimeUtil.now();
+    assertThat(
+            messageIdGenerator.fromReasonAccountIdAndTimestamp(reason, admin.id(), timestamp).id())
+        .isEqualTo(reason + "-" + admin.id().toString() + "-" + timestamp.toString());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 5c786a5..744035e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3065,7 +3065,8 @@
 
       assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id()).id(), c.updated, serverIdent.get());
+          changeNoteUtil.newAccountIdIdent(
+              getAccount(admin.id()).id(), c.updated, serverIdent.get());
       assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitPatchSetCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -3074,7 +3075,8 @@
       RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0));
       assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
       expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id()).id(), c.created, serverIdent.get());
+          changeNoteUtil.newAccountIdIdent(
+              getAccount(admin.id()).id(), c.created, serverIdent.get());
       assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitChangeCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
@@ -4382,6 +4384,7 @@
           .message("Modify rules.pl")
           .create();
     }
+    projectCache.evict(project);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index be0cc04..7d73374 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -31,13 +31,16 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.config.FactoryModule;
@@ -54,6 +57,7 @@
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -64,6 +68,7 @@
 
   @Inject private CommentValidator mockCommentValidator;
   @Inject private TestCommentHelper testCommentHelper;
+  @Inject private RequestScopeOperations requestScopeOperations;
 
   private static final String COMMENT_TEXT = "The comment text";
   private static final CommentForValidation FILE_COMMENT_FOR_VALIDATION =
@@ -382,6 +387,93 @@
         .contains("Exceeding maximum cumulative size of comments");
   }
 
+  @Test
+  public void ccToReviewer() throws Exception {
+    PushOneCommit.Result r = createChange();
+    // User adds themselves and changes state
+    requestScopeOperations.setApiUser(user.id());
+
+    ReviewInput input = new ReviewInput().reviewer(user.id().toString(), ReviewerState.CC, false);
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    Map<ReviewerState, Collection<AccountInfo>> reviewers =
+        gApi.changes().id(r.getChangeId()).get().reviewers;
+    assertThat(reviewers).hasSize(1);
+    AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.CC));
+    assertThat(reviewer._accountId).isEqualTo(user.id().get());
+
+    // CC -> Reviewer
+    ReviewInput input2 = new ReviewInput().reviewer(user.id().toString());
+    gApi.changes().id(r.getChangeId()).current().review(input2);
+
+    Map<ReviewerState, Collection<AccountInfo>> reviewers2 =
+        gApi.changes().id(r.getChangeId()).get().reviewers;
+    assertThat(reviewers2).hasSize(1);
+    AccountInfo reviewer2 = Iterables.getOnlyElement(reviewers2.get(ReviewerState.REVIEWER));
+    assertThat(reviewer2._accountId).isEqualTo(user.id().get());
+  }
+
+  @Test
+  public void reviewerToCc() throws Exception {
+    // Admin owns the change
+    PushOneCommit.Result r = createChange();
+    // User adds themselves and changes state
+    requestScopeOperations.setApiUser(user.id());
+
+    ReviewInput input = new ReviewInput().reviewer(user.id().toString());
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    Map<ReviewerState, Collection<AccountInfo>> reviewers =
+        gApi.changes().id(r.getChangeId()).get().reviewers;
+    assertThat(reviewers).hasSize(1);
+    AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.REVIEWER));
+    assertThat(reviewer._accountId).isEqualTo(user.id().get());
+
+    // Reviewer -> CC
+    ReviewInput input2 = new ReviewInput().reviewer(user.id().toString(), ReviewerState.CC, false);
+    gApi.changes().id(r.getChangeId()).current().review(input2);
+
+    Map<ReviewerState, Collection<AccountInfo>> reviewers2 =
+        gApi.changes().id(r.getChangeId()).get().reviewers;
+    assertThat(reviewers2).hasSize(1);
+    AccountInfo reviewer2 = Iterables.getOnlyElement(reviewers2.get(ReviewerState.CC));
+    assertThat(reviewer2._accountId).isEqualTo(user.id().get());
+  }
+
+  @Test
+  public void votingMakesCallerReviewer() throws Exception {
+    // Admin owns the change
+    PushOneCommit.Result r = createChange();
+    // User adds themselves and changes state
+    requestScopeOperations.setApiUser(user.id());
+
+    ReviewInput input = new ReviewInput().label("Code-Review", 1);
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    Map<ReviewerState, Collection<AccountInfo>> reviewers =
+        gApi.changes().id(r.getChangeId()).get().reviewers;
+    assertThat(reviewers).hasSize(1);
+    AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.REVIEWER));
+    assertThat(reviewer._accountId).isEqualTo(user.id().get());
+  }
+
+  @Test
+  public void commentingMakesUserCC() throws Exception {
+    // Admin owns the change
+    PushOneCommit.Result r = createChange();
+    // User adds themselves and changes state
+    requestScopeOperations.setApiUser(user.id());
+
+    ReviewInput input = new ReviewInput().message("Foo bar!");
+    gApi.changes().id(r.getChangeId()).current().review(input);
+
+    Map<ReviewerState, Collection<AccountInfo>> reviewers =
+        gApi.changes().id(r.getChangeId()).get().reviewers;
+    assertThat(reviewers).hasSize(1);
+    AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.CC));
+    assertThat(reviewer._accountId).isEqualTo(user.id().get());
+  }
+
   private List<RobotCommentInfo> getRobotComments(String changeId) throws RestApiException {
     return gApi.changes().id(changeId).robotComments().values().stream()
         .flatMap(Collection::stream)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 24d08db..0ac7e20 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -1283,6 +1283,7 @@
           .message("Modify rules.pl")
           .create();
     }
+    projectCache.evict(project);
   }
 
   private List<ChangeApi> getChangeApis(RevertSubmissionInfo revertSubmissionInfo)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index dab2d00..a9afcbc 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -106,6 +106,7 @@
       r.rule = rule;
       r.commit(md);
     }
+    projectCache.evict(project);
   }
 
   private static final String SUBMIT_TYPE_FROM_SUBJECT =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
index 8dfebad..62140ed 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.revision;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -35,6 +36,16 @@
   }
 
   @Test
+  public void forPatchsetLevelFile() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r.getChangeId()).current().file(PATCHSET_LEVEL).blameRequest().get();
+
+    // File doesn't exist in commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
   public void forNonExistingFileFromBase() throws Exception {
     PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
     List<BlameInfo> blameInfos =
@@ -51,6 +62,22 @@
   }
 
   @Test
+  public void forPatchsetLevelFileFromBase() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes()
+            .id(r.getChangeId())
+            .current()
+            .file(PATCHSET_LEVEL)
+            .blameRequest()
+            .forBase(true)
+            .get();
+
+    // File doesn't exist in base commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
   public void forNewlyAddedFile() throws Exception {
     PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
     List<BlameInfo> blameInfos =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 717d3cc..5684b1f 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.FileInfoSubject.assertThat;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
@@ -121,6 +122,24 @@
   }
 
   @Test
+  public void patchsetLevelFileDiffIsEmpty() throws Exception {
+    PushOneCommit.Result result = createChange();
+    DiffInfo diffForPatchsetLevelFile =
+        gApi.changes()
+            .id(result.getChangeId())
+            .revision(result.getCommit().name())
+            .file(PATCHSET_LEVEL)
+            .diff();
+    // This behavior is the same as the behavior for non-existent files.
+    assertThat(diffForPatchsetLevelFile).binary().isNull();
+    assertThat(diffForPatchsetLevelFile).content().isEmpty();
+    assertThat(diffForPatchsetLevelFile).diffHeader().isNull();
+    assertThat(diffForPatchsetLevelFile).metaA().isNull();
+    assertThat(diffForPatchsetLevelFile).metaB().isNull();
+    assertThat(diffForPatchsetLevelFile).webLinks().isNull();
+  }
+
+  @Test
   public void deletedFileIsIncludedInDiff() throws Exception {
     gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
     gApi.changes().id(changeId).edit().publish();
@@ -353,6 +372,31 @@
   }
 
   @Test
+  public void copiedFileDetectedIfOriginalFileIsRenamedInDiff() throws Exception {
+    /*
+     * Copies are detected when a file is deleted and more than 1 file with the same content are
+     * added. In this case, the added file with the closest name to the original file is tagged as a
+     * rename and the remaining files are considered copies. This implementation is done by JGit in
+     * the RenameDetector component.
+     */
+    String renamedFileName = "renamed_some_file.txt";
+    String copyFileName1 = "copy1_with_different_name.txt";
+    String copyFileName2 = "copy2_with_different_name.txt";
+    gApi.changes().id(changeId).edit().modifyFile(copyFileName1, RawInputUtil.create(FILE_CONTENT));
+    gApi.changes().id(changeId).edit().modifyFile(copyFileName2, RawInputUtil.create(FILE_CONTENT));
+    gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFileName);
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files();
+
+    assertThat(changedFiles.keySet())
+        .containsExactly("/COMMIT_MSG", renamedFileName, copyFileName1, copyFileName2);
+    assertThat(changedFiles.get(renamedFileName).status).isEqualTo('R');
+    assertThat(changedFiles.get(copyFileName1).status).isEqualTo('C');
+    assertThat(changedFiles.get(copyFileName2).status).isEqualTo('C');
+  }
+
+  @Test
   public void addedBinaryFileIsIncludedInDiff() throws Exception {
     String imageFileName = "an_image.png";
     byte[] imageBytes = createRgbImage(255, 0, 0);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 74f9134..dd13643 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
@@ -1500,6 +1501,19 @@
   }
 
   @Test
+  public void patchsetLevelContentDoesNotExist() throws Exception {
+    PushOneCommit.Result change = createChange();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () ->
+            gApi.changes()
+                .id(change.getChangeId())
+                .revision(change.getCommit().name())
+                .file(PATCHSET_LEVEL)
+                .content());
+  }
+
+  @Test
   public void cannotGetContentOfDirectory() throws Exception {
     Map<String, String> files = ImmutableMap.of("dir/file1.txt", "content 1");
     PushOneCommit.Result result =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 0b8f441..27b866b 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.MoreCollectors.onlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
@@ -35,6 +36,7 @@
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.ChangeType;
@@ -151,7 +153,7 @@
     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 */
-    TestTimeUtil.incrementClock(5, TimeUnit.SECONDS);
+    TestTimeUtil.incrementClock(10, TimeUnit.SECONDS);
 
     RobotCommentInput c1 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
     RobotCommentInput c2 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
@@ -220,9 +222,9 @@
             .changeMessageId;
 
     /**
-     * Upload PS message, robot message 1 & robot comment 1 all have the same timestamp. The robot
-     * comment is matched to robot message 1 because the PS upload message is auto-generated and is
-     * ignored in matching
+     * All change messages have the auto-generated tag. Robot comments can be linked to
+     * auto-generated messages where each comment is linked to the next nearest change message in
+     * timestamp
      */
     assertThat(message1ChangeId).isEqualTo(comment1MessageId);
     assertThat(message2ChangeId).isEqualTo(comment2MessageId);
@@ -267,6 +269,57 @@
   }
 
   @Test
+  public void patchsetLevelRobotCommentCanBeAddedAndRetrieved() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    testCommentHelper.addRobotComment(changeId, input);
+
+    List<RobotCommentInfo> results = getRobotComments();
+    assertThatList(results).onlyElement().path().isEqualTo(PATCHSET_LEVEL);
+  }
+
+  @Test
+  public void patchsetLevelRobotCommentCantHaveLine() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.line = 1;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelRobotCommentCantHaveRange() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.range = createRange(2, 9, 5, 10);
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelRobotCommentCantHaveSide() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("side");
+  }
+
+  @Test
+  public void fixSuggestionCannotPointToPatchsetLevel() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(FILE_NAME);
+    FixReplacementInfo brokenFixReplacement = createFixReplacementInfo();
+    brokenFixReplacement.path = PATCHSET_LEVEL;
+    input.fixSuggestions = ImmutableList.of(createFixSuggestionInfo(brokenFixReplacement));
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("file path must not be " + PATCHSET_LEVEL);
+  }
+
+  @Test
   public void hugeRobotCommentIsRejected() {
     int defaultSizeLimit = 1 << 20;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit + 1);
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 1083377..14288b5 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -1071,7 +1071,7 @@
 
       PersonIdent committer = serverIdent.get();
       PersonIdent author =
-          noteUtil.newIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
+          noteUtil.newAccountIdIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
       tr.branch(RefNames.changeMetaRef(cd3.getId()))
           .commit()
           .author(author)
diff --git a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
index 4caee64..4db0177 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.MustBeClosed;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.StandaloneSiteTest;
 import com.google.gerrit.entities.Project;
@@ -82,27 +83,33 @@
 
   private void setProjectsIndexLastModifiedInThePast(Path indexDir, Instant time)
       throws IOException {
-    for (Path path : getAllProjectsIndexFiles(indexDir).collect(Collectors.toList())) {
-      FS.DETECTED.setLastModified(path, time);
+    try (Stream<Path> allprojectsIndexFiles = getAllProjectsIndexFiles(indexDir)) {
+      for (Path path : allprojectsIndexFiles.collect(Collectors.toList())) {
+        FS.DETECTED.setLastModified(path, time);
+      }
     }
   }
 
   private Optional<Instant> getProjectsIndexLastModified(Path indexDir) throws IOException {
-    return getAllProjectsIndexFiles(indexDir)
-        .map(FS.DETECTED::lastModifiedInstant)
-        .max(Comparator.comparingLong(Instant::toEpochMilli));
+    try (Stream<Path> allprojectsIndexFiles = getAllProjectsIndexFiles(indexDir)) {
+      return allprojectsIndexFiles
+          .map(FS.DETECTED::lastModifiedInstant)
+          .max(Comparator.comparingLong(Instant::toEpochMilli));
+    }
   }
 
+  @MustBeClosed
   private Stream<Path> getAllProjectsIndexFiles(Path indexDir) throws IOException {
-    Optional<Path> projectsPath =
-        Files.walk(indexDir, 1)
-            .filter(Files::isDirectory)
-            .filter(p -> p.getFileName().toString().startsWith("projects_"))
-            .findFirst();
-    if (!projectsPath.isPresent()) {
-      return Stream.empty();
+    try (Stream<Path> stream = Files.walk(indexDir, 1)) {
+      Optional<Path> projectsPath =
+          stream
+              .filter(Files::isDirectory)
+              .filter(p -> p.getFileName().toString().startsWith("projects_"))
+              .findFirst();
+      if (!projectsPath.isPresent()) {
+        return Stream.empty();
+      }
+      return Files.walk(projectsPath.get(), 1, FileVisitOption.FOLLOW_LINKS);
     }
-
-    return Files.walk(projectsPath.get(), 1, FileVisitOption.FOLLOW_LINKS);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 72db9b3..2901361 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -299,6 +299,89 @@
     assertTrees(project, actual);
   }
 
+  /**
+   * Tests the following situation:
+   *
+   * <ul>
+   *   <li>1. create a change series, consisting out of a merge commit and a normal commit
+   *   <li>2. before submitting the change series, another non-conflicting change gets submitted
+   *   <li>3. when the change series gets submitted, Gerrit must perform a merge/rebase/cherry-pick
+   * </ul>
+   */
+  @Test
+  public void submitChangeSeriesWithMergeCommitThatIsBasedOnOldTip() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+
+    // create a commit which will become the first parent of a merge commit
+    PushOneCommit.Result parent1 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "parent 2",
+                ImmutableMap.of("foo", "foo-2", "bar", "bar-2"))
+            .to("refs/heads/master");
+
+    // reset the testRepo in order to create a sibling of parent1
+    testRepo.reset(initialHead);
+
+    // create a stable branch that we can merge back into master later
+    BranchInput in = new BranchInput();
+    in.revision = initialHead.getName();
+    gApi.projects().name(project.get()).branch("refs/heads/stable").create(in);
+
+    // create one commit in the stable branch, which will become the second parent of the merge
+    // commit
+    PushOneCommit.Result parent2 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "parent 1",
+                ImmutableMap.of("foo", "foo-1", "bar", "bar-1"))
+            .to("refs/heads/stable");
+
+    // create a merge change that merges the stable branch back into master
+    testRepo.reset(parent1.getCommit());
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
+    PushOneCommit.Result mergeChange = m.to("refs/for/master");
+    mergeChange.assertOkStatus();
+
+    // approve the merge change so that it becomes submittable
+    approve(mergeChange.getChangeId());
+
+    // create a successor change that depends on the merge change
+    PushOneCommit.Result successorChange = createChange("refs/for/master");
+
+    // simulate another developer submitting a change in the meantime (non-conflicting sibling
+    // commit of the merge commit), this means when the change series gets submitted Gerrit must
+    // perform a merge/rebase/cherry-pick now
+    testRepo.reset(parent1.getCommit());
+    submit(createChange("Other Change", "x.txt", "x content").getChangeId());
+
+    // submit the change series
+    if (getSubmitType() != SubmitType.FAST_FORWARD_ONLY) {
+      submit(successorChange.getChangeId());
+    } else {
+      submitWithConflict(
+          successorChange.getChangeId(),
+          "Failed to submit 2 changes due to the following problems:\n"
+              + "Change "
+              + mergeChange.getChange().getId()
+              + ": Project policy "
+              + "requires all submissions to be a fast-forward. Please "
+              + "rebase the change locally and upload again for review.\n"
+              + "Change "
+              + successorChange.getChange().getId()
+              + ": Project policy "
+              + "requires all submissions to be a fast-forward. Please "
+              + "rebase the change locally and upload again for review.");
+    }
+  }
+
   @Test
   public void submitNoPermission() throws Throwable {
     // create project where submit is blocked
@@ -1265,7 +1348,7 @@
     } else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
       assertThat(last).startsWith("Change has been successfully rebased and submitted as");
     } else {
-      assertThat(last).isEqualTo("Change has been successfully merged by Administrator");
+      assertThat(last).isEqualTo("Change has been successfully merged by Gerrit User 1000000");
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index a0ebf02..68e9b14 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -429,7 +429,8 @@
       assertThat(commit.getShortMessage()).isEqualTo("Create change");
 
       PersonIdent expectedAuthor =
-          changeNoteUtil.newIdent(getAccount(admin.id()).id(), c.created, serverIdent.get());
+          changeNoteUtil.newAccountIdIdent(
+              getAccount(admin.id()).id(), c.created, serverIdent.get());
       assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
 
       assertThat(commit.getCommitterIdent())
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index ecd4025..e48c9d5 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,7 +18,9 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
 
@@ -176,6 +178,203 @@
   }
 
   @Test
+  public void patchsetLevelCommentCanBeAddedAndRetrieved() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput comment = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addComments(changeId, ps1, comment);
+
+    Map<String, List<CommentInfo>> results = getPublishedComments(changeId, ps1);
+    assertThatMap(results).keys().containsExactly(PATCHSET_LEVEL);
+  }
+
+  @Test
+  public void deletePatchsetLevelComment() throws Exception {
+    requestScopeOperations.setApiUser(admin.id());
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    String commentMessage = "to be deleted";
+    CommentInput comment = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, commentMessage);
+    addComments(changeId, revId, comment);
+
+    Map<String, List<CommentInfo>> results = getPublishedComments(changeId, revId);
+    CommentInfo oldComment = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL));
+
+    DeleteCommentInput input = new DeleteCommentInput("reason");
+    gApi.changes().id(changeId).revision(revId).comment(oldComment.id).delete(input);
+    CommentInfo updatedComment =
+        Iterables.getOnlyElement(getPublishedComments(changeId, revId).get(PATCHSET_LEVEL));
+
+    assertThat(updatedComment.message).doesNotContain(commentMessage);
+  }
+
+  @Test
+  public void patchsetLevelCommentEmailNotification() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput comment = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addComments(changeId, ps1, comment);
+
+    String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
+    assertThat(emailBody).contains("Patchset");
+    assertThat(emailBody).doesNotContain("/PATCHSET_LEVEL");
+  }
+
+  @Test
+  public void patchsetLevelCommentCantHaveLine() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput input = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    input.line = 1;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, ps1, input));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelCommentCantHaveRange() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput input = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    input.range = createLineRange(1, 3);
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, ps1, input));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelCommentCantHaveSide() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput input = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    input.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, ps1, input));
+    assertThat(ex.getMessage()).contains("side");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCanBeAddedAndRetrieved() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    assertThatMap(results).keys().containsExactly(PATCHSET_LEVEL);
+  }
+
+  @Test
+  public void deletePatchsetLevelDraft() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput draft = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment 1");
+    CommentInfo returned = addDraft(changeId, revId, draft);
+    deleteDraft(changeId, revId, returned.id);
+    Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+    assertThat(drafts).isEmpty();
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantHaveLine() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    comment.line = 1;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantHaveRange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    comment.range = createLineRange(1, 3);
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantHaveSide() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    comment.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantBeUpdatedToHaveLine() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    DraftInput update = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
+    update.line = 1;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantBeUpdatedToHaveRange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    DraftInput update = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
+    update.range = createLineRange(1, 3);
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantBeUpdatedToHaveSide() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    DraftInput update = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
+    update.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
+    assertThat(ex.getMessage()).contains("side");
+  }
+
+  @Test
   public void postCommentWithReply() throws Exception {
     for (Integer line : lines) {
       String file = "file";
@@ -1083,7 +1282,7 @@
     addComments(changeId, ps4, c7, c8);
 
     // 11th commit: Add (c9) to PS2.
-    CommentInput c9 = newComment("b.txt", "comment 9");
+    CommentInput c9 = newCommentWithOnlyMandatoryFields("b.txt", "comment 9");
     addComments(changeId, ps2, c9);
 
     List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(id.get());
@@ -1340,6 +1539,11 @@
     return newComment(file, Side.REVISION, 0, message, false);
   }
 
+  private static CommentInput newCommentWithOnlyMandatoryFields(String path, String message) {
+    CommentInput c = new CommentInput();
+    return populate(c, path, null, null, null, null, message, false);
+  }
+
   private static CommentInput newComment(
       String path, Side side, int line, String message, Boolean unresolved) {
     CommentInput c = new CommentInput();
@@ -1367,19 +1571,24 @@
     return populate(d, path, Side.PARENT, parent, line, message, false);
   }
 
+  private DraftInput newDraftWithOnlyMandatoryFields(String path, String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, null, null, null, null, message, false);
+  }
+
   private static <C extends Comment> C populate(
       C c,
       String path,
       Side side,
       Integer parent,
-      int line,
+      Integer line,
       Comment.Range range,
       String message,
       Boolean unresolved) {
     c.path = path;
     c.side = side;
     c.parent = parent;
-    c.line = line != 0 ? line : null;
+    c.line = line != null && line != 0 ? line : null;
     c.message = message;
     c.unresolved = unresolved;
     if (range != null) {
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 069387c..e39f967 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -831,7 +831,7 @@
   private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
     PersonIdent committer = serverIdent.get();
     PersonIdent author =
-        noteUtil.newIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
+        noteUtil.newAccountIdIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
     serverSideTestRepo
         .branch(RefNames.changeMetaRef(id))
         .commit()
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index fc44822..d7d67b8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.extensions.validators.CommentForValidation;
 import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.mail.MailMessage;
 import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.mail.receive.MailProcessor;
@@ -47,6 +48,7 @@
 import com.google.gerrit.testing.TestCommentHelper;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import java.net.URL;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Collection;
@@ -296,6 +298,10 @@
     assertNotifyTo(user);
     Message message = sender.nextMessage();
     assertThat(message.body()).contains("rejected one or more comments");
+
+    // ensure the message header contains a valid message id.
+    assertThat(((EmailHeader.String) (message.headers().get("Message-ID"))).getString())
+        .containsMatch("<someid-REJECTION-HTML@" + new URL(canonicalWebUrl.get()).getHost() + ">");
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendIT.java
new file mode 100644
index 0000000..2aab159
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendIT.java
@@ -0,0 +1,70 @@
+// 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.
+
+package com.google.gerrit.acceptance.server.permissions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+/** Asserts behavior on {@link PermissionBackend} using a fully-started Gerrit. */
+public class PermissionBackendIT extends AbstractDaemonTest {
+  @Inject PermissionBackend pb;
+  @Inject ChangeNotes.Factory changeNotesFactory;
+
+  @Test
+  public void changeDataFromIndex_canCheckReviewerState() throws Exception {
+    Change.Id changeId = createChange().getChange().getId();
+    gApi.changes().id(changeId.get()).setPrivate(true);
+    gApi.changes().id(changeId.get()).addReviewer(user.email());
+
+    ChangeData changeData =
+        Iterables.getOnlyElement(queryProvider.get().byLegacyChangeId(changeId));
+    boolean reviewerCanSee =
+        pb.absentUser(user.id()).change(changeData).test(ChangePermission.READ);
+    assertThat(reviewerCanSee).isTrue();
+  }
+
+  @Test
+  public void changeDataFromNoteDb_canCheckReviewerState() throws Exception {
+    Change.Id changeId = createChange().getChange().getId();
+    gApi.changes().id(changeId.get()).setPrivate(true);
+    gApi.changes().id(changeId.get()).addReviewer(user.email());
+
+    ChangeNotes notes = changeNotesFactory.create(project, changeId);
+    ChangeData changeData = changeDataFactory.create(notes);
+    boolean reviewerCanSee =
+        pb.absentUser(user.id()).change(changeData).test(ChangePermission.READ);
+    assertThat(reviewerCanSee).isTrue();
+  }
+
+  @Test
+  public void changeNotes_canCheckReviewerState() throws Exception {
+    Change.Id changeId = createChange().getChange().getId();
+    gApi.changes().id(changeId.get()).setPrivate(true);
+    gApi.changes().id(changeId.get()).addReviewer(user.email());
+
+    ChangeNotes notes = changeNotesFactory.create(project, changeId);
+    boolean reviewerCanSee = pb.absentUser(user.id()).change(notes).test(ChangePermission.READ);
+    assertThat(reviewerCanSee).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index e5432d1..18ae6c4 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -29,7 +29,6 @@
 import java.util.Collection;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -44,14 +43,6 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
 
-  @Before
-  public void setUp() {
-    // We don't want caches to interfere with our tests. If we didn't, the cache would take
-    // precedence over the index, which would never be called.
-    baseConfig.setString("cache", "changes", "memoryLimit", "0");
-    baseConfig.setString("cache", "projects", "memoryLimit", "0");
-  }
-
   @Test
   public void testUnresolvedCommentsCountPredicate() throws Exception {
     modifySubmitRules("gerrit:unresolved_comments_count(0)");
@@ -119,5 +110,6 @@
           .message("Modify rules.pl")
           .create();
     }
+    projectCache.evict(project);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 5a31bfd..58c2517 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
@@ -65,7 +65,7 @@
     session.exec(
         String.format("gerrit set-reviewers -%s %s %s", add ? "a" : "r", user.email(), id));
     session.assertSuccess();
-    ImmutableSet<Account.Id> reviewers = change.getChange().getReviewers().all();
+    ImmutableSet<Account.Id> reviewers = change.getChange().reviewers().all();
     if (add) {
       assertThat(reviewers).contains(user.id());
     } else {
diff --git a/javatests/com/google/gerrit/entities/PatchTest.java b/javatests/com/google/gerrit/entities/PatchTest.java
index 9f906a9..dce1b3e 100644
--- a/javatests/com/google/gerrit/entities/PatchTest.java
+++ b/javatests/com/google/gerrit/entities/PatchTest.java
@@ -24,6 +24,7 @@
   public void isMagic() {
     assertThat(Patch.isMagic("/COMMIT_MSG")).isTrue();
     assertThat(Patch.isMagic("/MERGE_LIST")).isTrue();
+    assertThat(Patch.isMagic("/PATCHSET_LEVEL")).isTrue();
 
     assertThat(Patch.isMagic("/COMMIT_MSG/")).isFalse();
     assertThat(Patch.isMagic("COMMIT_MSG")).isFalse();
diff --git a/javatests/com/google/gerrit/server/cache/h2/BUILD b/javatests/com/google/gerrit/server/cache/h2/BUILD
index 98f1b0e..438990c 100644
--- a/javatests/com/google/gerrit/server/cache/h2/BUILD
+++ b/javatests/com/google/gerrit/server/cache/h2/BUILD
@@ -6,10 +6,12 @@
     deps = [
         "//java/com/google/gerrit/server/cache/h2",
         "//java/com/google/gerrit/server/cache/serialize",
+        "//java/com/google/gerrit/server/util/time",
         "//lib:guava",
         "//lib:h2",
         "//lib:junit",
         "//lib/guice",
+        "//lib/mockito",
         "//lib/truth",
     ],
 )
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 69c2799..ddcfe0c 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -16,16 +16,27 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
 import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
 import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.TypeLiteral;
+import java.time.Duration;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
 import org.junit.Test;
 
 public class H2CacheTest {
@@ -38,23 +49,31 @@
   }
 
   private static H2CacheImpl<String, String> newH2CacheImpl(
-      int id, Cache<String, ValueHolder<String>> mem, int version) {
-    SqlStore<String, String> store =
-        new SqlStore<>(
-            "jdbc:h2:mem:Test_" + id,
-            KEY_TYPE,
-            StringCacheSerializer.INSTANCE,
-            StringCacheSerializer.INSTANCE,
-            version,
-            1 << 20,
-            null);
+      SqlStore<String, String> store, Cache<String, ValueHolder<String>> mem) {
     return new H2CacheImpl<>(MoreExecutors.directExecutor(), store, KEY_TYPE, mem);
   }
 
+  private static SqlStore<String, String> newStore(
+      int id,
+      int version,
+      @Nullable Duration expireAfterWrite,
+      @Nullable Duration refreshAfterWrite) {
+    return new SqlStore<>(
+        "jdbc:h2:mem:Test_" + id,
+        KEY_TYPE,
+        StringCacheSerializer.INSTANCE,
+        StringCacheSerializer.INSTANCE,
+        version,
+        1 << 20,
+        expireAfterWrite,
+        refreshAfterWrite);
+  }
+
   @Test
   public void get() throws ExecutionException {
     Cache<String, ValueHolder<String>> mem = CacheBuilder.newBuilder().build();
-    H2CacheImpl<String, String> impl = newH2CacheImpl(nextDbId(), mem, DEFAULT_VERSION);
+    H2CacheImpl<String, String> impl =
+        newH2CacheImpl(newStore(nextDbId(), DEFAULT_VERSION, null, null), mem);
 
     assertThat(impl.getIfPresent("foo")).isNull();
 
@@ -94,11 +113,12 @@
   }
 
   @Test
-  public void version() throws Exception {
+  public void version() {
     int id = nextDbId();
-    H2CacheImpl<String, String> oldImpl = newH2CacheImpl(id, disableMemCache(), DEFAULT_VERSION);
+    H2CacheImpl<String, String> oldImpl =
+        newH2CacheImpl(newStore(id, DEFAULT_VERSION, null, null), disableMemCache());
     H2CacheImpl<String, String> newImpl =
-        newH2CacheImpl(id, disableMemCache(), DEFAULT_VERSION + 1);
+        newH2CacheImpl(newStore(id, DEFAULT_VERSION + 1, null, null), disableMemCache());
 
     assertThat(oldImpl.diskStats().space()).isEqualTo(0);
     assertThat(newImpl.diskStats().space()).isEqualTo(0);
@@ -124,6 +144,57 @@
     assertThat(oldImpl.getIfPresent("key")).isNull();
   }
 
+  @Test
+  public void refreshAfterWrite_triggeredWhenConfigured() throws Exception {
+    SqlStore<String, String> store =
+        newStore(nextDbId(), DEFAULT_VERSION, null, Duration.ofMillis(10));
+
+    // This is the loader that we configure for the cache when calling .loader(...)
+    @SuppressWarnings("unchecked")
+    CacheLoader<String, String> baseLoader = mock(CacheLoader.class);
+    resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
+
+    // We wrap baseLoader just like H2CacheFactory is wrapping it. The wrapped version will call out
+    // to the store for refreshing values.
+    H2CacheImpl.Loader<String, String> wrappedLoader =
+        new H2CacheImpl.Loader<>(MoreExecutors.directExecutor(), store, baseLoader);
+    // memCache is the in-memory variant of the cache. Its loader is wrappedLoader which will call
+    // out to the store to save or delete cached values.
+    LoadingCache<String, ValueHolder<String>> memCache =
+        CacheBuilder.newBuilder().maximumSize(10).build(wrappedLoader);
+
+    // h2Cache puts it all together
+    H2CacheImpl<String, String> h2Cache = newH2CacheImpl(store, memCache);
+
+    // Initial load and cache retrieval do not trigger refresh
+    // This works because we use a directExecutor() for refreshes
+    TimeUtil.setCurrentMillisSupplier(() -> 0);
+    assertThat(h2Cache.get("foo")).isEqualTo("load:foo");
+    verify(baseLoader).load("foo");
+    assertThat(h2Cache.get("foo")).isEqualTo("load:foo");
+    verifyNoMoreInteractions(baseLoader);
+    resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
+
+    // Load after refresh duration returns old value, triggers refresh and returns new value
+    TimeUtil.setCurrentMillisSupplier(() -> 11);
+    assertThat(h2Cache.get("foo")).isEqualTo("load:foo");
+    assertThat(h2Cache.get("foo")).isEqualTo("reload:foo");
+    verify(baseLoader).reload("foo", "load:foo");
+    verifyNoMoreInteractions(baseLoader);
+    resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
+
+    // Refreshed value was persisted
+    memCache.invalidateAll(); // Invalidates only the memcache, not the store.
+    assertThat(h2Cache.getIfPresent("foo")).isEqualTo("reload:foo");
+  }
+
+  private static void resetLoaderAndAnswerLoadAndRefreshCalls(CacheLoader<String, String> loader)
+      throws Exception {
+    reset(loader);
+    when(loader.load("foo")).thenReturn("load:foo");
+    when(loader.reload("foo", "load:foo")).thenReturn(Futures.immediateFuture("reload:foo"));
+  }
+
   private static <K, V> Cache<K, ValueHolder<V>> disableMemCache() {
     return CacheBuilder.newBuilder().maximumSize(0).build();
   }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 8ffcc8b..5bfe97c 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -519,7 +519,7 @@
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     return writeCommit(
         body,
-        noteUtil.newIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
         false);
   }
 
@@ -531,7 +531,7 @@
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     return writeCommit(
         body,
-        noteUtil.newIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
         initWorkInProgress);
   }
 
diff --git a/javatests/com/google/gerrit/server/patch/PatchListTest.java b/javatests/com/google/gerrit/server/patch/PatchListTest.java
index e224191a..56adefa 100644
--- a/javatests/com/google/gerrit/server/patch/PatchListTest.java
+++ b/javatests/com/google/gerrit/server/patch/PatchListTest.java
@@ -26,16 +26,30 @@
 import org.junit.Test;
 
 public class PatchListTest {
+
   @Test
   public void fileOrder() {
     String[] names = {
-      "zzz", "def/g", "/!xxx", "abc", Patch.MERGE_LIST, "qrx", Patch.COMMIT_MSG,
+      "zzz",
+      "def/g",
+      "/!xxx",
+      "abc",
+      Patch.MERGE_LIST,
+      "qrx",
+      Patch.COMMIT_MSG,
+      Patch.PATCHSET_LEVEL
     };
     String[] want = {
-      Patch.COMMIT_MSG, Patch.MERGE_LIST, "/!xxx", "abc", "def/g", "qrx", "zzz",
+      Patch.COMMIT_MSG,
+      Patch.MERGE_LIST,
+      Patch.PATCHSET_LEVEL,
+      "/!xxx",
+      "abc",
+      "def/g",
+      "qrx",
+      "zzz",
     };
-
-    Arrays.sort(names, 0, names.length, PatchList::comparePaths);
+    Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
     assertThat(names).isEqualTo(want);
   }
 
@@ -48,7 +62,7 @@
       Patch.COMMIT_MSG, "/!xxx", "abc", "def/g", "qrx", "zzz",
     };
 
-    Arrays.sort(names, 0, names.length, PatchList::comparePaths);
+    Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
     assertThat(names).isEqualTo(want);
   }
 
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1aa0f35..11e98c6 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3039,6 +3039,7 @@
     List<ChangeInfo> result = newQuery("attention:" + user2Id.toString()).get();
     assertThat(result).hasSize(1);
     ChangeInfo changeInfo = Iterables.getOnlyElement(result);
+    assertThat(changeInfo.attentionSet).isNotNull();
     assertThat(changeInfo.attentionSet.keySet()).containsExactly(userId.get(), user2Id.get());
     assertThat(changeInfo.attentionSet.get(userId.get()).reason).isEqualTo("reason 1");
     assertThat(changeInfo.attentionSet.get(user2Id.get()).reason).isEqualTo("reason 2");
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index 9f34377..daefd7c 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -1,78 +1,139 @@
-/*
- * 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.
- */
+// 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.
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.common.collect.MoreCollectors.onlyElement;
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 @RunWith(JUnit4.class)
 public class ListChangeCommentsTest {
-
   @Test
-  public void commentsLinkedToChangeMessages() {
-    CommentInfo c1 = getNewCommentInfo("c1", Timestamp.valueOf("2018-01-01 09:01:00"));
-    CommentInfo c2 = getNewCommentInfo("c2", Timestamp.valueOf("2018-01-01 09:01:15"));
-    CommentInfo c3 = getNewCommentInfo("c3", Timestamp.valueOf("2018-01-01 09:01:25"));
+  public void commentsLinkedToChangeMessagesIgnoreGerritAutoGenTaggedMessages() {
+    /* Comments should not be linked to Gerrit's autogenerated messages */
+    List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
+    List<ChangeMessage> changeMessages =
+        createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");
 
-    ChangeMessage cm1 =
-        getNewChangeMessage("cm1key", "cm1", Timestamp.valueOf("2018-01-01 00:00:00"));
-    ChangeMessage cm2 =
-        getNewChangeMessage("cm2key", "cm2", Timestamp.valueOf("2018-01-01 09:01:15"));
-    ChangeMessage cm3 =
-        getNewChangeMessage("cm3key", "cm3", Timestamp.valueOf("2018-01-01 09:01:27"));
+    changeMessages.add(
+        newChangeMessage("ignore", "cmAutoGenByGerrit", "15", ChangeMessagesUtil.TAG_MERGED));
 
-    assertThat(c1.changeMessageId).isNull();
-    assertThat(c2.changeMessageId).isNull();
-    assertThat(c3.changeMessageId).isNull();
+    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
 
-    ImmutableList<CommentInfo> comments = ImmutableList.of(c1, c2, c3);
-    ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(cm1, cm2, cm3);
+    assertThat(getComment(comments, "c1").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
+    /* comment 2 ignored the auto-generated message because it has a Gerrit tag */
+    assertThat(getComment(comments, "c2").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm2").getKey().uuid());
+    assertThat(getComment(comments, "c3").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
 
-    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages);
-
-    assertThat(c1.changeMessageId).isEqualTo(changeMessageKey(cm2));
-    assertThat(c2.changeMessageId).isEqualTo(changeMessageKey(cm2));
-    assertThat(c3.changeMessageId).isEqualTo(changeMessageKey(cm3));
+    // Make sure no comment is linked to the auto-gen message
+    assertThat(comments.stream().map(c -> c.changeMessageId).collect(Collectors.toSet()))
+        .doesNotContain(getChangeMessage(changeMessages, "cmAutoGenByGerrit"));
   }
 
-  private static CommentInfo getNewCommentInfo(String message, Timestamp ts) {
+  @Test
+  public void commentsLinkedToChangeMessagesAllowLinkingToAutoGenTaggedMessages() {
+    /* Human comments are allowed to be linked to autogenerated messages */
+    List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
+    List<ChangeMessage> changeMessages =
+        createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");
+
+    changeMessages.add(
+        newChangeMessage(
+            "cmAutoGen", "cmAutoGen", "15", ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX));
+
+    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
+
+    assertThat(getComment(comments, "c1").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
+    /* comment 2 did not ignore the auto-generated change message */
+    assertThat(getComment(comments, "c2").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cmAutoGen").getKey().uuid());
+    assertThat(getComment(comments, "c3").changeMessageId)
+        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
+  }
+
+  /**
+   * Create a list of comments from the specified args args should be passed as consecutive pairs of
+   * messages and timestamps example: (m1, t1, m2, t2, ...)
+   */
+  private static List<CommentInfo> createComments(String... args) {
+    List<CommentInfo> comments = new ArrayList<>();
+    for (int i = 0; i < args.length; i += 2) {
+      String message = args[i];
+      String ts = args[i + 1];
+      comments.add(newCommentInfo(message, ts));
+    }
+    return comments;
+  }
+
+  /**
+   * Create a list of change messages from the specified args args should be passed as consecutive
+   * pairs of messages and timestamps example: (m1, t1, m2, t2, ...). the tag parameter for the
+   * created change messages will be null.
+   */
+  private static List<ChangeMessage> createChangeMessages(String... args) {
+    List<ChangeMessage> changeMessages = new ArrayList<>();
+    for (int i = 0; i < args.length; i += 2) {
+      String key = args[i] + "Key";
+      String message = args[i];
+      String ts = args[i + 1];
+      changeMessages.add(newChangeMessage(key, message, ts, null));
+    }
+    return changeMessages;
+  }
+
+  /** Create a new CommentInfo with a given message and timestamp */
+  private static CommentInfo newCommentInfo(String message, String ts) {
     CommentInfo c = new CommentInfo();
     c.message = message;
-    c.updated = ts;
+    c.updated = Timestamp.valueOf("2000-01-01 00:00:" + ts);
     return c;
   }
 
-  private static ChangeMessage getNewChangeMessage(String id, String message, Timestamp ts) {
+  /** Create a new change message with an id, message, timestamp and tag */
+  private static ChangeMessage newChangeMessage(String id, String message, String ts, String tag) {
     ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
-    ChangeMessage cm = new ChangeMessage(key, null, ts, null);
+    ChangeMessage cm =
+        new ChangeMessage(key, null, Timestamp.valueOf("2000-01-01 00:00:" + ts), null);
     cm.setMessage(message);
+    cm.setTag(tag);
     return cm;
   }
 
-  private static String changeMessageKey(ChangeMessage changeMessage) {
-    return changeMessage.getKey().uuid();
+  /** Return the change message from the list of messages that has specific message text */
+  private static ChangeMessage getChangeMessage(List<ChangeMessage> messages, String messageText) {
+    return messages.stream().filter(m -> m.getMessage().equals(messageText)).collect(onlyElement());
+  }
+
+  /** Return the comment from the list of comments that has specific message text */
+  private CommentInfo getComment(List<CommentInfo> comments, String messageText) {
+    return comments.stream().filter(c -> c.message.equals(messageText)).collect(onlyElement());
   }
 }
diff --git a/lib/BUILD b/lib/BUILD
index f0c0aad..d3ef4b9 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -47,6 +47,13 @@
 )
 
 java_library(
+    name = "jgit-ssh-jsch",
+    data = ["//lib:LICENSE-jgit"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.ssh.jsch:ssh-jsch"],
+)
+
+java_library(
     name = "jgit-archive",
     data = ["//lib:LICENSE-jgit"],
     visibility = ["//visibility:public"],
diff --git a/modules/jgit b/modules/jgit
index 75fccca..8a44216 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 75fcccaea39f7a2112886e04a94458d6b7b7c37f
+Subproject commit 8a44216e8bdad1a13ce637b1395467600870849a
diff --git a/package.json b/package.json
index 526d201..0f10b62 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
   "description": "Gerrit Code Review",
   "dependencies": {},
   "devDependencies": {
-    "@bazel/rollup": "^1.1.0",
-    "@bazel/typescript": "^1.0.1",
+    "@bazel/rollup": "^1.6.1",
+    "@bazel/typescript": "^1.6.1",
     "eslint": "^6.6.0",
     "eslint-config-google": "^0.13.0",
     "eslint-plugin-html": "^6.0.0",
@@ -15,7 +15,7 @@
     "fried-twinkie": "^0.2.2",
     "polymer-cli": "^1.9.11",
     "prettier": "2.0.5",
-    "typescript": "^3.7.4",
+    "typescript": "3.8.2",
     "web-component-tester": "^6.5.1"
   },
   "scripts": {
@@ -26,7 +26,10 @@
     "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",
     "test-template": "./polygerrit-ui/app/run_template_test.sh",
-    "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test"
+    "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
+    "test:karma:debug": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
+    "test:karma": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles",
+    "test:wct": "npm run test -- --test_tag_filters=wct"
   },
   "repository": {
     "type": "git",
diff --git a/plugins/BUILD b/plugins/BUILD
index 5f9c142..a071bde 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -71,6 +71,7 @@
     "//lib/jackson:jackson-core",
     "//lib:jgit-servlet",
     "//lib:jgit",
+    "//lib:jgit-ssh-jsch",
     "//lib:jsr305",
     "//lib/log:api",
     "//lib/log:log4j",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index e211fb1..7357ab4 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit e211fb1bd21043e2574c438a687c8f492d538c97
+Subproject commit 7357ab473599d16ae33cc982bbd65472f08c2dd6
diff --git a/plugins/delete-project b/plugins/delete-project
index e345e6e..7671def 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit e345e6e79900a72981e4ad19d37c7fbdcae4818b
+Subproject commit 7671def07882aab89b19eb7496418588ea7375d9
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 9e7fd9b..e952b92 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 9e7fd9b420ac9a5caa045cf82b566cc0b51c93ad
+Subproject commit e952b920ecbee5225f1098a02d4a39b19aa7e234
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index a029df4..7bca96d 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -32,3 +32,43 @@
         "@org_golang_x_tools//godoc/vfs/zipfs:go_default_library",
     ],
 )
+
+# 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"],
+    data = [
+        "@ui_dev_npm//@open-wc/karma-esm",
+        "@ui_dev_npm//chai",
+        "@ui_dev_npm//karma-chrome-launcher",
+        "@ui_dev_npm//karma-mocha",
+        "@ui_dev_npm//karma-mocha-reporter",
+        "@ui_dev_npm//karma/bin:karma",
+        "@ui_dev_npm//mocha",
+    ],
+)
+
+# 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(
+    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",
+    ],
+)
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
new file mode 100644
index 0000000..c9a5d9b
--- /dev/null
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -0,0 +1,263 @@
+# Gerrit JavaScript style guide
+
+Gerrit frontend follows [recommended eslint rules](https://eslint.org/docs/rules/)
+and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html).
+Eslint is used to automate rules checking where possible. You can find exact eslint rules
+[here](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/.eslintrc.js).
+
+Gerrit JavaScript code uses ES6 modules and doesn't use goog.module files.
+
+Additionally to the rules above, Gerrit frontend uses the following rules (some of them have automated checks,
+some don't):
+
+- [Use destructuring imports only](#destructuring-imports-only)
+- [Use classes and services for storing and manipulating global state](#services-for-global-state)
+- [Pass required services in the constructor for plain classes](#pass-dependencies-in-constructor)
+- [Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)
+
+## <a name="destructuring-imports-only"></a>Use destructuring imports only
+Always use destructuring import statement and specify all required names explicitly (e.g. `import {a,b,c} from '...'`)
+where possible.
+
+**Note:** Destructuring imports are not always possible with 3rd-party libraries, because a 3rd-party library
+can expose a class/function/const/etc... as a default export. In this situation you can use default import, but please
+keep consistent naming across the whole gerrit project. The best way to keep consistency is to search across our
+codebase for the same import. If you find an exact match - always use the same name for your import. If you can't
+find exact matches - find a similar import and assign appropriate/similar name for your default import. Usually the
+name should include a library name and part of the file path.
+
+You can read more about different type of imports
+[here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).
+
+**Good:**
+```Javascript
+// Import from the module in the same project.
+import {getDisplayName, getAccount} from './user-utils.js'
+
+// The following default import is allowed only for 3rd-party libraries.
+// Please ensure, that all imports have the same name accross gerrit project (downloadImage in this example)
+import downloadImage from 'third-party-library/images/download.js'
+```
+
+**Bad:**
+```Javascript
+import * as userUtils from './user-utils.js'
+```
+
+## <a name="services-for-global-state"></a>Use classes and services for storing and manipulating global state
+
+You must use classes and services to share global state across the gerrit frontend code. Do not put a state at the
+top level of a module.
+
+It is not easy to define precise what can be a shared global state and what is not. Below are some
+examples of what can treated as a shared global state:
+
+* Information about enabled experiments
+* Information about current user
+* Information about current change
+
+**Note:**
+
+Service name must ends with a `Service` suffix.
+
+To share global state across modules in the project, do the following:
+- put the state in a class
+- add a new service to the
+[appContext](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/services/app-context.js)
+- add a service initialization code to the
+[services/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/services/app-context-init.js) file.
+- add a service or service-mock initialization code to the
+[embed/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/embed/app-context-init.js) file.
+- recommended: add a separate service-mock for testing. Do not use the same mock for testing and for
+the shared gr-diff (i.e. in the `services/app-context-init.js`). Even if the mocks are simple and looks
+identically, keep them separate. It allows to change them independently in the future.
+
+Also see the example below if a service depends on another services.
+
+**Note 1:** Be carefull with the shared gr-diff element. If a service is not required for the shared gr-diff,
+the safest option is to provide a mock for this service in the embed/app-context-init.js file. In exceptional
+cases you can keep the service uninitialized in
+[embed/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/embed/app-context-init.js) file
+, but it is recommended to write a comment why mocking is not possible. In the future we can
+review/update rules regarding the shared gr-diff element.
+
+**Good:**
+```Javascript
+export class CounterService {
+    constructor() {
+        this._count = 0;
+    }
+    get count() {
+        return this._count;
+    }
+    inc() {
+        this._count++;
+    }
+}
+
+// app-context.js
+export const appContext = {
+    //...
+    mouseClickCounterService: null,
+    keypressCounterService: null,
+};
+
+// services/app-context-init.js
+export function initAppContext() {
+    //...
+    // Add the following line before the Object.defineProperties(appContext, registeredServices);
+    addService('mouseClickCounterService', () => new CounterService());
+    addService('keypressCounterService', () => new CounterService());
+    // If a service depends on other services, pass dependencies as shown below
+    // If circular dependencies exist, app-init-context tests fail with timeout or stack overflow
+    // (we are  going to improve it in the future)
+    addService('analyticService', () =>
+        new CounterService(appContext.mouseClickCounterService, appContext.keypressCounterService));
+    //...
+    // This following line must remains the last one in the initAppContext
+    Object.defineProperties(appContext, registeredServices);
+}
+```
+
+**Bad:**
+```Javascript
+// module counter.js
+// Incorrect: shared state declared at the top level of the counter.js module
+let count = 0;
+export function getCount() {
+    return count;
+}
+export function incCount() {
+    count++;
+}
+```
+
+## <a name="pass-dependencies-in-constructor"></a>Pass required services in the constructor for plain classes
+
+If a class/service depends on some other service (or multiple services), the class must accept all dependencies
+as parameters in the constructor.
+
+Do not use appContext anywhere else in a class.
+
+**Note:** This rule doesn't apply for HTML/Polymer elements classes. A browser creates instances of such classes
+implicitly and calls the constructor without parameters. See
+[Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)
+
+**Good:**
+```Javascript
+export class UserService {
+    constructor(restApiService) {
+        this._restApiService = restApiService;
+    }
+    getLoggedIn() {
+        // Send request to server using this._restApiService
+    }
+}
+```
+
+**Bad:**
+```Javascript
+import {appContext} from "./app-context";
+
+export class UserService {
+    constructor() {
+        // Incorrect: you must pass all dependencies to a constructor
+        this._restApiService = appContext.restApiService;
+    }
+}
+
+export class AdminService {
+    isAdmin() {
+        // Incorrect: you must pass all dependencies to a constructor
+        return appContext.restApiService.sendRequest(...);
+    }
+}
+
+```
+
+## <a name="assign-dependencies-in-html-element-constructor"></a>Assign required services in a HTML/Polymer element constructor
+If a class is a custom HTML/Polymer element, the class must assign all required services in the constructor.
+A browser creates instances of such classes implicitly, so it is impossible to pass anything as a parameter to
+the element's class constructor.
+
+Do not use appContext anywhere except the constructor of the class.
+
+**Note for legacy elements:** If a polymer element extends a LegacyElementMixin and overrides the `created()` method,
+move all code from this method to a constructor right after the call to a `super()`
+([example](#assign-dependencies-legacy-element-example)). The `created()`
+method is [deprecated](https://polymer-library.polymer-project.org/2.0/docs/about_20#lifecycle-changes) and is called
+when a super (i.e. base) class constructor is called. If you are unsure about moving the code from the `created` method
+to the class constructor, consult with the source code:
+[`LegacyElementMixin._initializeProperties`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/legacy/legacy-element-mixin.js#L318)
+and
+[`PropertiesChanged.constructor`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/mixins/properties-changed.js#L177)
+
+
+
+**Good:**
+```Javascript
+import {appContext} from `.../services/app-context.js`;
+
+export class MyCustomElement extends ...{
+    constructor() {
+        super(); //This is mandatory to call parent constructor
+        this._userService = appContext.userService;
+    }
+    //...
+    _getUserName() {
+        return this._userService.activeUserName();
+    }
+}
+```
+
+**Bad:**
+```Javascript
+import {appContext} from `.../services/app-context.js`;
+
+export class MyCustomElement extends ...{
+    created() {
+        // Incorrect: assign all dependencies in the constructor
+        this._userService = appContext.userService;
+    }
+    //...
+    _getUserName() {
+        // Incorrect: use appContext outside of a constructor
+        return appContext.userService.activeUserName();
+    }
+}
+```
+
+<a name="assign-dependencies-legacy-element-example"></a>
+**Legacy element:**
+
+Before:
+```Javascript
+export class MyCustomElement extends ...LegacyElementMixin(...) {
+    constructor() {
+        super();
+        someAction();
+    }
+    created() {
+        super();
+        createdAction1();
+        createdAction2();
+    }
+}
+```
+
+After:
+```Javascript
+export class MyCustomElement extends ...LegacyElementMixin(...) {
+    constructor() {
+        super();
+        // Assign services here
+        this._userService = appContext.userService;
+        // Code from the created method - put it before existing actions in constructor
+        createdAction1();
+        createdAction2();
+        // Original constructor code
+        someAction();
+    }
+    // created method is removed
+}
+```
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index de25d79..29ba857 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -148,10 +148,13 @@
 ## Running Tests
 
 For daily development you typically only want to run and debug individual tests.
-Run the local [Go proxy server](#go-server) and navigate for example to
-<http://localhost:8081/elements/shared/gr-account-entry/gr-account-entry_test.html>.
-Check "Disable cache" in the "Network" tab of Chrome's dev tools, so code
-changes are picked up on "reload".
+There are 2 types of fronted tests in gerrit:
+- Karma tests - all tests matches `*_test.js` pattern
+- web-component-tester(WCT) tests - all tests matches the `*_test.html` pattern.
+
+**Note:** WCT tests are deprecated. We are migrating to Karma tests now. If you are going to change
+something in a WCT test file, we strongly recommend to convert it to Karma tests before making
+any change. See [Converting WCT tests to Karma](#wct-to-karma).
 
 Our CI integration ensures that all tests are run when you upload a change to
 Gerrit, but you can also run all tests locally in headless mode:
@@ -160,6 +163,44 @@
 npm test
 ```
 
+### Running Karma tests
+There are several ways to run Karma tests:
+
+* Run all Karma tests in headless mode:
+```sh
+npm run test:karma
+```
+
+* Run all Karma tests in debug mode (the command opens Chrome browser with
+the default Karma page; you should click the "Debug" button to start testing):
+```sh
+npm run test:karma:debug
+```
+
+* Run a single test file:
+```
+# Headless mode
+npm run test:karma async-foreach-behavior_test.js
+# Debug mode
+npm run test:karma:debug async-foreach-behavior_test.js
+```
+
+* You can run tests in IDE:
+  - [IntelliJ: running unit tests on Karma](https://www.jetbrains.com/help/idea/running-unit-tests-on-karma.html#ws_karma_running)
+
+### Running WCT tests
+
+Run the local [Go proxy server](#go-server) and navigate for example to
+<http://localhost:8081/elements/shared/gr-account-entry/gr-account-entry_test.html>.
+Check "Disable cache" in the "Network" tab of Chrome's dev tools, so code
+changes are picked up on "reload".
+
+You can also run all WCT tests locally in headless mode:
+
+```sh
+npm test:wct
+```
+
 To allow the tests to run in Safari:
 
 * In the Advanced preferences tab, check "Show Develop menu in menu bar".
@@ -171,6 +212,96 @@
 WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh
 ```
 
+### <a name="wct-to-karma"></a>Converting WCT tests to Karma
+
+If you are want to change a WCT test file (any `..._test.html` file), please convert the file to a
+Karma test file before making any changes. It is better to make a conversion in a separate change,
+so any conversion-related problems can be catch at this step.
+
+Usually, our WCT tests files have the following structure:
+```Html
+<!-- Test header: meta, title, wct scripts -->
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<meta charset="utf-8">
+<title>gr-account-link</title>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<!-- Templates for test fixtures (optional) -->
+<test-fixture id="basic">
+   <template>
+     <gr-account-link></gr-account-link>
+   </template>
+ </test-fixture>
+
+<test-fixture id="other">
+   <template>
+     <gr-dialog>
+       <span>Hello!</span>
+     </gr-dialog>
+   </template>
+ </test-fixture>
+
+<!-- Tests -->
+<script type="module">
+// One or more imports:
+import '../../../test/common-test-setup.js';
+import ...;
+
+// Tests - one or more suites
+suite(..., () => {
+   ...
+   // instantiate 'basic' template:
+   element = fixture('basic');
+
+   ...
+   // instantiate 'other' template:
+   otherElements = fixture('other');
+
+);
+</script>
+```
+
+A conversion requires the following changes:
+* Rename the `..._test.html` file to the `..._test.js` file.
+* Remove test header (see a WCT test example above)
+* Remove all `<script...>` and `</script>` tags, but preserve javascript code
+* Change imports - use `test/common-test-setup-karma.js` instead of `test/common-test-setup.js`.
+Ensure, that the `common-test-setup-karma.js` import is placed above any other imports.
+* If there are test fixtures in the html file, move them inside a `<script>` tag and use
+the `fixtureFromTemplate` or `fixtureFromElement` (if there is only one element in a template)
+to define a test fixture template.
+* Use `instantiate` method instead of `fixture` method.
+
+After conversion, the Karma test file for the example above can look like:
+```Javascript
+import '../../../test/common-test-setup-karma.js';
+// Other imports:
+import ...
+
+// Define test fixtures templates:
+const fixture =
+  fixtureFromElement('gr-account-link');
+const otherFixture = fixtureFromTemplate(html`<gr-dialog>
+  <span>Hello!</span>
+</gr-dialog>
+`);
+
+// Tests - one or more suites
+suite(..., () => {
+   ...
+   // instantiate 'basic' template:
+   element = fixture.instantiate();
+
+   ...
+   // instantiate 'other' template:
+   otherElements = otherFixture.instantiate();
+
+);
+```
+
 ## Style guide
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
@@ -299,4 +430,4 @@
 
 // install all dependencies and start the server
 npm start
-```
\ No newline at end of file
+```
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 0302a76..e7506e0 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -109,7 +109,7 @@
     "prefer-promise-reject-errors": "error",
     "prefer-spread": "error",
     "quote-props": ["error", "consistent-as-needed"],
-    "semi": [2, "always"],
+    "semi": ["error", "always"],
     "template-curly-spacing": "error",
 
     "require-jsdoc": 0,
@@ -165,11 +165,9 @@
     // You must not add anything new in this list!
     // Instead export variables from modules
     // TODO(dmfilippov): Remove global variables from polygerrit
-    "GrReporting": "readonly",
     // Global variables from 3rd party libraries.
     // You should not add anything in this list, always try to import
     // If import is not possible - you can extend this list
-    "Polymer": "readonly",
     "ShadyCSS": "readonly",
     "linkify": "readonly",
     "security": "readonly",
@@ -182,7 +180,13 @@
       },
     },
     {
-      "files": ["*.html", "common-test-setup.js"],
+      "files": [
+        "*.html",
+        "common-test-setup.js",
+        "common-test-setup-karma.js",
+        "*_test.js",
+        "a11y-test-utils.js",
+      ],
       // Additional global variables allowed in tests
       "globals": {
         // Global variables from 3rd party test libraries/frameworks.
@@ -190,6 +194,7 @@
         // variables from these libraries and import is not possible
         "MockInteractions": "readonly",
         "_": "readonly",
+        "axs": "readonly",
         "a11ySuite": "readonly",
         "assert": "readonly",
         "expect": "readonly",
@@ -203,6 +208,8 @@
         "suiteSetup": "readonly",
         "teardown": "readonly",
         "test": "readonly",
+        "fixtureFromElement": "readonly",
+        "fixtureFromTemplate": "readonly",
       }
     },
     {
@@ -216,6 +223,7 @@
       "globals": {
         // Settings for samples. You can add globals here if you want to use it
         "Gerrit": "readonly",
+        "Polymer": "readonly",
       }
     },
     {
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index ca021db..096c665 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -58,6 +58,7 @@
     name = "test-srcs-fg",
     srcs = [
         "test/common-test-setup.js",
+        "test/common-test-setup-karma.js",
         "test/index.html",
         ":pg_code",
         "@ui_dev_npm//:node_modules",
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
deleted file mode 100644
index 1d50cc4..0000000
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>async-foreach-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../test/common-test-setup.js';
-import {AsyncForeachBehavior} from './async-foreach-behavior.js';
-suite('async-foreach-behavior tests', () => {
-  test('loops over each item', () => {
-    const fn = sinon.stub().returns(Promise.resolve());
-    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
-        .then(() => {
-          assert.isTrue(fn.calledThrice);
-          assert.equal(fn.getCall(0).args[0], 1);
-          assert.equal(fn.getCall(1).args[0], 2);
-          assert.equal(fn.getCall(2).args[0], 3);
-        });
-  });
-
-  test('halts on stop condition', () => {
-    const stub = sinon.stub();
-    const fn = (e, stop) => {
-      stub(e);
-      stop();
-      return Promise.resolve();
-    };
-    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
-        .then(() => {
-          assert.isTrue(stub.calledOnce);
-          assert.equal(stub.lastCall.args[0], 1);
-        });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js
new file mode 100644
index 0000000..0a44884
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js
@@ -0,0 +1,45 @@
+/**
+ * @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 {AsyncForeachBehavior} from './async-foreach-behavior.js';
+suite('async-foreach-behavior tests', () => {
+  test('loops over each item', () => {
+    const fn = sinon.stub().returns(Promise.resolve());
+    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+        .then(() => {
+          assert.isTrue(fn.calledThrice);
+          assert.equal(fn.getCall(0).args[0], 1);
+          assert.equal(fn.getCall(1).args[0], 2);
+          assert.equal(fn.getCall(2).args[0], 3);
+        });
+  });
+
+  test('halts on stop condition', () => {
+    const stub = sinon.stub();
+    const fn = (e, stop) => {
+      stub(e);
+      stop();
+      return Promise.resolve();
+    };
+    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+        .then(() => {
+          assert.isTrue(stub.calledOnce);
+          assert.equal(stub.lastCall.args[0], 1);
+        });
+  });
+});
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
index b8d54a4..46dfaf2 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {descendedFromClass} from '../../utils/dom-util.js';
+
 export const DomUtilBehavior = {
   /**
    * Are any ancestors of the element (or the element itself) members of the
@@ -28,13 +30,9 @@
    * @return {boolean}
    */
   descendedFromClass(element, className, opt_stopElement) {
-    let isDescendant = element.classList.contains(className);
-    while (!isDescendant && element.parentElement &&
-        (!opt_stopElement || element.parentElement !== opt_stopElement)) {
-      isDescendant = element.classList.contains(className);
-      element = element.parentElement;
-    }
-    return isDescendant;
+    console.warn('DomUtilBehavior is deprecated.' +
+     'Use descendedFromClass from utils directly.');
+    return descendedFromClass(element, className, opt_stopElement);
   },
 };
 
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
deleted file mode 100644
index 88c8835..0000000
--- a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 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.
- */
-
-/** @polymerBehavior Gerrit.FireBehavior */
-export const FireBehavior = {
-  /**
-   * Dispatches a custom event with an optional detail value.
-   *
-   * @param {string} type Name of event type.
-   * @param {*=} detail Detail value containing event-specific
-   *   payload.
-   * @param {{ bubbles: (boolean|undefined), cancelable: (boolean|undefined),
-   *     composed: (boolean|undefined) }=}
-   *  options Object specifying options.  These may include:
-   *  `bubbles` (boolean, defaults to `true`),
-   *  `cancelable` (boolean, defaults to false), and
-   *  `composed` (boolean, defaults to true).
-   * @return {!Event} The new event that was fired.
-   * @override
-   */
-  fire(type, detail, options) {
-    console.warn('\'fire\' is deprecated, please use dispatchEvent instead!');
-    options = options || {};
-    detail = (detail === null || detail === undefined) ? {} : detail;
-    const event = new Event(type, {
-      bubbles: options.bubbles === undefined ? true : options.bubbles,
-      cancelable: Boolean(options.cancelable),
-      composed: options.composed === undefined ? true: options.composed,
-    });
-    event.detail = detail;
-    this.dispatchEvent(event);
-    return event;
-  },
-};
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use the behavior because it was accessible with
-// the global Gerrit... variable. To avoid breaking changes in plugins
-// temporary assign global variables.
-window.Gerrit = window.Gerrit || {};
-window.Gerrit.FireBehavior = FireBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
index 3c48fbe..51c52f1 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
@@ -87,7 +87,7 @@
     let links = ADMIN_LINKS.slice(0);
     let expandedSection;
 
-    const isExernalLink = link => link.url[0] !== '/';
+    const isExternalLink = link => link.url[0] !== '/';
 
     // Append top-level links that are defined by plugins.
     links.push(...getAdminMenuLinks().map(link => {
@@ -95,10 +95,10 @@
         url: link.url,
         name: link.text,
         capability: link.capability || null,
-        noBaseUrl: !isExernalLink(link),
+        noBaseUrl: !isExternalLink(link),
         view: null,
         viewableToAll: !link.capability,
-        target: isExernalLink(link) ? '_blank' : null,
+        target: isExternalLink(link) ? '_blank' : null,
       };
     }));
 
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
index 7745b42..be8c811 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 
+import {SpecialFilePath} from '../../constants/constants.js';
+
 /** @polymerBehavior Gerrit.PathListBehavior */
 export const PathListBehavior = {
 
-  COMMIT_MESSAGE_PATH: '/COMMIT_MSG',
-  MERGE_LIST_PATH: '/MERGE_LIST',
-
   /**
    * @param {string} a
    * @param {string} b
@@ -28,18 +27,18 @@
    */
   specialFilePathCompare(a, b) {
     // The commit message always goes first.
-    if (a === PathListBehavior.COMMIT_MESSAGE_PATH) {
+    if (a === SpecialFilePath.COMMIT_MESSAGE) {
       return -1;
     }
-    if (b === PathListBehavior.COMMIT_MESSAGE_PATH) {
+    if (b === SpecialFilePath.COMMIT_MESSAGE) {
       return 1;
     }
 
     // The merge list always comes next.
-    if (a === PathListBehavior.MERGE_LIST_PATH) {
+    if (a === SpecialFilePath.MERGE_LIST) {
       return -1;
     }
-    if (b === PathListBehavior.MERGE_LIST_PATH) {
+    if (b === SpecialFilePath.MERGE_LIST) {
       return 1;
     }
 
@@ -67,10 +66,22 @@
     return aFile.localeCompare(bFile) || a.localeCompare(b);
   },
 
+  shouldHideFile(file) {
+    return file === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+  },
+
+  addUnmodifiedFiles(files, commentedPaths) {
+    Object.keys(commentedPaths).forEach(commentedPath => {
+      if (files.hasOwnProperty(commentedPath) ||
+        this.shouldHideFile(commentedPath)) { return; }
+      files[commentedPath] = {status: 'U'};
+    });
+  },
+
   computeDisplayPath(path) {
-    if (path === PathListBehavior.COMMIT_MESSAGE_PATH) {
+    if (path === SpecialFilePath.COMMIT_MESSAGE) {
       return 'Commit message';
-    } else if (path === PathListBehavior.MERGE_LIST_PATH) {
+    } else if (path === SpecialFilePath.MERGE_LIST) {
       return 'Merge list';
     }
     return path;
@@ -78,8 +89,8 @@
 
   isMagicPath(path) {
     return !!path &&
-        (path === PathListBehavior.COMMIT_MESSAGE_PATH || path ===
-            PathListBehavior.MERGE_LIST_PATH);
+        (path === SpecialFilePath.COMMIT_MESSAGE || path ===
+            SpecialFilePath.MERGE_LIST);
   },
 
   computeTruncatedPath(path) {
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 1b7a42a..cf5105e 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -24,6 +24,8 @@
 <script type="module">
 import '../../test/common-test-setup.js';
 import {PathListBehavior} from './gr-path-list-behavior.js';
+import {SpecialFilePath} from '../../constants/constants.js';
+
 suite('gr-path-list-behavior tests', () => {
   test('special sort', () => {
     const sort = PathListBehavior.specialFilePathCompare;
@@ -63,6 +65,20 @@
     assert.isTrue(isMagic('/MERGE_LIST'));
   });
 
+  test('patchset level comments are hidden', () => {
+    const commentedPaths = {
+      [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: true,
+      'file1.txt': true,
+    };
+
+    const files = {'file2.txt': {status: 'M'}};
+    PathListBehavior.addUnmodifiedFiles(files, commentedPaths);
+    assert.equal(files['file1.txt'].status, 'U');
+    assert.equal(files['file2.txt'].status, 'M');
+    assert.isFalse(files.hasOwnProperty(
+        SpecialFilePath.PATCHSET_LEVEL_COMMENTS));
+  });
+
   test('truncatePath with long path should add ellipsis', () => {
     const truncatePath = PathListBehavior.truncatePath;
     let path = 'level1/level2/level3/level4/file.js';
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 2a592f0..cc8b55a 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../scripts/bundled-polymer.js';
-
 import '../../elements/shared/gr-tooltip/gr-tooltip.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {getRootElement} from '../../scripts/rootElement.js';
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index cb21a9f..ca31ee8 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -95,12 +95,6 @@
 NOTE: doc-only shortcuts will not be customizable in the same way that other
 shortcuts are.
 */
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-import '../../scripts/bundled-polymer.js';
 
 import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -177,6 +171,7 @@
   TOGGLE_FILE_REVIEWED: 'TOGGLE_FILE_REVIEWED',
   TOGGLE_ALL_INLINE_DIFFS: 'TOGGLE_ALL_INLINE_DIFFS',
   TOGGLE_INLINE_DIFF: 'TOGGLE_INLINE_DIFF',
+  TOGGLE_HIDE_ALL_COMMENT_THREADS: 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
 
   OPEN_FIRST_FILE: 'OPEN_FIRST_FILE',
   OPEN_LAST_FILE: 'OPEN_LAST_FILE',
@@ -257,6 +252,8 @@
     'Expand all comment threads');
 _describe(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
     'Collapse all comment threads');
+_describe(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
+    'Hide/Display all comment threads');
 _describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane');
 _describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane');
 _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS,
@@ -296,6 +293,8 @@
     'Go to selected file');
 _describe(Shortcut.TOGGLE_ALL_INLINE_DIFFS, ShortcutSection.FILE_LIST,
     'Show/hide all inline diffs');
+_describe(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.FILE_LIST,
+    'Hide/Display all comment threads');
 _describe(Shortcut.TOGGLE_INLINE_DIFF, ShortcutSection.FILE_LIST,
     'Show/hide selected inline diff');
 
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
index 919a763..05bf169 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
@@ -14,8 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../scripts/bundled-polymer.js';
 import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
+import {ChangeStatus} from '../../constants/constants.js';
 
 /** @polymerBehavior Gerrit.RESTClientBehavior */
 export const RESTClientBehavior = [{
@@ -28,12 +28,6 @@
     REWRITE: 'REWRITE',
   },
 
-  ChangeStatus: {
-    ABANDONED: 'ABANDONED',
-    MERGED: 'MERGED',
-    NEW: 'NEW',
-  },
-
   // Must be kept in sync with the ListChangesOption enum and protobuf.
   ListChangesOption: {
     LABELS: 0,
@@ -125,7 +119,7 @@
   },
 
   changeIsOpen(change) {
-    return change && change.status === this.ChangeStatus.NEW;
+    return change && change.status === ChangeStatus.NEW;
   },
 
   /**
@@ -136,9 +130,9 @@
    */
   changeStatuses(change, opt_options) {
     const states = [];
-    if (change.status === this.ChangeStatus.MERGED) {
+    if (change.status === ChangeStatus.MERGED) {
       states.push('Merged');
-    } else if (change.status === this.ChangeStatus.ABANDONED) {
+    } else if (change.status === ChangeStatus.ABANDONED) {
       states.push('Abandoned');
     } else if (change.mergeable === false ||
         (opt_options && opt_options.mergeable === false)) {
diff --git a/polygerrit-ui/app/constants/constants.js b/polygerrit-ui/app/constants/constants.js
index cab50f6..ff34442 100644
--- a/polygerrit-ui/app/constants/constants.js
+++ b/polygerrit-ui/app/constants/constants.js
@@ -19,17 +19,68 @@
  * @enum
  * @desc Tab names for primary tabs on change view page.
  */
-export const PrimaryTabs = {
-  FILES: '_files',
-  FINDINGS: '_findings',
+export const PrimaryTab = {
+  FILES: 'files',
+  /**
+   * When renaming this, the links in UrlFormatter must be updated.
+   */
+  COMMENT_THREADS: 'comments',
+  FINDINGS: 'findings',
 };
 
 /**
  * @enum
  * @desc Tab names for secondary tabs on change view page.
  */
-export const SecondaryTabs = {
+export const SecondaryTab = {
   CHANGE_LOG: '_changeLog',
-  COMMENT_THREADS: '_commentThreads',
 };
 
+/**
+ * @enum
+ * @desc Tag names of change log messages.
+ */
+export const MessageTag = {
+  TAG_DELETE_REVIEWER: 'autogenerated:gerrit:deleteReviewer',
+  TAG_NEW_PATCHSET: 'autogenerated:gerrit:newPatchSet',
+  TAG_NEW_WIP_PATCHSET: 'autogenerated:gerrit:newWipPatchSet',
+  TAG_REVIEWER_UPDATE: 'autogenerated:gerrit:reviewerUpdate',
+  TAG_SET_PRIVATE: 'autogenerated:gerrit:setPrivate',
+  TAG_UNSET_PRIVATE: 'autogenerated:gerrit:unsetPrivate',
+  TAG_SET_READY: 'autogenerated:gerrit:setReadyForReview',
+  TAG_SET_WIP: 'autogenerated:gerrit:setWorkInProgress',
+  TAG_SET_ASSIGNEE: 'autogenerated:gerrit:setAssignee',
+  TAG_UNSET_ASSIGNEE: 'autogenerated:gerrit:deleteAssignee',
+};
+
+/**
+ * @enum
+ * @desc Modes for gr-diff-cursor
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+export const ScrollMode = {
+  KEEP_VISIBLE: 'keep-visible',
+  NEVER: 'never',
+};
+
+/**
+ * @enum
+ * @desc Specifies status for a change
+ */
+export const ChangeStatus = {
+  ABANDONED: 'ABANDONED',
+  MERGED: 'MERGED',
+  NEW: 'NEW',
+};
+
+/**
+ * @enum
+ * @desc Special file paths
+ */
+export const SpecialFilePath = {
+  PATCHSET_LEVEL_COMMENTS: '/PATCHSET_LEVEL',
+  COMMIT_MESSAGE: '/COMMIT_MSG',
+  MERGE_LIST: '/MERGE_LIST',
+};
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index e07a64e..632387e 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -51,7 +50,7 @@
 const LABEL = 'Label';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccessSection extends mixinBehaviors( [
   AccessBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
index c46cf30..e9c16241 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
@@ -82,7 +82,7 @@
     <div id="mainContainer">
       <div class="header">
         <div class="name">
-          <h3>[[_computeSectionName(section.id)]]</h3>
+          <h3 class="heading-3">[[_computeSectionName(section.id)]]</h3>
           <gr-button
             id="editBtn"
             link=""
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
similarity index 92%
rename from polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
rename to polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
index 95345fe..2128e29 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
@@ -1,47 +1,32 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-access-section</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-access-section></gr-access-section>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-access-section.js';
+
+const fixture = fixtureFromElement('gr-access-section');
+
 suite('gr-access-section tests', () => {
   let element;
   let sandbox;
 
   setup(() => {
     sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = fixture.instantiate();
   });
 
   teardown(() => {
@@ -245,7 +230,7 @@
     test('_computeSectionName', () => {
       let name;
       // When computing the section name for an undefined name, it means a
-      // new section is being added. In this case, it should defualt to
+      // new section is being added. In this case, it should default to
       // 'refs/heads/*'.
       element._editingRef = false;
       assert.equal(element._computeSectionName(name),
@@ -477,7 +462,7 @@
       });
 
       test('_handleValueChange', () => {
-        // For an exising section.
+        // For an existing section.
         const modifiedHandler = sandbox.stub();
         element.section = {id: 'refs/for/bar', value: {permissions: {}}};
         assert.notOk(element.section.value.updatedId);
@@ -553,4 +538,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 36e2cc4..865fd33 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
@@ -33,7 +32,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAdminGroupList extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index b318ee6..25ba83e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-menu-page-styles.js';
 import '../../../styles/gr-page-nav-styles.js';
 import '../../../styles/shared-styles.js';
@@ -48,7 +47,7 @@
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAdminView extends mixinBehaviors( [
   AdminNavBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index b6b21bd..288af4c 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -30,7 +28,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmDeleteItemDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index 3347655..e16f5ce 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
 import '@polymer/iron-input/iron-input.js';
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
@@ -37,7 +34,7 @@
 const REF_PREFIX = 'refs/heads/';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateChangeDialog extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -152,11 +149,7 @@
     } else if (config && config.configured_value === 'FALSE') {
       return false;
     } else if (config && config.configured_value === 'INHERIT') {
-      if (config && config.inherited_value) {
-        return true;
-      } else {
-        return false;
-      }
+      return !!(config && config.inherited_value);
     } else {
       return false;
     }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index 87105a7..eaee4a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -57,7 +57,7 @@
       },
     });
     element = fixture('basic');
-    element.repoName = 'test-repo',
+    element.repoName = 'test-repo';
     element._repoConfig = {
       private_by_default: {
         configured_value: 'FALSE',
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index b21bdde..7f33663 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -30,7 +28,7 @@
 import page from 'page/page.mjs';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateGroupDialog extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index 72a6b99..eb131f5 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -37,7 +35,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreatePointerDialog extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index 040f41b..0410f14 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -33,7 +31,7 @@
 import page from 'page/page.mjs';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateRepoDialog extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index a3c05cb..2c0c1302 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
@@ -32,7 +31,7 @@
 const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrGroupAuditLog extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 45f7612..c54a709 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-subpage-styles.js';
@@ -41,7 +40,7 @@
 const URL_REGEX = '^(?:[a-z]+:)?//';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrGroupMembers extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -122,12 +121,12 @@
           this._groupName = config.name;
 
           promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
-            this._isAdmin = isAdmin ? true : false;
+            this._isAdmin = !!isAdmin;
           }));
 
           promises.push(this.$.restAPI.getIsGroupOwner(config.name)
               .then(isOwner => {
-                this._groupOwner = isOwner ? true : false;
+                this._groupOwner = !!isOwner;
               }));
 
           promises.push(this.$.restAPI.getGroupMembers(config.name).then(
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
index c5577c2..47657ac 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
@@ -63,9 +63,9 @@
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h1 id="Title">[[_groupName]]</h1>
+      <h1 id="Title" class="heading-1">[[_groupName]]</h1>
       <div id="form">
-        <h3 id="members">Members</h3>
+        <h3 id="members" class="heading-3">Members</h3>
         <fieldset>
           <span class="value">
             <gr-autocomplete
@@ -112,7 +112,7 @@
             </tbody>
           </table>
         </fieldset>
-        <h3 id="includedGroups">Included Groups</h3>
+        <h3 id="includedGroups" class="heading-3">Included Groups</h3>
         <fieldset>
           <span class="value">
             <gr-autocomplete
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 4dd9a7b..3a5bfd8 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -302,32 +302,32 @@
   });
 
   test('delete member', () => {
-    const deletelBtns = dom(element.root)
+    const deleteBtns = dom(element.root)
         .querySelectorAll('.deleteMembersButton');
-    MockInteractions.tap(deletelBtns[0]);
+    MockInteractions.tap(deleteBtns[0]);
     assert.equal(element._itemId, '1000097');
     assert.equal(element._itemName, 'jane');
-    MockInteractions.tap(deletelBtns[1]);
+    MockInteractions.tap(deleteBtns[1]);
     assert.equal(element._itemId, '1000096');
     assert.equal(element._itemName, 'Test User');
-    MockInteractions.tap(deletelBtns[2]);
+    MockInteractions.tap(deleteBtns[2]);
     assert.equal(element._itemId, '1000095');
     assert.equal(element._itemName, 'Gerrit');
-    MockInteractions.tap(deletelBtns[3]);
+    MockInteractions.tap(deleteBtns[3]);
     assert.equal(element._itemId, '1000098');
     assert.equal(element._itemName, '1000098');
   });
 
   test('delete included groups', () => {
-    const deletelBtns = dom(element.root)
+    const deleteBtns = dom(element.root)
         .querySelectorAll('.deleteIncludedGroupButton');
-    MockInteractions.tap(deletelBtns[0]);
+    MockInteractions.tap(deleteBtns[0]);
     assert.equal(element._itemId, 'testId');
     assert.equal(element._itemName, 'testName');
-    MockInteractions.tap(deletelBtns[1]);
+    MockInteractions.tap(deleteBtns[1]);
     assert.equal(element._itemId, 'testId2');
     assert.equal(element._itemName, 'testName2');
-    MockInteractions.tap(deletelBtns[2]);
+    MockInteractions.tap(deleteBtns[2]);
     assert.equal(element._itemId, 'testId3');
     assert.equal(element._itemName, 'testName3');
   });
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 1c7cc91..49cd8ea 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-subpage-styles.js';
@@ -43,7 +42,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrGroup extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -142,12 +141,12 @@
           this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
 
           promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
-            this._isAdmin = isAdmin ? true : false;
+            this._isAdmin = !!isAdmin;
           }));
 
           promises.push(this.$.restAPI.getIsGroupOwner(config.name)
               .then(isOwner => {
-                this._groupOwner = isOwner ? true : false;
+                this._groupOwner = !!isOwner;
               }));
 
           // If visible to all is undefined, set to false. If it is defined
@@ -262,7 +261,7 @@
   }
 
   _computeGroupDisabled(owner, admin, groupIsInternal) {
-    return groupIsInternal && (admin || owner) ? false : true;
+    return !(groupIsInternal && (admin || owner));
   }
 
   _getGroupUUID(id) {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
index e11f989..7a843dc 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -37,18 +37,21 @@
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h1 id="Title">[[_groupName]]</h1>
-      <h2 id="configurations">General</h2>
+      <h1 id="Title" class="heading-1">[[_groupName]]</h1>
+      <h2 id="configurations" class="heading-2">General</h2>
       <div id="form">
         <fieldset>
-          <h3 id="groupUUID">Group UUID</h3>
+          <h3 id="groupUUID" class="heading-3">Group UUID</h3>
           <fieldset>
             <gr-copy-clipboard
               id="uuid"
               text="[[_getGroupUUID(_groupConfig.id)]]"
             ></gr-copy-clipboard>
           </fieldset>
-          <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
+          <h3
+            id="groupName"
+            class$="heading-3 [[_computeHeaderClass(_rename)]]"
+          >
             Group Name
           </h3>
           <fieldset>
@@ -72,7 +75,10 @@
               >
             </span>
           </fieldset>
-          <h3 id="groupOwner" class$="[[_computeHeaderClass(_owner)]]">
+          <h3
+            id="groupOwner"
+            class$="heading-3 [[_computeHeaderClass(_owner)]]"
+          >
             Owners
           </h3>
           <fieldset>
@@ -99,7 +105,7 @@
               >
             </span>
           </fieldset>
-          <h3 class$="[[_computeHeaderClass(_description)]]">
+          <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
             Description
           </h3>
           <fieldset>
@@ -123,10 +129,10 @@
               </gr-button>
             </span>
           </fieldset>
-          <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
+          <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
             Group Options
           </h3>
-          <fieldset id="visableToAll">
+          <fieldset>
             <section>
               <span class="title">
                 Make group visible to all registered users
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 9fac11e..d25ee76 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -48,7 +47,7 @@
  * Fired when a permission that was previously added was removed.
  *
  * @event added-permission-removed
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrPermission extends mixinBehaviors( [
   AccessBehavior,
@@ -211,11 +210,10 @@
     // It is possible to have a label name that is not included in the
     // 'labels' object. In this case, treat it like anything else.
     if (!labels[labelName]) { return; }
-    const label = {
+    return {
       name: labelName,
       values: this._computeLabelValues(labels[labelName].values),
     };
-    return label;
   }
 
   _computeLabelValues(values) {
@@ -299,7 +297,7 @@
     }
 
     // Wait for new rule to get value populated via gr-rule-editor, and then
-    // add to permission values as well, so that the change gets propogated
+    // add to permission values as well, so that the change gets propagated
     // back to the section. Since the rule is inside a dom-repeat, a flush
     // is needed.
     flush();
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
index 318c2c3..d9d37f6 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -27,7 +25,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-plugin-config-array-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrPluginConfigArrayEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index 154af6e..868a7cd 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-list-view/gr-list-view.js';
@@ -29,7 +27,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrPluginList extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 9aa81f8..f6a1c10 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-menu-page-styles.js';
 import '../../../styles/gr-subpage-styles.js';
 import '../../../styles/shared-styles.js';
@@ -83,7 +81,7 @@
 Defs.projectAccessInput;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoAccess extends mixinBehaviors( [
   AccessBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
index 5f0739a..b46c7c0 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
@@ -62,7 +62,10 @@
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
+      <h3
+        id="inheritsFrom"
+        class$="heading-3 [[_computeShowInherit(_inheritsFrom)]]"
+      >
         <span class="rightsText">Rights Inherit From</span>
         <a
           href$="[[_computeParentHref(_inheritsFrom.name)]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
deleted file mode 100644
index 53b4989..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ /dev/null
@@ -1,54 +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 '../../../scripts/bundled-polymer.js';
-
-import '../../../styles/shared-styles.js';
-import '../../shared/gr-button/gr-button.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-repo-command_html.js';
-
-/** @extends Polymer.Element */
-class GrRepoCommand extends GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement)) {
-  static get template() { return htmlTemplate; }
-
-  static get is() { return 'gr-repo-command'; }
-
-  static get properties() {
-    return {
-      title: String,
-      disabled: Boolean,
-      tooltip: String,
-    };
-  }
-
-  /**
-   * Fired when command button is tapped.
-   *
-   * @event command-tap
-   */
-
-  _onCommandTap() {
-    this.dispatchEvent(
-        new CustomEvent('command-tap', {bubbles: true, composed: true}));
-  }
-}
-
-customElements.define(GrRepoCommand.is, GrRepoCommand);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
deleted file mode 100644
index a73f071..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-command</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-repo-command></gr-repo-command>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-repo-command.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-suite('gr-repo-command tests', () => {
-  let element;
-
-  setup(() => {
-    element = fixture('basic');
-  });
-
-  test('dispatched command-tap on button tap', done => {
-    element.addEventListener('command-tap', () => {
-      done();
-    });
-    MockInteractions.tap(
-        dom(element.root).querySelector('gr-button'));
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 1f1dc5b..4ab1b98 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-subpage-styles.js';
@@ -26,7 +24,6 @@
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-create-change-dialog/gr-create-change-dialog.js';
-import '../gr-repo-command/gr-repo-command.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -43,7 +40,7 @@
 const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoCommands extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -62,6 +59,10 @@
       /** @type {?} */
       _repoConfig: Object,
       _canCreate: Boolean,
+      // states
+      _creatingChange: Boolean,
+      _editingConfig: Boolean,
+      _runningGC: Boolean,
     };
   }
 
@@ -104,13 +105,17 @@
   }
 
   _handleRunningGC() {
+    this._runningGC = true;
     return this.$.restAPI.runRepoGC(this.repo).then(response => {
       if (response.status === 200) {
         this.dispatchEvent(new CustomEvent(
             'show-alert',
             {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
       }
-    });
+    })
+        .finally(() => {
+          this._runningGC = false;
+        });
   }
 
   _createNewChange() {
@@ -118,7 +123,11 @@
   }
 
   _handleCreateChange() {
-    this.$.createNewChangeModal.handleCreateChange();
+    this._creatingChange = true;
+    this.$.createNewChangeModal.handleCreateChange()
+        .finally(() => {
+          this._creatingChange = false;
+        });
     this._handleCloseCreateChange();
   }
 
@@ -127,6 +136,7 @@
   }
 
   _handleEditRepoConfig() {
+    this._editingConfig = true;
     return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
         EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
       const message = change ?
@@ -138,7 +148,10 @@
 
       GerritNav.navigateToRelativeUrl(GerritNav.getEditUrlForDiff(
           change, CONFIG_PATH, INITIAL_PATCHSET));
-    });
+    })
+        .finally(() => {
+          this._editingConfig = false;
+        });
   }
 }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
index b27c36b..3ae5b29 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
@@ -24,34 +24,41 @@
     /* 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 */
+    #form gr-button {
+      margin-bottom: var(--spacing-xxl);
+    }
   </style>
   <main class="gr-form-styles read-only">
-    <h1 id="Title">Repository Commands</h1>
+    <h1 id="Title" class="heading-1">Repository Commands</h1>
     <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
       Loading...
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h2 id="options">Command</h2>
+      <h2 id="options" class="heading-2">Command</h2>
       <div id="form">
-        <gr-repo-command
-          title="Create change"
-          on-command-tap="_createNewChange"
-        >
-        </gr-repo-command>
-        <gr-repo-command
+        <h3>Create change</h3>
+        <gr-button loading="[[_creatingChange]]" on-click="_createNewChange">
+          Create change
+        </gr-button>
+        <h3>Edit repo config</h3>
+        <gr-button
           id="editRepoConfig"
-          title="Edit repo config"
-          on-command-tap="_handleEditRepoConfig"
+          loading="[[_editingConfig]]"
+          on-click="_handleEditRepoConfig"
         >
-        </gr-repo-command>
-        <gr-repo-command
-          title="[[_repoConfig.actions.gc.label]]"
-          tooltip="[[_repoConfig.actions.gc.title]]"
-          hidden$="[[!_repoConfig.actions.gc.enabled]]"
-          on-command-tap="_handleRunningGC"
+          Edit repo config
+        </gr-button>
+        <h3 hidden="[[!_repoConfig.actions.gc.enabled]]">
+          [[_repoConfig.actions.gc.label]]
+        </h3>
+        <gr-button
+          hidden="[[!_repoConfig.actions.gc.enabled]]"
+          title="[[_repoConfig.actions.gc.title]]"
+          loading="[[_runningGC]]"
+          on-click="_handleRunningGC"
         >
-        </gr-repo-command>
+          [[_repoConfig.actions.gc.label]]
+        </gr-button>
         <gr-endpoint-decorator name="repo-command">
           <gr-endpoint-param name="config" value="[[_repoConfig]]">
           </gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
index db2bfcf..a52ab92 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -99,8 +99,8 @@
     test('successful creation of change', () => {
       const change = {_number: '1'};
       createChangeStub.returns(Promise.resolve(change));
-      MockInteractions.tap(element.$.editRepoConfig.shadowRoot
-          .querySelector('gr-button'));
+      MockInteractions.tap(element.$.editRepoConfig);
+      assert.isTrue(element.$.editRepoConfig.loading);
       return handleSpy.lastCall.returnValue.then(() => {
         flushAsynchronousOperations();
 
@@ -110,13 +110,14 @@
         assert.isTrue(urlStub.called);
         assert.deepEqual(urlStub.lastCall.args,
             [change, 'project.config', 1]);
+        assert.isFalse(element.$.editRepoConfig.loading);
       });
     });
 
     test('unsuccessful creation of change', () => {
       createChangeStub.returns(Promise.resolve(null));
-      MockInteractions.tap(element.$.editRepoConfig.shadowRoot
-          .querySelector('gr-button'));
+      MockInteractions.tap(element.$.editRepoConfig);
+      assert.isTrue(element.$.editRepoConfig.loading);
       return handleSpy.lastCall.returnValue.then(() => {
         flushAsynchronousOperations();
 
@@ -124,6 +125,7 @@
         assert.equal(alertStub.lastCall.args[0].detail.message,
             'Failed to create change.');
         assert.isFalse(urlStub.called);
+        assert.isFalse(element.$.editRepoConfig.loading);
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index 072fc721..f47ff76 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -26,7 +25,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoDashboards extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index e8b3d9a..e3a9958 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -16,7 +16,6 @@
  */
 
 import '@polymer/iron-input/iron-input.js';
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
@@ -47,7 +46,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoDetailList extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index 59abb72..a8119e6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -33,7 +31,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoList extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
index d01f9ab..a70aa11 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -34,7 +32,7 @@
 import {RepoPluginConfig} from '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoPluginConfig extends mixinBehaviors( [
   RepoPluginConfig,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 05ae73d..f272708 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -68,7 +66,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepo extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -355,8 +353,8 @@
       commands.push({
         title,
         command: commandObj[title]
-            .replace(/\$\{project\}/gi, encodeURI(repo))
-            .replace(/\$\{project-base-name\}/gi,
+            .replace(/\${project}/gi, encodeURI(repo))
+            .replace(/\${project-base-name}/gi,
                 encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
       });
     }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
index de36e73..ea8a8b9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
@@ -50,7 +50,7 @@
       /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
     </style>
     <div class="info">
-      <h1 id="Title" class$="name">
+      <h1 id="Title" class="heading-1">
         [[repo]]
         <hr />
       </h1>
@@ -63,7 +63,7 @@
     </div>
     <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
       <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
-        <h2 id="download">Download</h2>
+        <h2 id="download" class="heading-2">Download</h2>
         <fieldset>
           <gr-download-commands
             id="downloadCommands"
@@ -73,12 +73,15 @@
           ></gr-download-commands>
         </fieldset>
       </div>
-      <h2 id="configurations" class$="[[_computeHeaderClass(_configChanged)]]">
+      <h2
+        id="configurations"
+        class$="heading-2 [[_computeHeaderClass(_configChanged)]]"
+      >
         Configurations
       </h2>
       <div id="form">
         <fieldset>
-          <h3 id="Description">Description</h3>
+          <h3 id="Description" class="heading-3">Description</h3>
           <fieldset>
             <iron-autogrow-textarea
               id="descriptionInput"
@@ -89,7 +92,7 @@
               disabled$="[[_readOnly]]"
             ></iron-autogrow-textarea>
           </fieldset>
-          <h3 id="Options">Repository Options</h3>
+          <h3 id="Options" class="heading-3">Repository Options</h3>
           <fieldset id="options">
             <section>
               <span class="title">State</span>
@@ -362,7 +365,7 @@
               </span>
             </section>
           </fieldset>
-          <h3 id="Options">Contributor Agreements</h3>
+          <h3 id="Options" class="heading-3">Contributor Agreements</h3>
           <fieldset id="agreements">
             <section>
               <span class="title">
@@ -407,7 +410,7 @@
             class$="pluginConfig [[_computeHideClass(_pluginData)]]"
             on-plugin-config-changed="_handlePluginConfigChanged"
           >
-            <h3>Plugins</h3>
+            <h3 class="heading-3">Plugins</h3>
             <template is="dom-repeat" items="[[_pluginData]]" as="data">
               <gr-repo-plugin-config
                 plugin-data="[[data]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index 234015a..99957ff 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -79,7 +77,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRuleEditor extends mixinBehaviors( [
   AccessBehavior,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index da13492..a3fe990 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-change-list-styles.js';
 import '../../shared/gr-account-link/gr-account-link.js';
 import '../../shared/gr-change-star/gr-change-star.js';
@@ -50,7 +49,7 @@
 
 /**
  * @appliesMixin RESTClientMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeListItem extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
index 13b3c24..5ff7dde 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
@@ -90,10 +90,10 @@
       line-height: var(--line-height-mono);
     }
     .u-green {
-      color: var(--vote-text-color-recommended);
+      color: var(--positive-green-text-color);
     }
     .u-red {
-      color: var(--vote-text-color-disliked);
+      color: var(--negative-red-text-color);
     }
     .u-gray-background {
       background-color: var(--table-header-background-color);
@@ -117,7 +117,7 @@
   <style include="gr-change-list-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
-  <td class="cell leftPadding"></td>
+  <td aria-hidden="true" class="cell leftPadding"></td>
   <td class="cell star" hidden$="[[!showStar]]" hidden="">
     <gr-change-star change="{{change}}"></gr-change-star>
   </td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index c416b11..361ad83 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-icons/gr-icons.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-change-list/gr-change-list.js';
@@ -45,7 +44,7 @@
 const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeListView extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
index 4add1da..b322f38 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
@@ -86,14 +86,15 @@
         href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
         class$="[[_computePrevArrowClass(_offset)]]"
       >
-        <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+        <iron-icon icon="gr-icons:chevron-left" aria-label="Older"> </iron-icon>
       </a>
       <a
         id="nextArrow"
         href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
         class$="[[_computeNextArrowClass(_changes)]]"
       >
-        <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+        <iron-icon icon="gr-icons:chevron-right" aria-label="Newer">
+        </iron-icon>
       </a>
     </nav>
   </div>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 0a19ae1..bd78ae9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-change-list-styles.js';
 import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -45,7 +44,7 @@
 const MAX_SHORTCUT_CHARS = 5;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeList extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
index 17150df..f7c50e6 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
@@ -52,8 +52,13 @@
       <template is="dom-if" if="[[changeSection.name]]">
         <tbody>
           <tr class="groupHeader">
-            <td class="leftPadding"></td>
-            <td class="star" hidden$="[[!showStar]]" hidden=""></td>
+            <td aria-hidden="true" class="leftPadding"></td>
+            <td
+              aria-hidden="true"
+              class="star"
+              hidden$="[[!showStar]]"
+              hidden=""
+            ></td>
             <td
               class="cell"
               colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"
@@ -74,8 +79,8 @@
       <tbody class="groupContent">
         <template is="dom-if" if="[[_isEmpty(changeSection)]]">
           <tr class="noChanges">
-            <td class="leftPadding"></td>
-            <td class="star" hidden$="[[!showStar]]" hidden=""></td>
+            <td aria-hidden="true" class="leftPadding"></td>
+            <td aria-hidden="true" class="star" hidden></td>
             <td
               class="cell"
               colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"
@@ -91,8 +96,13 @@
         </template>
         <template is="dom-if" if="[[!_isEmpty(changeSection)]]">
           <tr class="groupTitle">
-            <td class="leftPadding"></td>
-            <td class="star" hidden$="[[!showStar]]" hidden=""></td>
+            <td aria-hidden="true" class="leftPadding"></td>
+            <td
+              aria-label="Star status column"
+              class="star"
+              hidden$="[[!showStar]]"
+              hidden=""
+            ></td>
             <td class="number" hidden$="[[!showNumber]]" hidden="">#</td>
             <template is="dom-repeat" items="[[changeTableColumns]]" as="item">
               <td
@@ -138,7 +148,7 @@
   <gr-cursor-manager
     id="cursor"
     index="{{selectedIndex}}"
-    scroll-behavior="keep-visible"
+    scroll-mode="keep-visible"
     focus-on-move=""
   ></gr-cursor-manager>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index 3758a78..d9fd378 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -24,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-change-help_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCreateChangeHelp extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
index 4a357af..cc6223d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
@@ -48,12 +48,6 @@
       padding-top: var(--spacing-xl);
       vertical-align: top;
     }
-    #help h1 {
-      font-family: var(--header-font-family);
-      font-size: var(--font-size-h3);
-      font-weight: var(--font-weight-h3);
-      line-height: var(--line-height-h3);
-    }
     #help p {
       margin-bottom: var(--spacing-m);
       max-width: 35em;
@@ -73,7 +67,7 @@
     </p>
   </div>
   <div id="help">
-    <h1>Push your first change for code review</h1>
+    <h2 class="heading-3">Push your first change for code review</h2>
     <p>
       Pushing a change for review is easy, but a little different from other git
       code review tools. Click on the \`Create Change' button and follow the
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index 7e5e749..cc53e49 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-overlay/gr-overlay.js';
@@ -30,7 +29,7 @@
   PUSH_PREFIX: 'git push origin HEAD:refs/for/',
 };
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCreateCommandsDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index f8757ba..9062a3f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-overlay/gr-overlay.js';
@@ -29,7 +28,7 @@
  * name and the branch name.
  *
  * @event confirm
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateDestinationDialog extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 8b8b981..1cc2316 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-change-list/gr-change-list.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-overlay/gr-overlay.js';
@@ -34,11 +31,12 @@
 import {htmlTemplate} from './gr-dashboard-view_html.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
 
 const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDashboardView extends mixinBehaviors( [
   RESTClientBehavior,
@@ -98,6 +96,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   static get observers() {
     return [
       '_paramsChanged(params.*)',
@@ -197,7 +200,7 @@
         })
         .then(() => {
           this._maybeShowDraftsBanner();
-          this.$.reporting.dashboardDisplayed();
+          this.reporting.dashboardDisplayed();
         })
         .catch(err => {
           this.dispatchEvent(new CustomEvent('title-change', {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
index 3389bd0..cf1036f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
@@ -119,5 +119,4 @@
   ></gr-create-destination-dialog>
   <gr-create-commands-dialog id="commandsDialog"></gr-create-commands-dialog>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 5965d06..2fcf233 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -357,7 +357,7 @@
         });
     element.addEventListener('page-error', e => {
       assert.strictEqual(e.detail.response, response);
-      done();
+      paramsChangedPromise.then(done);
     });
     element.params = {
       view: GerritNav.View.DASHBOARD,
@@ -367,14 +367,14 @@
   });
 
   test('params change triggers dashboardDisplayed()', () => {
-    sandbox.stub(element.$.reporting, 'dashboardDisplayed');
+    sandbox.stub(element.reporting, 'dashboardDisplayed');
     element.params = {
       view: GerritNav.View.DASHBOARD,
       project: 'project',
       dashboard: 'dashboard',
     };
     return paramsChangedPromise.then(() => {
-      assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+      assert.isTrue(element.reporting.dashboardDisplayed.calledOnce);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
deleted file mode 100644
index 2523700..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 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 '../../../scripts/bundled-polymer.js';
-
-import '../gr-change-list/gr-change-list.js';
-import '../gr-create-change-help/gr-create-change-help.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-embed-dashboard_html.js';
-
-/** @extends Polymer.Element */
-class GrEmbedDashboard extends GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement)) {
-  static get template() { return htmlTemplate; }
-
-  static get is() { return 'gr-embed-dashboard'; }
-
-  static get properties() {
-    return {
-      account: Object,
-      sections: Array,
-      preferences: Object,
-      showNewUserHelp: Boolean,
-    };
-  }
-}
-
-customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
deleted file mode 100644
index 802e365..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
+++ /dev/null
@@ -1,35 +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.js';
-
-export const htmlTemplate = html`
-  <gr-change-list
-    show-star=""
-    account="[[account]]"
-    preferences="[[preferences]]"
-    sections="[[sections]]"
-  >
-    <div id="emptyOutgoing" slot="empty-outgoing">
-      <template is="dom-if" if="[[showNewUserHelp]]">
-        <gr-create-change-help></gr-create-change-help>
-      </template>
-      <template is="dom-if" if="[[!showNewUserHelp]]">
-        No changes
-      </template>
-    </div>
-  </gr-change-list>
-`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index 5f0021e..303c612 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/dashboard-header-styles.js';
 import '../../../styles/shared-styles.js';
@@ -26,7 +25,7 @@
 import {htmlTemplate} from './gr-repo-header_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrRepoHeader extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
index f6fb1d0..75af51e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
@@ -24,7 +24,7 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <div class="info">
-    <h1 class$="name">
+    <h1 class="heading-1">
       [[repo]]
       <hr />
     </h1>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index 6bb1bf8..7901c53 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -30,7 +29,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrUserHeader extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
index 5a5d590..8207284 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
@@ -21,12 +21,6 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <style include="dashboard-header-styles">
-    .name {
-      display: inline-block;
-    }
-    .name hr {
-      width: 100%;
-    }
     .status.hide,
     .name.hide,
     .dashboardLink.hide {
@@ -39,7 +33,7 @@
     aria-label="Account avatar"
   ></gr-avatar>
   <div class="info">
-    <h1 class="name">
+    <h1 class="heading-1">
       [[_computeDetail(_accountDetails, 'name')]]
     </h1>
     <hr />
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index ca9016f..8dc8058 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../admin/gr-create-change-dialog/gr-create-change-dialog.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
@@ -44,6 +41,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {appContext} from '../../../services/app-context.js';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -89,13 +87,15 @@
   PRIVATE: 'private',
   PRIVATE_DELETE: 'private.delete',
   PUBLISH_EDIT: 'publishEdit',
-  READY: 'ready',
+  REBASE: 'rebase',
   REBASE_EDIT: 'rebaseEdit',
+  READY: 'ready',
   RESTORE: 'restore',
   REVERT: 'revert',
   REVERT_SUBMISSION: 'revert_submission',
   REVIEWED: 'reviewed',
   STOP_EDIT: 'stopEdit',
+  SUBMIT: 'submit',
   UNIGNORE: 'unignore',
   UNREVIEWED: 'unreviewed',
   WIP: 'wip',
@@ -234,7 +234,7 @@
 const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeActions extends mixinBehaviors( [
   PatchSetBehavior,
@@ -274,6 +274,7 @@
     this.ActionType = ActionType;
     this.ChangeActions = ChangeActions;
     this.RevisionActions = RevisionActions;
+    this.reporting = appContext.reportingService;
   }
 
   static get properties() {
@@ -1001,7 +1002,7 @@
     this._handleAction(type, key);
   }
 
-  _handleOveflowItemTap(e) {
+  _handleOverflowItemTap(e) {
     e.preventDefault();
     const el = dom(e).localTarget;
     const key = e.detail.action.__key;
@@ -1017,7 +1018,7 @@
   }
 
   _handleAction(type, key) {
-    this.$.reporting.reportInteraction(`${type}-${key}`);
+    this.reporting.reportInteraction(`${type}-${key}`);
     switch (type) {
       case ActionType.REVISION:
         this._handleRevisionAction(key);
@@ -1357,6 +1358,8 @@
         case ChangeActions.DELETE_EDIT:
         case ChangeActions.PUBLISH_EDIT:
         case ChangeActions.REBASE_EDIT:
+        case ChangeActions.REBASE:
+        case ChangeActions.SUBMIT:
           GerritNav.navigateToChange(this.change);
           break;
         case ChangeActions.REVERT_SUBMISSION:
@@ -1543,6 +1546,12 @@
           if (ACTIONS_WITH_ICONS.has(action.__key)) {
             action.icon = action.__key;
           }
+          // TODO(brohlfs): Temporary hack until change 269573 is live in all
+          // backends.
+          if (action.__key === ChangeActions.READY) {
+            action.label = 'Mark as Active';
+          }
+          // End of hack
           return action;
         })
         .filter(action => !this._shouldSkipAction(action));
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
index f12e600..200ffc9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
@@ -137,15 +137,15 @@
     <gr-dropdown
       id="moreActions"
       link=""
-      tabindex="0"
       vertical-offset="32"
       horizontal-align="right"
-      on-tap-item="_handleOveflowItemTap"
+      on-tap-item="_handleOverflowItemTap"
       hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
       disabled-ids="[[_disabledMenuActions]]"
       items="[[_menuActions]]"
     >
-      <iron-icon icon="gr-icons:more-vert"></iron-icon>
+      <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
+      </iron-icon>
       <span id="moreMessage">More</span>
     </gr-dropdown>
   </div>
@@ -271,5 +271,4 @@
   </gr-overlay>
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting" category="change-actions"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index acf17ea..cda53c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -415,6 +415,17 @@
       });
     });
 
+    test('rebase change calls navigateToChange', done => {
+      const navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange');
+      sandbox.stub(element.$.restAPI, 'getResponseObject').returns(
+          Promise.resolve({}));
+      element._handleResponse({__key: 'rebase'}, {});
+      flush(() => {
+        assert.isTrue(navigateToChangeStub.called);
+        done();
+      });
+    });
+
     test(`rebase dialog gets recent changes each time it's opened`, done => {
       const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
           'fetchRecentChanges').returns(Promise.resolve([]));
@@ -1966,7 +1977,7 @@
 
     test('_handleAction reports', () => {
       sandbox.stub(element, '_fireAction');
-      const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+      const reportStub = sandbox.stub(element.reporting, 'reportInteraction');
       element._handleAction('type', 'key');
       assert.isTrue(reportStub.called);
       assert.equal(reportStub.lastCall.args[0], 'type-key');
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index fda13cd..9d3b455 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-change-metadata-shared-styles.js';
 import '../../../styles/gr-change-view-integration-shared-styles.js';
@@ -45,6 +43,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
@@ -78,7 +77,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeMetadata extends mixinBehaviors( [
   RESTClientBehavior,
@@ -323,7 +322,7 @@
   }
 
   _computeShowRequirements(change) {
-    if (change.status !== this.ChangeStatus.NEW) {
+    if (change.status !== ChangeStatus.NEW) {
       // TODO(maximeg) change this to display the stored
       // requirements, once it is implemented server-side.
       return false;
@@ -399,7 +398,7 @@
   _computeBranchUrl(project, branch) {
     if (!this.change || !this.change.status) return '';
     return GerritNav.getUrlForBranch(branch, project,
-        this.change.status == this.ChangeStatus.NEW ? 'open' :
+        this.change.status == ChangeStatus.NEW ? 'open' :
           this.change.status.toLowerCase());
   }
 
@@ -462,7 +461,7 @@
    *
    * @param {!Object} change
    * @param {string} role One of the values from _CHANGE_ROLE
-   * @return {Object|null} either an accound or null.
+   * @return {Object|null} either an account or null.
    */
   _getNonOwnerRole(change, role) {
     if (!change || !change.current_revision ||
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
index 1b18412..ca176be 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
@@ -74,10 +74,10 @@
       color: #ffa62f;
     }
     .icon.invalid {
-      color: var(--vote-text-color-disliked);
+      color: var(--negative-red-text-color);
     }
     .icon.trusted {
-      color: var(--vote-text-color-recommended);
+      color: var(--positive-green-text-color);
     }
     .parentList.notCurrent.nonMerge #parentNotCurrentMessage {
       --arrow-color: #ffa62f;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index d301813..d2d6037 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-icons/gr-icons.js';
@@ -30,7 +28,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeRequirements extends mixinBehaviors( [
   RESTClientBehavior,
@@ -106,7 +104,7 @@
   }
 
   _computeRequirementIcon(requirementStatus) {
-    return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
+    return requirementStatus ? 'gr-icons:check' : 'gr-icons:schedule';
   }
 
   _computeLabels(labelsRecord) {
@@ -134,7 +132,7 @@
   _computeLabelIcon(labelInfo) {
     if (labelInfo.approved) { return 'gr-icons:check'; }
     if (labelInfo.rejected) { return 'gr-icons:close'; }
-    return 'gr-icons:hourglass';
+    return 'gr-icons:schedule';
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
index 0da31de..657915b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
@@ -31,10 +31,10 @@
       line-height: var(--line-height-mono);
     }
     .approved.status {
-      color: var(--vote-text-color-recommended);
+      color: var(--positive-green-text-color);
     }
     .rejected.status {
-      color: var(--vote-text-color-disliked);
+      color: var(--negative-red-text-color);
     }
     iron-icon {
       color: inherit;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index e100f91..276e821 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -51,13 +51,13 @@
 
     assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
     assert.equal(element._computeRequirementIcon(false),
-        'gr-icons:hourglass');
+        'gr-icons:schedule');
   });
 
   test('label computed fields', () => {
     assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
     assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
-    assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass');
+    assert.equal(element._computeLabelIcon({}), 'gr-icons:schedule');
 
     assert.equal(element._computeLabelClass({approved: []}), 'approved');
     assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
@@ -90,7 +90,7 @@
     assert.equal(element._requiredLabels.length, 1);
 
     assert.equal(element._optionalLabels[0].label, 'opt_test');
-    assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
+    assert.equal(element._optionalLabels[0].icon, 'gr-icons:schedule');
     assert.equal(element._optionalLabels[0].style, '');
     assert.ok(element._optionalLabels[0].labelInfo);
   });
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index b48c474..799adde 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-tabs/paper-tabs.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
@@ -60,15 +57,17 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GrEditConstants} from '../../edit/gr-edit-constants.js';
 import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
 
-import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
+import {PrimaryTab, SecondaryTab} from '../../../constants/constants.js';
 import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
 import {appContext} from '../../../services/app-context.js';
+import {ExperimentIds} from '../../../services/flags.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -131,7 +130,7 @@
 /**
  * @appliesMixin RESTClientMixin
  * @appliesMixin PatchSetMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeView extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -229,7 +228,6 @@
         type: Boolean,
         computed: '_computeCanStartReview(_change)',
       },
-      _comments: Object,
       /** @type {?} */
       _change: {
         type: Object,
@@ -272,8 +270,8 @@
       _constants: {
         type: Object,
         value: {
-          SecondaryTabs,
-          PrimaryTabs,
+          SecondaryTab,
+          PrimaryTab,
         },
       },
       _messages: {
@@ -407,7 +405,7 @@
        */
       _activeTabs: {
         type: Array,
-        value: [PrimaryTabs.FILES, SecondaryTabs.CHANGE_LOG],
+        value: [PrimaryTab.FILES, SecondaryTab.CHANGE_LOG],
       },
       _showAllRobotComments: {
         type: Boolean,
@@ -449,6 +447,7 @@
   constructor() {
     super();
     this.flagsService = appContext.flagsService;
+    this.reporting = appContext.reportingService;
   }
 
   /** @override */
@@ -539,7 +538,7 @@
   }
 
   _isChangeLogExperimentEnabled() {
-    return this.flagsService.isEnabled('UiFeature__cleaner_changelog');
+    return this.flagsService.isEnabled(ExperimentIds.CLEANER_CHANGELOG);
   }
 
   get messagesList() {
@@ -626,7 +625,7 @@
     }
     if (paperTabs.selected !== activeIndex) {
       paperTabs.selected = activeIndex;
-      this.$.reporting.reportInteraction('show-tab', {tabName});
+      this.reporting.reportInteraction('show-tab', {tabName});
     }
     return tabName;
   }
@@ -741,7 +740,7 @@
   _computeHideEditCommitMessage(
       loggedIn, editing, change, editMode, collapsed, collapsible) {
     if (!loggedIn || editing ||
-        (change && change.status === this.ChangeStatus.MERGED) ||
+        (change && change.status === ChangeStatus.MERGED) ||
         editMode ||
         (collapsed && collapsible)) {
       return true;
@@ -820,8 +819,7 @@
     // Get any new drafts that have been saved in the diff view and show
     // in the comment thread view.
     this._reloadDrafts().then(() => {
-      this._commentThreads = this._changeComments.getAllThreadsForChange()
-          .map(c => Object.assign({}, c));
+      this._commentThreads = this._changeComments.getAllThreadsForChange();
       flush();
     });
   }
@@ -978,7 +976,7 @@
   _handleReplySent(e) {
     this.addEventListener('change-details-loaded',
         () => {
-          this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
+          this.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
         }, {once: true});
     this.$.replyOverlay.close();
     this._reload();
@@ -1035,10 +1033,7 @@
         (value.patchNum !== undefined && value.basePatchNum !== undefined) &&
         (this._patchRange.patchNum !== value.patchNum ||
         this._patchRange.basePatchNum !== value.basePatchNum);
-
-    if (this._changeNum !== value.changeNum) {
-      this._initialLoadComplete = false;
-    }
+    const changeChanged = this._changeNum !== value.changeNum;
 
     const patchRange = {
       patchNum: value.patchNum,
@@ -1050,7 +1045,7 @@
 
     // If the change has already been loaded and the parameter change is only
     // in the patch range, then don't do a full reload.
-    if (this._initialLoadComplete && patchChanged) {
+    if (!changeChanged && patchChanged) {
       if (patchRange.patchNum == null) {
         patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
       }
@@ -1060,6 +1055,7 @@
       return;
     }
 
+    this._initialLoadComplete = false;
     this._changeNum = value.changeNum;
     this.$.relatedChanges.clear();
 
@@ -1073,7 +1069,7 @@
   }
 
   _initActiveTabs(params = {}) {
-    let primaryTab = PrimaryTabs.FILES;
+    let primaryTab = PrimaryTab.FILES;
     if (params.queryMap && params.queryMap.has('tab')) {
       primaryTab = params.queryMap.get('tab');
     }
@@ -1082,16 +1078,9 @@
         tab: primaryTab,
       },
     });
-
-    // TODO: should drop this once we move CommentThreads tab
-    // to primary as well
-    let secondaryTab = SecondaryTabs.CHANGE_LOG;
-    if (params.queryMap && params.queryMap.has('secondaryTab')) {
-      secondaryTab = params.queryMap.get('secondaryTab');
-    }
     this._setActiveSecondaryTab({
       detail: {
-        tab: secondaryTab,
+        tab: SecondaryTab.CHANGE_LOG,
       },
     });
   }
@@ -1182,7 +1171,7 @@
         .then(this._getLoggedIn.bind(this))
         .then(loggedIn => {
           if (!loggedIn || !this._change ||
-              this._change.status !== this.ChangeStatus.MERGED) {
+              this._change.status !== ChangeStatus.MERGED) {
           // Do not display dialog if not logged-in or the change is not
           // merged.
             return;
@@ -1509,15 +1498,8 @@
     });
   }
 
-  _handleReloadChange(e) {
-    return this._reload().then(() => {
-      // If the change was rebased or submitted, we need to reload the page
-      // with the latest patch.
-      const action = e.detail.action;
-      if (action === 'rebase' || action === 'submit') {
-        GerritNav.navigateToChange(this._change);
-      }
-    });
+  _handleReloadChange() {
+    return this._reload();
   }
 
   _handleGetChangeDetailError(response) {
@@ -1706,6 +1688,13 @@
    * (comments, robot comments, draft comments) is requested.
    */
   _reloadComments() {
+    // We are resetting all comment related properties, because we want to avoid
+    // a new change being loaded and then paired with outdated comments.
+    this._changeComments = undefined;
+    this._commentThreads = undefined;
+    this._diffDrafts = undefined;
+    this._draftCommentThreads = undefined;
+    this._robotCommentThreads = undefined;
     return this.$.commentAPI.loadAll(this._changeNum)
         .then(comments => this._recomputeComments(comments));
   }
@@ -1725,11 +1714,18 @@
 
   _recomputeComments(comments) {
     this._changeComments = comments;
-    this._diffDrafts = Object.assign({}, this._changeComments.drafts);
-    this._commentThreads = this._changeComments.getAllThreadsForChange()
-        .map(c => Object.assign({}, c));
+    this._diffDrafts = {...this._changeComments.drafts};
+    this._commentThreads = this._changeComments.getAllThreadsForChange();
     this._draftCommentThreads = this._commentThreads
-        .filter(c => c.comments[c.comments.length - 1].__draft);
+        .filter(thread => thread.comments[thread.comments.length - 1].__draft)
+        .map(thread => {
+          const copiedThread = {...thread};
+          // Make a hardcopy of all comments and collapse all but last one
+          const commentsInThread = copiedThread.comments = thread.comments
+              .map(comment => { return {...comment, collapsed: true}; });
+          commentsInThread[commentsInThread.length - 1].collapsed = false;
+          return copiedThread;
+        });
   }
 
   /**
@@ -1744,8 +1740,8 @@
   _reload(opt_isLocationChange) {
     this._loading = true;
     this._relatedChangesCollapsed = true;
-    this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
-    this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
+    this.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
+    this.reporting.time(CHANGE_DATA_TIMING_LABEL);
 
     // Array to house all promises related to data requests.
     const allDataPromises = [];
@@ -1764,9 +1760,9 @@
               {bubbles: true, composed: true}));
         })
         .then(() => {
-          this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
+          this.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
           if (opt_isLocationChange) {
-            this.$.reporting.changeDisplayed();
+            this.reporting.changeDisplayed();
           }
         });
 
@@ -1836,9 +1832,9 @@
     }
 
     Promise.all(allDataPromises).then(() => {
-      this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
+      this.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
       if (opt_isLocationChange) {
-        this.$.reporting.changeFullyLoaded();
+        this.reporting.changeFullyLoaded();
       }
     });
 
@@ -1864,8 +1860,8 @@
     // If the change is closed, it is not mergeable. Note: already merged
     // changes are obviously not mergeable, but the mergeability API will not
     // answer for abandoned changes.
-    if (this._change.status === this.ChangeStatus.MERGED ||
-        this._change.status === this.ChangeStatus.ABANDONED) {
+    if (this._change.status === ChangeStatus.MERGED ||
+        this._change.status === ChangeStatus.ABANDONED) {
       this._mergeable = false;
       return Promise.resolve();
     }
@@ -2007,7 +2003,7 @@
   _computeShowRelatedToggle() {
     // Make sure the max height has been applied, since there is now content
     // to populate.
-    if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
+    if (!getComputedStyleValue('--relation-chain-max-height', this)) {
       this._updateRelatedChangeMaxHeight();
     }
     // Prevents showMore from showing when click on related change, since the
@@ -2045,11 +2041,11 @@
         let toastMessage = null;
         if (!result.isLatest) {
           toastMessage = ReloadToastMessage.NEWER_REVISION;
-        } else if (result.newStatus === this.ChangeStatus.MERGED) {
+        } else if (result.newStatus === ChangeStatus.MERGED) {
           toastMessage = ReloadToastMessage.MERGED;
-        } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+        } else if (result.newStatus === ChangeStatus.ABANDONED) {
           toastMessage = ReloadToastMessage.ABANDONED;
-        } else if (result.newStatus === this.ChangeStatus.NEW) {
+        } else if (result.newStatus === ChangeStatus.NEW) {
           toastMessage = ReloadToastMessage.RESTORED;
         } else if (result.newMessages) {
           toastMessage = ReloadToastMessage.NEW_MESSAGE;
@@ -2116,7 +2112,8 @@
 
   _handleFileActionTap(e) {
     e.preventDefault();
-    const controls = this.$.fileListHeader.$.editControls;
+    const controls = this.$.fileListHeader
+        .shadowRoot.querySelector('#editControls');
     const path = e.detail.path;
     switch (e.detail.action) {
       case GrEditConstants.Actions.DELETE.id:
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
index a1d4c52..f5a0463 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -539,7 +539,18 @@
     </section>
 
     <paper-tabs id="primaryTabs" on-selected-changed="_setActivePrimaryTab">
-      <paper-tab data-name$="[[_constants.PrimaryTabs.FILES]]">Files</paper-tab>
+      <paper-tab data-name$="[[_constants.PrimaryTab.FILES]]">Files</paper-tab>
+      <paper-tab
+        data-name$="[[_constants.PrimaryTab.COMMENT_THREADS]]"
+        class="commentThreads"
+      >
+        <gr-tooltip-content
+          has-tooltip=""
+          title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
+        >
+          <span>Comments</span></gr-tooltip-content
+        >
+      </paper-tab>
       <template
         is="dom-repeat"
         items="[[_dynamicTabHeaderEndpoints]]"
@@ -554,14 +565,14 @@
           </gr-endpoint-decorator>
         </paper-tab>
       </template>
-      <paper-tab data-name$="[[_constants.PrimaryTabs.FINDINGS]]">
+      <paper-tab data-name$="[[_constants.PrimaryTab.FINDINGS]]">
         Findings
       </paper-tab>
     </paper-tabs>
 
     <section class="patchInfo">
       <div
-        hidden$="[[!_isTabActive(_constants.PrimaryTabs.FILES, _activeTabs)]]"
+        hidden$="[[!_isTabActive(_constants.PrimaryTab.FILES, _activeTabs)]]"
       >
         <gr-file-list-header
           id="fileListHeader"
@@ -614,10 +625,22 @@
         >
         </gr-file-list>
       </div>
-
       <template
         is="dom-if"
-        if="[[_isTabActive(_constants.PrimaryTabs.FINDINGS, _activeTabs)]]"
+        if="[[_isTabActive(_constants.PrimaryTab.COMMENT_THREADS, _activeTabs)]]"
+      >
+        <gr-thread-list
+          threads="[[_commentThreads]]"
+          change="[[_change]]"
+          change-num="[[_changeNum]]"
+          logged-in="[[_loggedIn]]"
+          only-show-robot-comments-with-human-reply=""
+          on-thread-list-modified="_handleReloadDiffComments"
+        ></gr-thread-list>
+      </template>
+      <template
+        is="dom-if"
+        if="[[_isTabActive(_constants.PrimaryTab.FINDINGS, _activeTabs)]]"
       >
         <gr-dropdown-list
           class="patch-set-dropdown"
@@ -667,27 +690,16 @@
 
     <paper-tabs id="secondaryTabs" on-selected-changed="_setActiveSecondaryTab">
       <paper-tab
-        data-name$="[[_constants.SecondaryTabs.CHANGE_LOG]]"
+        data-name$="[[_constants.SecondaryTab.CHANGE_LOG]]"
         class="changeLog"
       >
         Change Log
       </paper-tab>
-      <paper-tab
-        data-name$="[[_constants.SecondaryTabs.COMMENT_THREADS]]"
-        class="commentThreads"
-      >
-        <gr-tooltip-content
-          has-tooltip=""
-          title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
-        >
-          <span>Comment Threads</span></gr-tooltip-content
-        >
-      </paper-tab>
     </paper-tabs>
     <section class="changeLog">
       <template
         is="dom-if"
-        if="[[_isTabActive(_constants.SecondaryTabs.CHANGE_LOG, _activeTabs)]]"
+        if="[[_isTabActive(_constants.SecondaryTab.CHANGE_LOG, _activeTabs)]]"
       >
         <template is="dom-if" if="[[!_isChangeLogExperimentEnabled()]]">
           <gr-messages-list
@@ -706,6 +718,7 @@
         <template is="dom-if" if="[[_isChangeLogExperimentEnabled()]]">
           <gr-messages-list-experimental
             class="hideOnMobileOverlay"
+            change="[[_change]]"
             change-num="[[_changeNum]]"
             labels="[[_change.labels]]"
             messages="[[_change.messages]]"
@@ -718,19 +731,6 @@
           ></gr-messages-list-experimental>
         </template>
       </template>
-      <template
-        is="dom-if"
-        if="[[_isTabActive(_constants.SecondaryTabs.COMMENT_THREADS, _activeTabs)]]"
-      >
-        <gr-thread-list
-          threads="[[_commentThreads]]"
-          change="[[_change]]"
-          change-num="[[_changeNum]]"
-          logged-in="[[_loggedIn]]"
-          only-show-robot-comments-with-human-reply=""
-          on-thread-list-modified="_handleReloadDiffComments"
-        ></gr-thread-list>
-      </template>
     </section>
   </div>
 
@@ -789,5 +789,4 @@
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-comment-api id="commentAPI"></gr-comment-api>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
similarity index 93%
rename from polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
rename to polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index c935a84..faa81b6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -1,59 +1,38 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-view></gr-change-view>
-  </template>
-</test-fixture>
-
-<test-fixture id="blank">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../edit/gr-edit-constants.js';
 import './gr-change-view.js';
-import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
+import {PrimaryTab, SecondaryTab, ChangeStatus} from '../../../constants/constants.js';
 
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GrEditConstants} from '../../edit/gr-edit-constants.js';
 import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 
+import 'lodash/lodash.js';
+
 const pluginApi = _testOnly_initGerritPluginApi();
+const fixture = fixtureFromElement('gr-change-view');
 
 suite('gr-change-view tests', () => {
   const kb = KeyboardShortcutBinder;
@@ -76,6 +55,7 @@
 
   const ROBOT_COMMENTS_LIMIT = 10;
 
+  // TODO: should have a mock service to generate VALID fake data
   const THREADS = [
     {
       comments: [
@@ -88,7 +68,7 @@
           },
           patch_set: 2,
           robot_id: 'rb1',
-          id: 'ecf9fa_fe1a5f62',
+          id: 'ecf0b9fa_fe1a5f62',
           line: 5,
           updated: '2018-02-08 18:49:18.000000000',
           message: 'test',
@@ -102,7 +82,7 @@
             username: 'user',
           },
           patch_set: 4,
-          id: 'ecf0b9fa_fe1a5f62',
+          id: 'ecf0b9fa_fe1a5f62_1',
           line: 5,
           updated: '2018-02-08 18:49:18.000000000',
           message: 'test',
@@ -313,7 +293,7 @@
       getDiffDrafts() { return Promise.resolve({}); },
       _fetchSharedCacheURL() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
+    element = fixture.instantiate();
     sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
     pluginLoader.loadPlugins([]);
     pluginApi.install(
@@ -340,7 +320,7 @@
   });
 
   const getCustomCssValue =
-      cssParam => util.getComputedStyleValue(cssParam, element);
+      cssParam => getComputedStyleValue(cssParam, element);
 
   test('_handleMessageAnchorTap', () => {
     element._changeNum = '1';
@@ -367,9 +347,9 @@
       assert(element._dynamicTabHeaderEndpoints.includes(
           'change-view-tab-header-url'));
       const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
-      // 3 Tabs are : Files, Plugin, Findings
-      assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
-      assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
+      // 4 Tabs are : Files, Comment Threads, Plugin, Findings
+      assert.equal(paperTabs.querySelectorAll('paper-tab').length, 4);
+      assert.equal(paperTabs.querySelectorAll('paper-tab')[2].dataset.name,
           'change-view-tab-header-url');
     });
 
@@ -391,9 +371,9 @@
     });
 
     test('param change should switch primary tab correctly', done => {
-      assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
+      assert.equal(element._activeTabs[0], PrimaryTab.FILES);
       const queryMap = new Map();
-      queryMap.set('tab', PrimaryTabs.FINDINGS);
+      queryMap.set('tab', PrimaryTab.FINDINGS);
       // view is required
       element.params = Object.assign(
           {
@@ -401,13 +381,13 @@
           },
           element.params, {queryMap});
       flush(() => {
-        assert.equal(element._activeTabs[0], PrimaryTabs.FINDINGS);
+        assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
         done();
       });
     });
 
     test('invalid param change should not switch primary tab', done => {
-      assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
+      assert.equal(element._activeTabs[0], PrimaryTab.FILES);
       const queryMap = new Map();
       queryMap.set('tab', 'random');
       // view is required
@@ -417,14 +397,14 @@
           },
           element.params, {queryMap});
       flush(() => {
-        assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
+        assert.equal(element._activeTabs[0], PrimaryTab.FILES);
         done();
       });
     });
 
     test('switching tab sets _selectedTabPluginEndpoint', done => {
       const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
-      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
+      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
       flush(() => {
         assert.equal(element._selectedTabPluginEndpoint,
             'change-view-tab-content-url');
@@ -714,6 +694,56 @@
     });
   });
 
+  suite('_recomputeComments', () => {
+    setup(() => {
+      // Fake computeDraftCount as its required for ChangeComments,
+      // see gr-comment-api#reloadDrafts.
+      sandbox.stub(element.$.commentAPI, 'reloadDrafts')
+          .returns(Promise.resolve({
+            drafts: {},
+            getAllThreadsForChange: () => THREADS,
+            computeDraftCount: () => 0,
+          }));
+    });
+
+    test('draft threads should be a new copy with correct states', done => {
+      element.$.fileList.dispatchEvent(
+          new CustomEvent('reload-drafts', {
+            detail: {
+              resolve: () => {
+                assert.equal(element._draftCommentThreads.length, 2);
+                assert.equal(
+                    element._draftCommentThreads[0].rootId,
+                    THREADS[0].rootId
+                );
+                assert.notEqual(
+                    element._draftCommentThreads[0].comments,
+                    THREADS[0].comments
+                );
+                assert.notEqual(
+                    element._draftCommentThreads[0].comments[0],
+                    THREADS[0].comments[0]
+                );
+                assert.isTrue(
+                    element._draftCommentThreads[0]
+                        .comments
+                        .slice(0, 2)
+                        .every(c => c.collapsed === true)
+                );
+
+                assert.isTrue(
+                    element._draftCommentThreads[0]
+                        .comments[2]
+                        .collapsed === false
+                );
+                done();
+              },
+            },
+            composed: true, bubbles: true,
+          }));
+    });
+  });
+
   test('diff comments modified', () => {
     sandbox.spy(element, '_handleReloadCommentThreads');
     return element._reloadComments().then(() => {
@@ -727,7 +757,7 @@
 
   test('thread list modified', () => {
     sandbox.spy(element, '_handleReloadDiffComments');
-    element._activeTabs = [PrimaryTabs.FILES, SecondaryTabs.COMMENT_THREADS];
+    element._activeTabs = [PrimaryTab.COMMENT_THREADS, SecondaryTab.CHANGE_LOG];
     flushAsynchronousOperations();
 
     return element._reloadComments().then(() => {
@@ -790,61 +820,6 @@
       sandbox.spy(element, '_paramsChanged');
       element.params = {view: 'change', changeNum: '1'};
     });
-
-    test('tab switch works correctly', done => {
-      assert.isTrue(element._paramsChanged.called);
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-
-      const commentTab = element.shadowRoot.querySelector(
-          'paper-tab.commentThreads'
-      );
-      // Switch to comment thread tab
-      MockInteractions.tap(commentTab);
-      assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
-
-      // Switch back to 'Change Log' tab
-      element._paramsChanged(element.params);
-      flush(() => {
-        assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-        done();
-      });
-    });
-
-    test('show-secondary-tab event works', () => {
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-      // Switch to comment thread tab
-      element.fire('show-secondary-tab', {tab: SecondaryTabs.COMMENT_THREADS});
-      assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
-    });
-
-    test('param change should switched secondary tab correctly', done => {
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-      const queryMap = new Map();
-      queryMap.set('secondaryTab', SecondaryTabs.COMMENT_THREADS);
-      // view is required
-      element.params = Object.assign(
-          {view: GerritNav.View.CHANGE},
-          element.params, {queryMap}
-      );
-      flush(() => {
-        assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
-        done();
-      });
-    });
-
-    test('invalid secondaryTab should not switch tab', done => {
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-      const queryMap = new Map();
-      queryMap.set('secondaryTab', 'random');
-      // view is required
-      element.params = Object.assign({
-        view: GerritNav.View.CHANGE,
-      }, element.params, {queryMap});
-      flush(() => {
-        assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-        done();
-      });
-    });
   });
 
   suite('Findings comment tab', () => {
@@ -862,7 +837,7 @@
       };
       element._commentThreads = THREADS;
       const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
-      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
+      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[3]);
       flush(() => {
         done();
       });
@@ -1009,7 +984,7 @@
     commitMessage = 'CC=test@google.com';
     result = element._prepareCommitMsgForLinkify(commitMessage);
     assert.equal(result, 'CC=\u200Btest@google.com');
-  }),
+  });
 
   test('_isSubmitEnabled', () => {
     assert.isFalse(element._isSubmitEnabled({}));
@@ -1239,20 +1214,6 @@
     assert.isTrue(collapseStub.calledTwice);
   });
 
-  test('related changes are updated and new patch selected after rebase',
-      done => {
-        element._changeNum = '42';
-        sandbox.stub(element, 'computeLatestPatchNum', () => 1);
-        sandbox.stub(element, '_reload',
-            () => Promise.resolve());
-        const e = {detail: {action: 'rebase'}};
-        element._handleReloadChange(e).then(() => {
-          assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
-              element._change));
-          done();
-        });
-      });
-
   test('related changes are not updated after other action', done => {
     sandbox.stub(element, '_reload', () => Promise.resolve());
     sandbox.stub(element.$.relatedChanges, 'reload');
@@ -1315,7 +1276,7 @@
 
   test('show commit message edit button', () => {
     const _change = {
-      status: element.ChangeStatus.MERGED,
+      status: ChangeStatus.MERGED,
     };
     assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
     assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
@@ -1596,7 +1557,7 @@
         rev2: {_number: 2, commit: {parents: []}},
       },
       current_revision: 'rev1',
-      status: element.ChangeStatus.MERGED,
+      status: ChangeStatus.MERGED,
       labels: {},
       actions: {},
     };
@@ -1890,7 +1851,7 @@
         sandbox.stub(element, 'fetchChangeUpdates')
             .returns(Promise.resolve({
               isLatest: true,
-              newStatus: element.ChangeStatus.MERGED,
+              newStatus: ChangeStatus.MERGED,
             }));
         element.addEventListener('show-alert', e => {
           assert.equal(e.detail.message, 'This change has been merged');
@@ -2021,7 +1982,10 @@
     };
     const fileList = element.$.fileList;
     const Actions = GrEditConstants.Actions;
-    const controls = element.$.fileListHeader.$.editControls;
+    element.$.fileListHeader.editMode = true;
+    flushAsynchronousOperations();
+    const controls = element.$.fileListHeader
+        .shadowRoot.querySelector('#editControls');
     sandbox.stub(controls, 'openDeleteDialog');
     sandbox.stub(controls, 'openRenameDialog');
     sandbox.stub(controls, 'openRestoreDialog');
@@ -2247,7 +2211,7 @@
 
     test('merged change', () => {
       element._mergeable = null;
-      element._change.status = element.ChangeStatus.MERGED;
+      element._change.status = ChangeStatus.MERGED;
       return element._getMergeability().then(() => {
         assert.isFalse(element._mergeable);
         assert.isFalse(getMergeableStub.called);
@@ -2256,7 +2220,7 @@
 
     test('abandoned change', () => {
       element._mergeable = null;
-      element._change.status = element.ChangeStatus.ABANDONED;
+      element._change.status = ChangeStatus.ABANDONED;
       return element._getMergeability().then(() => {
         assert.isFalse(element._mergeable);
         assert.isFalse(getMergeableStub.called);
@@ -2315,9 +2279,9 @@
 
     test('don\'t report changedDisplayed on reply', done => {
       const changeDisplayStub =
-        sandbox.stub(element.$.reporting, 'changeDisplayed');
+        sandbox.stub(element.reporting, 'changeDisplayed');
       const changeFullyLoadedStub =
-        sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+        sandbox.stub(element.reporting, 'changeFullyLoaded');
       element._handleReplySent();
       flush(() => {
         assert.isFalse(changeDisplayStub.called);
@@ -2328,9 +2292,9 @@
 
     test('report changedDisplayed on _paramsChanged', done => {
       const changeDisplayStub =
-        sandbox.stub(element.$.reporting, 'changeDisplayed');
+        sandbox.stub(element.reporting, 'changeDisplayed');
       const changeFullyLoadedStub =
-        sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+        sandbox.stub(element.reporting, 'changeFullyLoaded');
       element._paramsChanged({
         view: GerritNav.View.CHANGE,
         changeNum: 101,
@@ -2344,4 +2308,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 7ca9d6b..576b8bd 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -23,8 +23,6 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-formatted-text/gr-formatted-text.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -38,7 +36,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCommentList extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
index 60b83ee..2811828 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
@@ -34,6 +34,9 @@
       min-width: 135px;
       text-align: right;
     }
+    .patchset-level-comment-text {
+      margin-right: var(--spacing-m);
+    }
     .message {
       flex: 1;
       --gr-formatted-text-prose-max-width: 80ch;
@@ -55,9 +58,13 @@
     as="file"
   >
     <div class="file">
-      <a class="fileLink" href="[[_computeDiffURL(file, changeNum, comments)]]"
-        >[[computeDisplayPath(file)]]</a
-      >
+      <template is="dom-if" if="[[!shouldHideFile(file)]]">
+        <a
+          class="fileLink"
+          href="[[_computeDiffURL(file, changeNum, comments)]]"
+          >[[computeDisplayPath(file)]]
+        </a>
+      </template>
     </div>
     <template
       is="dom-repeat"
@@ -65,18 +72,25 @@
       as="comment"
     >
       <div class="container">
-        <a
-          class="lineNum"
-          href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"
-        >
-          <span hidden$="[[!comment.line]]">
-            <span>[[_computePatchDisplayName(comment)]]</span>
-            Line <span>[[comment.line]]</span>
+        <template is="dom-if" if="[[shouldHideFile(file)]]">
+          <span class="patchset-level-comment-text">
+            Patchset Comment:
           </span>
-          <span hidden$="[[comment.line]]">
-            File comment:
-          </span>
-        </a>
+        </template>
+        <template is="dom-if" if="[[!shouldHideFile(file)]]">
+          <a
+            class="lineNum"
+            href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"
+          >
+            <span hidden$="[[!comment.line]]">
+              <span>[[_computePatchDisplayName(comment)]]</span>
+              Line <span>[[comment.line]]</span>
+            </span>
+            <span hidden$="[[comment.line]]">
+              File comment:
+            </span>
+          </a>
+        </template>
         <gr-formatted-text
           class="message"
           no-trailing-margin=""
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index 4dba3af..052189e 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-commit-info_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCommitInfo extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index d28e2b7..ada7dae 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,7 +25,7 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmAbandonDialog extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
index 8010814..c1e1165 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -61,7 +61,6 @@
     assert.isTrue(confirmHandler.calledOnce);
     assert.isTrue(element._handleConfirmTap.called);
     assert.isTrue(element._confirm.called);
-    assert.isTrue(element._confirm.called);
     assert.isTrue(element._confirm.calledOnce);
   });
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
index 480e6cf..34a3dcc 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-confirm-cherrypick-conflict-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmCherrypickConflictDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 2802046..d2dcbca 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -14,20 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-dialog/gr-dialog.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-confirm-cherrypick-dialog_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
 
 const SUGGESTIONS_LIMIT = 15;
 const CHANGE_SUBJECT_LIMIT = 50;
@@ -37,7 +35,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmCherrypickDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -97,6 +95,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   static get observers() {
     return [
       '_computeMessage(changeStatus, commitNum, commitMessage)',
@@ -260,7 +263,7 @@
     e.preventDefault();
     e.stopPropagation();
     if (this._cherryPickType === CHERRY_PICK_TYPES.TOPIC) {
-      this.$.reporting.reportInteraction('cherry-pick-topic-clicked');
+      this.reporting.reportInteraction('cherry-pick-topic-clicked');
       this._handleCherryPickTopic();
       return;
     }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
index aeb8061..eaf9cc8 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
@@ -212,9 +212,5 @@
       </template>
     </div>
   </gr-dialog>
-  <gr-reporting
-    id="reporting"
-    category="confirm-cherry-pick-dialog"
-  ></gr-reporting>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
index 25beb2d..f2ea45d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -31,7 +28,7 @@
 const SUGGESTIONS_LIMIT = 15;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmMoveDialog extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index e451034..0a45de2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -25,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-confirm-rebase-dialog_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrConfirmRebaseDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 6eb4c82..9489b94 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -37,7 +34,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmRevertDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
index 212f909..c8e14f7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
@@ -30,7 +27,7 @@
 const CHANGE_SUBJECT_LIMIT = 50;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmRevertSubmissionDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index 5d599b7..42afcc2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../shared/gr-icons/gr-icons.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -28,7 +26,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-confirm-submit-dialog_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrConfirmSubmitDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 4c457a1..6e22374 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-download-commands/gr-download-commands.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,7 +25,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDownloadDialog extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
index 9569c03..0446e4e 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
@@ -56,16 +56,12 @@
     .archives a:last-of-type {
       margin-right: 0;
     }
-    .title {
-      flex: 1;
-      font-weight: var(--font-weight-bold);
-    }
     .hidden {
       display: none;
     }
   </style>
   <section>
-    <h3 class="title">
+    <h3 class="heading-3">
       Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
     </h3>
   </section>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 73c6721..1f9ca20 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector.js';
 import '../../diff/gr-patch-range-select/gr-patch-range-select.js';
@@ -43,7 +41,7 @@
 const MERGED_STATUS = 'MERGED';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrFileListHeader extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
index 10b8606..bb04114 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
@@ -188,14 +188,16 @@
       </div>
     </div>
     <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
-      <span class="showOnEdit flexContainer">
-        <gr-edit-controls
-          id="editControls"
-          patch-num="[[patchNum]]"
-          change="[[change]]"
-        ></gr-edit-controls>
-        <span class="separator"></span>
-      </span>
+      <template is="dom-if" if="[[editMode]]">
+        <span class="showOnEdit flexContainer">
+          <gr-edit-controls
+            id="editControls"
+            patch-num="[[patchNum]]"
+            change="[[change]]"
+          ></gr-edit-controls>
+          <span class="separator"></span>
+        </span>
+      </template>
       <span class$="[[_computeUploadHelpContainerClass(change, account)]]">
         <gr-button link="" class="upload" on-click="_handleUploadTap"
           >Update Change</gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index 19362d5..2a5a302 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -299,13 +299,22 @@
     });
 
     test('edit-controls visibility', () => {
+      element.editMode = false;
+      flushAsynchronousOperations();
+      // on the first render, when editMode is false, editControls are not
+      // in the DOM to reduce size of DOM and make first render faster.
+      assert.isNull(element.shadowRoot
+          .querySelector('#editControls'));
+
       element.editMode = true;
       flushAsynchronousOperations();
-      assert.isTrue(isVisible(element.$.editControls.parentElement));
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('#editControls').parentElement));
 
       element.editMode = false;
       flushAsynchronousOperations();
-      assert.isFalse(isVisible(element.$.editControls.parentElement));
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('#editControls').parentElement));
     });
 
     test('_computeUploadHelpContainerClass', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index eb85cd7..fbae39f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../diff/gr-diff-cursor/gr-diff-cursor.js';
 import '../../diff/gr-diff-host/gr-diff-host.js';
 import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
@@ -46,6 +43,8 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {appContext} from '../../../services/app-context.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -71,6 +70,8 @@
   U: 'Unchanged',
 };
 
+const FILE_ROW_CLASS = 'file-row';
+
 /**
  * Type for FileInfo
  *
@@ -87,17 +88,7 @@
  */
 
 /**
- * Type for FileData
- *
- * This contains minimal info required about the file to get comments for
- *
- * @typedef {Object} FileData
- * @property {string} path
- * @property {?string} oldPath
- */
-
-/**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrFileList extends mixinBehaviors( [
   AsyncForeachBehavior,
@@ -206,7 +197,7 @@
        */
       _reportinShownFilesIncrement: Number,
 
-      /** @type {!Array<FileData>} */
+      /** @type {!Array<Gerrit.FileRange>} */
       _expandedFiles: {
         type: Array,
         value() { return []; },
@@ -236,6 +227,11 @@
         computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
                 '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
       },
+      _showPrependedDynamicColumns: {
+        type: Boolean,
+        computed: '_computeShowPrependedDynamicColumns(' +
+        '_dynamicPrependedHeaderEndpoints, _dynamicPrependedContentEndpoints)',
+      },
       /** @type {Array<string>} */
       _dynamicHeaderEndpoints: {
         type: Array,
@@ -248,6 +244,14 @@
       _dynamicSummaryEndpoints: {
         type: Array,
       },
+      /** @type {Array<string>} */
+      _dynamicPrependedHeaderEndpoints: {
+        type: Array,
+      },
+      /** @type {Array<string>} */
+      _dynamicPrependedContentEndpoints: {
+        type: Array,
+      },
     };
   }
 
@@ -271,6 +275,8 @@
       [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
       [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
       [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+      [this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+        '_handleToggleHideAllCommentThreads',
       [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
       [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
       [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
@@ -290,6 +296,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -301,18 +312,27 @@
   attached() {
     super.attached();
     pluginLoader.awaitPluginsLoaded().then(() => {
-      this._dynamicHeaderEndpoints = pluginEndpoints.getDynamicEndpoints(
-          'change-view-file-list-header');
-      this._dynamicContentEndpoints = pluginEndpoints.getDynamicEndpoints(
-          'change-view-file-list-content');
-      this._dynamicSummaryEndpoints = pluginEndpoints.getDynamicEndpoints(
-          'change-view-file-list-summary');
+      this._dynamicHeaderEndpoints = pluginEndpoints
+          .getDynamicEndpoints('change-view-file-list-header');
+      this._dynamicContentEndpoints = pluginEndpoints
+          .getDynamicEndpoints('change-view-file-list-content');
+      this._dynamicPrependedHeaderEndpoints = pluginEndpoints
+          .getDynamicEndpoints('change-view-file-list-header-prepend');
+      this._dynamicPrependedContentEndpoints = pluginEndpoints
+          .getDynamicEndpoints('change-view-file-list-content-prepend');
+      this._dynamicSummaryEndpoints = pluginEndpoints
+          .getDynamicEndpoints('change-view-file-list-summary');
 
       if (this._dynamicHeaderEndpoints.length !==
           this._dynamicContentEndpoints.length) {
         console.warn(
             'Different number of dynamic file-list header and content.');
       }
+      if (this._dynamicPrependedHeaderEndpoints.length !==
+        this._dynamicPrependedContentEndpoints.length) {
+        console.warn(
+            'Different number of dynamic file-list header and content.');
+      }
       if (this._dynamicHeaderEndpoints.length !==
           this._dynamicSummaryEndpoints.length) {
         console.warn(
@@ -375,14 +395,14 @@
     return Promise.all(promises).then(() => {
       this._loading = false;
       this._detectChromiteButler();
-      this.$.reporting.fileListDisplayed();
+      this.reporting.fileListDisplayed();
     });
   }
 
   _detectChromiteButler() {
     const hasButler = !!document.getElementById('butler-suggested-owners');
     if (hasButler) {
-      this.$.reporting.reportExtension('butler');
+      this.reporting.reportExtension('butler');
     }
   }
 
@@ -444,13 +464,13 @@
   }
 
   _toggleFileExpandedByIndex(index) {
-    this._toggleFileExpanded(this._computeFileData(this._files[index]));
+    this._toggleFileExpanded(this._computeFileRange(this._files[index]));
   }
 
   _updateDiffPreferences() {
     if (!this.diffs.length) { return; }
     // Re-render all expanded diffs sequentially.
-    this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+    this.reporting.time(EXPAND_ALL_TIMING_LABEL);
     this._renderInOrder(this._expandedFiles, this.diffs,
         this._expandedFiles.length);
   }
@@ -472,7 +492,7 @@
     for (let i = 0; i < this._shownFiles.length; i++) {
       path = this._shownFiles[i].__path;
       if (!this._expandedFiles.some(f => f.path === path)) {
-        newFiles.push(this._computeFileData(this._shownFiles[i]));
+        newFiles.push(this._computeFileRange(this._shownFiles[i]));
       }
     }
 
@@ -496,6 +516,9 @@
    * @return {string}
    */
   _computeCommentsString(changeComments, patchRange, path) {
+    if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+      return '';
+    }
     const unresolvedCount =
         changeComments.computeUnresolvedNum({
           patchNum: patchRange.basePatchNum,
@@ -535,6 +558,9 @@
    * @return {string}
    */
   _computeDraftsString(changeComments, patchRange, path) {
+    if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+      return '';
+    }
     const draftCount =
         changeComments.computeDraftCount({
           patchNum: patchRange.basePatchNum,
@@ -556,6 +582,9 @@
    * @return {string}
    */
   _computeDraftsStringMobile(changeComments, patchRange, path) {
+    if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+      return '';
+    }
     const draftCount =
         changeComments.computeDraftCount({
           patchNum: patchRange.basePatchNum,
@@ -577,6 +606,9 @@
    * @return {string}
    */
   _computeCommentsStringMobile(changeComments, patchRange, path) {
+    if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+      return '';
+    }
     const commentCount =
         changeComments.computeCommentCount({
           patchNum: patchRange.basePatchNum,
@@ -648,29 +680,55 @@
   }
 
   /**
+   * Returns true if the event e is a click on an element.
+   *
+   * The click is: mouse click or pressing Enter or Space key
+   * P.S> Screen readers sends click event as well
+   */
+  _isClickEvent(e) {
+    if (e.type === 'click') {
+      return true;
+    }
+    const isSpaceOrEnter = (e.key === 'Enter' || e.key === ' ');
+    return e.type === 'keydown' && isSpaceOrEnter;
+  }
+
+  _fileActionClick(e, fileAction) {
+    if (this._isClickEvent(e)) {
+      const fileRow = this._getFileRowFromEvent(e);
+      if (!fileRow) {
+        return;
+      }
+      // Prevent default actions (e.g. scrolling for space key)
+      e.preventDefault();
+      // Prevent _handleFileListClick handler call
+      e.stopPropagation();
+      this.$.fileCursor.setCursor(fileRow.element);
+      fileAction(fileRow.file);
+    }
+  }
+
+  _reviewedClick(e) {
+    this._fileActionClick(e,
+        file => this._reviewFile(file.path));
+  }
+
+  _expandedClick(e) {
+    this._fileActionClick(e,
+        file => this._toggleFileExpanded(file));
+  }
+
+  /**
    * Handle all events from the file list dom-repeat so event handleers don't
    * have to get registered for potentially very long lists.
    */
   _handleFileListClick(e) {
-    // Traverse upwards to find the row element if the target is not the row.
-    let row = e.target;
-    while (!row.classList.contains('row') && row.parentElement) {
-      row = row.parentElement;
-    }
-
-    // No action needed for item without a valid file
-    if (!row.dataset.file) {
+    const fileRow = this._getFileRowFromEvent(e);
+    if (!fileRow) {
       return;
     }
-
-    const file = JSON.parse(row.dataset.file);
+    const file = fileRow.file;
     const path = file.path;
-    // Handle checkbox mark as reviewed.
-    if (e.target.classList.contains('markReviewed')) {
-      e.preventDefault();
-      return this._reviewFile(path);
-    }
-
     // If a path cannot be interpreted from the click target (meaning it's not
     // somewhere in the row, e.g. diff content) or if the user clicked the
     // link, defer to the native behavior.
@@ -680,21 +738,40 @@
     if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
 
     e.preventDefault();
+    this.$.fileCursor.setCursor(fileRow.element);
     this._toggleFileExpanded(file);
   }
 
+  _getFileRowFromEvent(e) {
+    // Traverse upwards to find the row element if the target is not the row.
+    let row = e.target;
+    while (!row.classList.contains(FILE_ROW_CLASS) && row.parentElement) {
+      row = row.parentElement;
+    }
+
+    // No action needed for item without a valid file
+    if (!row.dataset.file) {
+      return null;
+    }
+
+    return {
+      file: JSON.parse(row.dataset.file),
+      element: row,
+    };
+  }
+
   /**
-   * Generates file data from file info object.
+   * Generates file range from file info object.
    *
    * @param {FileInfo} file
-   * @returns {FileData}
+   * @returns {Gerrit.FileRange}
    */
-  _computeFileData(file) {
+  _computeFileRange(file) {
     const fileData = {
       path: file.__path,
     };
     if (file.old_path) {
-      fileData.oldPath = file.old_path;
+      fileData.basePath = file.old_path;
     }
     return fileData;
   }
@@ -733,6 +810,15 @@
     this._toggleInlineDiffs();
   }
 
+  _handleToggleHideAllCommentThreads(e) {
+    if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+      return;
+    }
+
+    e.preventDefault();
+    this.toggleClass('hideComments');
+  }
+
   _handleCursorNext(e) {
     if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
       return;
@@ -910,7 +996,7 @@
         .some(arg => arg === undefined)) {
       return;
     }
-    if (editMode && path !== this.MERGE_LIST_PATH) {
+    if (editMode && path !== SpecialFilePath.MERGE_LIST) {
       return GerritNav.getEditUrlForDiff(change, path, patchRange.patchNum,
           patchRange.basePatchNum);
     }
@@ -953,12 +1039,18 @@
     if (baseClass) {
       classes.push(baseClass);
     }
-    if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
+    if (path === SpecialFilePath.COMMIT_MESSAGE ||
+      path === SpecialFilePath.MERGE_LIST) {
       classes.push('invisible');
     }
     return classes.join(' ');
   }
 
+  _computeStatusClass(file) {
+    const classStr = this._computeClass('status', file.__path);
+    return `${classStr} ${this._computeFileStatus(file.status)}`;
+  }
+
   _computePathClass(path, expandedFilesRecord) {
     return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
   }
@@ -985,10 +1077,7 @@
 
     const commentedPaths = changeComments.getPaths(patchRange);
     const files = Object.assign({}, filesByPath);
-    Object.keys(commentedPaths).forEach(commentedPath => {
-      if (files.hasOwnProperty(commentedPath)) { return; }
-      files[commentedPath] = {status: 'U'};
-    });
+    this.addUnmodifiedFiles(files, commentedPaths);
     const reviewedSet = new Set(reviewed || []);
     for (const filePath in files) {
       if (!files.hasOwnProperty(filePath)) { continue; }
@@ -1016,7 +1105,7 @@
     // Start the timer for the rendering work hwere because this is where the
     // _shownFiles property is being set, and _shownFiles is used in the
     // dom-repeat binding.
-    this.$.reporting.time(RENDER_TIMING_LABEL);
+    this.reporting.time(RENDER_TIMING_LABEL);
 
     // How many more files are being shown (if it's an increase).
     this._reportinShownFilesIncrement =
@@ -1034,9 +1123,8 @@
   _filesChanged() {
     if (this._files && this._files.length > 0) {
       flush();
-      const files = Array.from(
-          dom(this.root).querySelectorAll('.file-row'));
-      this.$.fileCursor.stops = files;
+      this.$.fileCursor.stops = Array.from(
+          dom(this.root).querySelectorAll(`.${FILE_ROW_CLASS}`));
       this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
     }
   }
@@ -1099,10 +1187,29 @@
       FileStatus[statusCode] : 'Status Unknown';
   }
 
+  /**
+   * Converts any boolean-like variable to the string 'true' or 'false'
+   *
+   * This method is useful when you bind aria-checked attribute to a boolean
+   * value. The aria-checked attribute is string attribute. Binding directly
+   * to boolean variable causes problem on gerrit-CI.
+   *
+   * @param {object} val
+   * @return {string} 'true' if val is true-like, otherwise false
+   */
+  _booleanToString(val) {
+    return val ? 'true' : 'false';
+  }
+
   _isFileExpanded(path, expandedFilesRecord) {
     return expandedFilesRecord.base.some(f => f.path === path);
   }
 
+  _isFileExpandedStr(path, expandedFilesRecord) {
+    return this._booleanToString(
+        this._isFileExpanded(path, expandedFilesRecord));
+  }
+
   _computeExpandedFiles(expandedCount, totalCount) {
     if (expandedCount === 0) {
       return GrFileListConstants.FilesExpandedState.NONE;
@@ -1141,14 +1248,14 @@
     // Required so that the newly created diff view is included in this.diffs.
     flush();
 
-    this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+    this.reporting.time(EXPAND_ALL_TIMING_LABEL);
 
     if (newFiles.length) {
       this._renderInOrder(newFiles, this.diffs, newFiles.length);
     }
 
     this._updateDiffCursor();
-    this.$.diffCursor.handleDiffUpdate();
+    this.$.diffCursor.reInitAndUpdateStops();
   }
 
   _clearCollapsedDiffs(collapsedDiffs) {
@@ -1161,9 +1268,9 @@
   /**
    * Given an array of paths and a NodeList of diff elements, render the diff
    * for each path in order, awaiting the previous render to complete before
-   * continung.
+   * continuing.
    *
-   * @param  {!Array<FileData>} files
+   * @param  {!Array<Gerrit.FileRange>} files
    * @param  {!NodeList<!Object>} diffElements (GrDiffHostElement)
    * @param  {number} initialCount The total number of paths in the pass. This
    *   is used to generate log messages.
@@ -1172,6 +1279,14 @@
   _renderInOrder(files, diffElements, initialCount) {
     let iter = 0;
 
+    for (const file of files) {
+      const path = file.path;
+      const diffElem = this._findDiffByPath(path, diffElements);
+      if (diffElem) {
+        diffElem.prefetchDiff();
+      }
+    }
+
     return (new Promise(resolve => {
       this.dispatchEvent(new CustomEvent('reload-drafts', {
         detail: {resolve},
@@ -1200,9 +1315,21 @@
       this._cancelForEachDiff = null;
       this._nextRenderParams = null;
       console.log('Finished expanding', initialCount, 'diff(s)');
-      this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
+      this.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
           EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
-      this.$.diffCursor.handleDiffUpdate();
+      /* Block diff cursor from auto scrolling after files are done rendering.
+       * This prevents the bug where the screen jumps to the first diff chunk
+       * after files are done being rendered after the user has already begun
+       * scrolling.
+       * This also however results in the fact that the cursor does not auto
+       * focus on the first diff chunk on a small screen. This is however, a use
+       * case we are willing to not support for now.
+
+       * Using handleDiffUpdate resulted in diffCursor.row being set which
+       * prevented the issue of scrolling to top when we expand the second
+       * file individually.
+       */
+      this.$.diffCursor.reInitAndUpdateStops();
     }));
   }
 
@@ -1262,7 +1389,6 @@
         c, {__commentSide: threadEl.commentSide}
     ));
     flush();
-    return;
   }
 
   _handleEscKey(e) {
@@ -1307,7 +1433,8 @@
    * @return {boolean}
    */
   _showBarsForPath(path) {
-    return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
+    return path !== SpecialFilePath.COMMIT_MESSAGE &&
+      path !== SpecialFilePath.MERGE_LIST;
   }
 
   /**
@@ -1421,18 +1548,30 @@
 
   /**
    * Shows registered dynamic columns iff the 'header', 'content' and
-   * 'summary' endpoints are regiestered the exact same number of times.
+   * 'summary' endpoints are registered the exact same number of times.
    * Ideally, there should be a better way to enforce the expectation of the
    * dependencies between dynamic endpoints.
    */
   _computeShowDynamicColumns(
       headerEndpoints, contentEndpoints, summaryEndpoints) {
     return headerEndpoints && contentEndpoints && summaryEndpoints &&
+           headerEndpoints.length &&
            headerEndpoints.length === contentEndpoints.length &&
            headerEndpoints.length === summaryEndpoints.length;
   }
 
   /**
+   * Shows registered dynamic prepended columns iff the 'header', 'content'
+   * endpoints are registered the exact same number of times.
+   */
+  _computeShowPrependedDynamicColumns(
+      headerEndpoints, contentEndpoints) {
+    return headerEndpoints && contentEndpoints &&
+           headerEndpoints.length &&
+           headerEndpoints.length === contentEndpoints.length;
+  }
+
+  /**
    * Returns true if none of the inline diffs have been expanded.
    *
    * @return {boolean}
@@ -1452,7 +1591,7 @@
   _reportRenderedRow(index) {
     if (index === this._shownFiles.length - 1) {
       this.async(() => {
-        this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
+        this.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
             RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
       }, 1);
     }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index a9a785e..df7cb00 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -26,8 +26,21 @@
       border-top: 1px solid var(--border-color);
       display: flex;
       min-height: calc(var(--line-height-normal) + 2 * var(--spacing-s));
-      padding: var(--spacing-xs) var(--spacing-l) var(--spacing-xs)
-        calc(var(--spacing-l) - 0.35rem);
+      padding: var(--spacing-xs) var(--spacing-l);
+    }
+    /* The class defines a content visible only to screen readers */
+    .noCommentsScreenReaderText {
+      opacity: 0;
+      max-width: 1px;
+      overflow: hidden;
+      display: none;
+    }
+    div[role='gridcell']
+      > div.comments
+      > span:empty
+      + span:empty
+      + span.noCommentsScreenReaderText {
+      display: inline;
     }
     :host(.loading) .row {
       opacity: 0.5;
@@ -62,12 +75,32 @@
       align-items: center;
       display: inline-flex;
     }
-    .reviewed,
-    .status {
+    .reviewed {
       display: inline-block;
       text-align: left;
       width: 1.5em;
     }
+    .status {
+      display: inline-block;
+      border-radius: var(--border-radius);
+      margin-left: var(--spacing-s);
+      padding: 0 var(--spacing-m);
+      color: var(--primary-text-color);
+      font-size: var(--font-size-small);
+      background-color: var(--dark-add-highlight-color);
+    }
+    .status.invisible,
+    .status.M {
+      display: none;
+    }
+    .status.D,
+    .status.R,
+    .status.W {
+      background-color: var(--dark-remove-highlight-color);
+    }
+    .status.U {
+      background-color: var(--comment-background-color);
+    }
     .file-row {
       cursor: pointer;
     }
@@ -133,10 +166,10 @@
       min-width: 3.5em;
     }
     .added {
-      color: var(--vote-text-color-recommended);
+      color: var(--positive-green-text-color);
     }
     .removed {
-      color: var(--vote-text-color-disliked);
+      color: var(--negative-red-text-color);
       text-align: left;
       min-width: 4em;
       padding-left: var(--spacing-s);
@@ -145,6 +178,9 @@
       color: #c62828;
       font-weight: var(--font-weight-bold);
     }
+    .show-hide-icon:focus {
+      outline: none;
+    }
     .show-hide {
       margin-left: var(--spacing-s);
       width: 1.9em;
@@ -181,27 +217,24 @@
       margin-left: var(--spacing-xxl);
       width: 15em;
     }
-    .reviewed label {
+    .reviewedSwitch {
       color: var(--link-color);
       opacity: 0;
       justify-content: flex-end;
       width: 100%;
     }
-    .reviewed label:hover {
+    .reviewedSwitch:hover {
       cursor: pointer;
       opacity: 100;
     }
     .row:focus {
       outline: none;
     }
-    .row:hover .reviewed label,
-    .row:focus .reviewed label,
-    .row.expanded .reviewed label {
+    .row:hover .reviewedSwitch,
+    .row:focus-within .reviewedSwitch,
+    .row.expanded .reviewedSwitch {
       opacity: 100;
     }
-    .reviewed input {
-      display: none;
-    }
     .reviewedLabel {
       color: var(--deemphasized-text-color);
       margin-right: var(--spacing-l);
@@ -214,6 +247,9 @@
     .editFileControls {
       width: 7em;
     }
+    .markReviewed:focus {
+      outline: none;
+    }
     .markReviewed,
     .pathLink {
       display: inline-block;
@@ -221,7 +257,8 @@
       padding: var(--spacing-s) 0;
       text-decoration: none;
     }
-    .pathLink:hover {
+    .pathLink:hover span.fullFileName,
+    .pathLink:hover span.truncatedFileName {
       text-decoration: underline;
     }
 
@@ -231,13 +268,12 @@
       display: inline-block;
       visibility: hidden;
       vertical-align: bottom;
-      text-decoration: none;
       --gr-button: {
         padding: 0px;
       }
     }
-    .pathLink:hover gr-copy-clipboard,
-    .oldPath:hover gr-copy-clipboard {
+    .row:focus-within gr-copy-clipboard,
+    .row:hover gr-copy-clipboard {
       visibility: visible;
     }
 
@@ -274,28 +310,51 @@
         display: none;
       }
     }
+    :host(.hideComments) {
+      --gr-comment-thread-display: none;
+    }
   </style>
-  <div id="container" on-click="_handleFileListClick">
-    <div class="header-row row">
-      <div class="status"></div>
-      <div class="path">File</div>
-      <div class="comments">Comments</div>
-      <div class="sizeBars">Size</div>
-      <div class="header-stats">Delta</div>
+  <div
+    id="container"
+    on-click="_handleFileListClick"
+    role="grid"
+    aria-label="Files list"
+  >
+    <div class="header-row row" role="row">
+      <!-- endpoint: change-view-file-list-header-prepend -->
+      <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
+        <template
+          is="dom-repeat"
+          items="[[_dynamicPrependedHeaderEndpoints]]"
+          as="headerEndpoint"
+        >
+          <gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
+          </gr-endpoint-decorator>
+        </template>
+      </template>
+      <div class="path" role="columnheader">File</div>
+      <div class="comments" role="columnheader">Comments</div>
+      <div class="sizeBars" role="columnheader">Size</div>
+      <div class="header-stats" role="columnheader">Delta</div>
+      <!-- endpoint: change-view-file-list-header -->
       <template is="dom-if" if="[[_showDynamicColumns]]">
         <template
           is="dom-repeat"
           items="[[_dynamicHeaderEndpoints]]"
           as="headerEndpoint"
         >
-          <gr-endpoint-decorator name$="[[headerEndpoint]]">
+          <gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
           </gr-endpoint-decorator>
         </template>
       </template>
       <!-- Empty div here exists to keep spacing in sync with file rows. -->
-      <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
-      <div class="editFileControls showOnEdit"></div>
-      <div class="show-hide"></div>
+      <div
+        class="reviewed hideOnEdit"
+        hidden$="[[!_loggedIn]]"
+        aria-hidden="true"
+      ></div>
+      <div class="editFileControls showOnEdit" aria-hidden="true"></div>
+      <div class="show-hide" aria-hidden="true"></div>
     </div>
 
     <template
@@ -310,21 +369,32 @@
       <div class="stickyArea">
         <div
           class$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]"
-          data-file$="[[_computeFileData(file)]]"
+          data-file$="[[_computeFileRange(file)]]"
           tabindex="-1"
+          role="row"
         >
-          <div
-            class$="[[_computeClass('status', file.__path)]]"
-            tabindex="0"
-            title$="[[_computeFileStatusLabel(file.status)]]"
-            aria-label$="[[_computeFileStatusLabel(file.status)]]"
-          >
-            [[_computeFileStatus(file.status)]]
-          </div>
+          <!-- endpoint: change-view-file-list-content-prepend -->
+          <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
+            <template
+              is="dom-repeat"
+              items="[[_dynamicPrependedContentEndpoints]]"
+              as="contentEndpoint"
+            >
+              <gr-endpoint-decorator name="[[contentEndpoint]]" role="gridcell">
+                <gr-endpoint-param name="changeNum" value="[[changeNum]]">
+                </gr-endpoint-param>
+                <gr-endpoint-param name="patchRange" value="[[patchRange]]">
+                </gr-endpoint-param>
+                <gr-endpoint-param name="path" value="[[file.__path]]">
+                </gr-endpoint-param>
+              </gr-endpoint-decorator>
+            </template>
+          </template>
           <!-- TODO: Remove data-url as it appears its not used -->
           <span
             data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
             class="path"
+            role="gridcell"
           >
             <a
               class="pathLink"
@@ -342,6 +412,14 @@
               >
                 [[computeTruncatedPath(file.__path)]]
               </span>
+              <span
+                class$="[[_computeStatusClass(file)]]"
+                tabindex="0"
+                title$="[[_computeFileStatusLabel(file.status)]]"
+                aria-label$="[[_computeFileStatusLabel(file.status)]]"
+              >
+                [[_computeFileStatusLabel(file.status)]]
+              </span>
               <gr-copy-clipboard
                 hide-input=""
                 text="[[file.__path]]"
@@ -357,70 +435,123 @@
               </div>
             </template>
           </span>
-          <div class="comments desktop">
-            <span class="drafts">
-              [[_computeDraftsString(changeComments, patchRange, file.__path)]]
-            </span>
-            [[_computeCommentsString(changeComments, patchRange, file.__path)]]
+          <div role="gridcell">
+            <div class="comments desktop">
+              <span class="drafts"
+                ><!-- This comments ensure that span is empty when the function
+                returns empty string.                
+              -->[[_computeDraftsString(changeComments, patchRange,
+                file.__path)]]<!-- This comments ensure that span is empty when
+                the function returns empty string.
+           --></span
+              >
+              <span
+                ><!--
+              -->[[_computeCommentsString(changeComments, patchRange,
+                file.__path)]]<!--
+           --></span
+              >
+              <span class="noCommentsScreenReaderText">
+                <!-- Screen readers read the following content only if 2 other
+              spans in the parent div is empty. The content is not visible on
+              the page.
+              Without this span, screen readers don't navigate correctly inside
+              table, because empty div doesn't rendered. For example, VoiceOver
+              jumps back to the whole table.
+              We can use &nbsp instead, but it sounds worse.                            
+              -->
+                No comments
+              </span>
+            </div>
+            <div class="comments mobile">
+              <span class="drafts"
+                ><!-- This comments ensure that span is empty when the function
+                returns empty string.
+              -->[[_computeDraftsStringMobile(changeComments, patchRange,
+                file.__path)]]<!-- This comments ensure that span is empty when
+                the function returns empty string.
+           --></span
+              >
+              <span
+                ><!--
+             -->[[_computeCommentsStringMobile(changeComments, patchRange,
+                file.__path)]]<!--
+           --></span
+              >
+              <span class="noCommentsScreenReaderText">
+                <!-- The same as for desktop comments -->
+                No comments
+              </span>
+            </div>
           </div>
-          <div class="comments mobile">
-            <span class="drafts">
-              [[_computeDraftsStringMobile(changeComments, patchRange,
-              file.__path)]]
-            </span>
-            [[_computeCommentsStringMobile(changeComments, patchRange,
-            file.__path)]]
-          </div>
-          <div class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
-            <svg width="61" height="8">
-              <rect
-                x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
-                y="0"
-                height="8"
-                fill="#388E3C"
-                width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
-              ></rect>
-              <rect
-                x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
-                y="0"
-                height="8"
-                fill="#D32F2F"
-                width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"
-              ></rect>
-            </svg>
-          </div>
-          <div class$="[[_computeClass('stats', file.__path)]]">
-            <span
-              class="added"
-              tabindex="0"
-              aria-label$="[[file.lines_inserted]] lines added"
-              hidden$="[[file.binary]]"
+          <div role="gridcell">
+            <!-- The content must be in a separate div. It guarantees, that
+              gridcell always visible for screen readers.
+              For example, without a nested div screen readers pronounce the
+              "Commit message" row content with incorrect column headers.
+            -->
+            <div
+              class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"
+              aria-label="A bar that represents the addition and deletion ratio for the current file"
             >
-              +[[file.lines_inserted]]
-            </span>
-            <span
-              class="removed"
-              tabindex="0"
-              aria-label$="[[file.lines_deleted]] lines removed"
-              hidden$="[[file.binary]]"
-            >
-              -[[file.lines_deleted]]
-            </span>
-            <span
-              class$="[[_computeBinaryClass(file.size_delta)]]"
-              hidden$="[[!file.binary]]"
-            >
-              [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size,
-              file.size_delta)]]
-            </span>
+              <svg width="61" height="8">
+                <rect
+                  x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
+                  y="0"
+                  height="8"
+                  fill="var(--positive-green-text-color)"
+                  width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
+                ></rect>
+                <rect
+                  x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
+                  y="0"
+                  height="8"
+                  fill="var(--negative-red-text-color)"
+                  width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"
+                ></rect>
+              </svg>
+            </div>
           </div>
+          <div class="stats" role="gridcell">
+            <!-- The content must be in a separate div. It guarantees, that
+            gridcell always visible for screen readers.
+            For example, without a nested div screen readers pronounce the
+            "Commit message" row content with incorrect column headers.
+            -->
+            <div class$="[[_computeClass('', file.__path)]]">
+              <span
+                class="added"
+                tabindex="0"
+                aria-label$="[[file.lines_inserted]] lines added"
+                hidden$="[[file.binary]]"
+              >
+                +[[file.lines_inserted]]
+              </span>
+              <span
+                class="removed"
+                tabindex="0"
+                aria-label$="[[file.lines_deleted]] lines removed"
+                hidden$="[[file.binary]]"
+              >
+                -[[file.lines_deleted]]
+              </span>
+              <span
+                class$="[[_computeBinaryClass(file.size_delta)]]"
+                hidden$="[[!file.binary]]"
+              >
+                [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size,
+                file.size_delta)]]
+              </span>
+            </div>
+          </div>
+          <!-- endpoint: change-view-file-list-content -->
           <template is="dom-if" if="[[_showDynamicColumns]]">
             <template
               is="dom-repeat"
               items="[[_dynamicContentEndpoints]]"
               as="contentEndpoint"
             >
-              <div class$="[[_computeClass('', file.__path)]]">
+              <div class$="[[_computeClass('', file.__path)]]" role="gridcell">
                 <gr-endpoint-decorator name="[[contentEndpoint]]">
                   <gr-endpoint-param name="changeNum" value="[[changeNum]]">
                   </gr-endpoint-param>
@@ -432,25 +563,44 @@
               </div>
             </template>
           </template>
-          <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden="">
+          <div
+            class="reviewed hideOnEdit"
+            role="gridcell"
+            hidden$="[[!_loggedIn]]"
+          >
             <span
               class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]"
+              aria-hidden$="[[!file.isReviewed]]"
               >Reviewed</span
             >
-            <label>
-              <input
-                class="reviewed"
-                type="checkbox"
-                checked="[[file.isReviewed]]"
-              />
+            <!-- Do not use input type="checkbox" with hidden input and
+                  visible label here. Screen readers don't read/interract
+                  correctly with such input. 
+              -->
+            <span
+              class="reviewedSwitch"
+              role="switch"
+              tabindex="0"
+              on-click="_reviewedClick"
+              on-keydown="_reviewedClick"
+              aria-label="Reviewed"
+              aria-checked$="[[_booleanToString(file.isReviewed)]]"
+            >
+              <!-- Trick with tabindex to avoid outline on mouse focus, but
+                preserve focus outline for keyboard navigation -->
               <span
+                tabindex="-1"
                 class="markReviewed"
                 title$="[[_reviewedTitle(file.isReviewed)]]"
                 >[[_computeReviewedText(file.isReviewed)]]</span
               >
-            </label>
+            </span>
           </div>
-          <div class="editFileControls showOnEdit">
+          <div
+            class="editFileControls showOnEdit"
+            role="gridcell"
+            aria-hidden$="[[!editMode]]"
+          >
             <template is="dom-if" if="[[editMode]]">
               <gr-edit-file-controls
                 class$="[[_computeClass('', file.__path)]]"
@@ -458,25 +608,32 @@
               ></gr-edit-file-controls>
             </template>
           </div>
-          <div class="show-hide">
-            <label
+          <div class="show-hide" role="gridcell">
+            <!-- Do not use input type="checkbox" with hidden input and
+                visible label here. Screen readers don't read/interract
+                correctly with such input. 
+            -->
+            <span
               class="show-hide"
               data-path$="[[file.__path]]"
               data-expand="true"
+              role="switch"
+              tabindex="0"
+              aria-checked$="[[_isFileExpandedStr(file.__path, _expandedFiles.*)]]"
+              aria-label="Expand file"
+              on-click="_expandedClick"
+              on-keydown="_expandedClick"
             >
-              <input
-                type="checkbox"
-                class="show-hide"
-                checked$="[[_isFileExpanded(file.__path, _expandedFiles.*)]]"
-                data-path$="[[file.__path]]"
-                data-expand="true"
-              />
+              <!-- Trick with tabindex to avoid outline on mouse focus, but
+              preserve focus outline for keyboard navigation -->
               <iron-icon
+                class="show-hide-icon"
+                tabindex="-1"
                 id="icon"
                 icon="[[_computeShowHideIcon(file.__path, _expandedFiles.*)]]"
               >
               </iron-icon>
-            </label>
+            </span>
           </div>
         </div>
         <template
@@ -490,6 +647,7 @@
             hidden="[[!_isFileExpanded(file.__path, _expandedFiles.*)]]"
             change-num="[[changeNum]]"
             patch-range="[[patchRange]]"
+            file="[[_computeFileRange(file)]]"
             path="[[file.__path]]"
             prefs="[[diffPrefs]]"
             project-name="[[change.project]]"
@@ -505,18 +663,19 @@
       <span
         class="added"
         tabindex="0"
-        aria-label$="[[_patchChange.inserted]] lines added"
+        aria-label$="Total [[_patchChange.inserted]] lines added"
       >
         +[[_patchChange.inserted]]
       </span>
       <span
         class="removed"
         tabindex="0"
-        aria-label$="[[_patchChange.deleted]] lines removed"
+        aria-label$="Total [[_patchChange.deleted]] lines removed"
       >
         -[[_patchChange.deleted]]
       </span>
     </div>
+    <!-- endpoint: change-view-file-list-summary -->
     <template is="dom-if" if="[[_showDynamicColumns]]">
       <template
         is="dom-repeat"
@@ -534,12 +693,18 @@
   </div>
   <div class="row totalChanges" hidden$="[[_hideBinaryChangeTotals]]">
     <div class="total-stats">
-      <span class="added" aria-label="Total lines added">
+      <span
+        class="added"
+        aria-label$="Total bytes inserted: [[_formatBytes(_patchChange.size_delta_inserted)]] "
+      >
         [[_formatBytes(_patchChange.size_delta_inserted)]]
         [[_formatPercentage(_patchChange.total_size,
         _patchChange.size_delta_inserted)]]
       </span>
-      <span class="removed" aria-label="Total lines removed">
+      <span
+        class="removed"
+        aria-label$="Total bytes removed: [[_formatBytes(_patchChange.size_delta_deleted)]]"
+      >
         [[_formatBytes(_patchChange.size_delta_deleted)]]
         [[_formatPercentage(_patchChange.total_size,
         _patchChange.size_delta_deleted)]]
@@ -583,9 +748,8 @@
   <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
   <gr-cursor-manager
     id="fileCursor"
-    scroll-behavior="keep-visible"
+    scroll-mode="keep-visible"
     focus-on-move=""
     cursor-target-class="selected"
   ></gr-cursor-manager>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 32945e4..ad0d1cf 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -45,9 +45,9 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-file-list.js';
-import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GrFileListConstants} from '../gr-file-list-constants.js';
@@ -95,6 +95,7 @@
       });
       stub('gr-diff-host', {
         reload() { return Promise.resolve(); },
+        prefetchDiff() {},
       });
 
       // Element must be wrapped in an element with direct access to the
@@ -836,36 +837,42 @@
         patchNum: '2',
       };
       element.$.fileCursor.setCursorAtIndex(0);
+      const reviewSpy = sandbox.spy(element, '_reviewFile');
+      const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
 
       flushAsynchronousOperations();
       const fileRows =
           dom(element.root).querySelectorAll('.row:not(.header-row)');
-      const checkSelector = 'input.reviewed[type="checkbox"]';
+      const checkSelector = 'span.reviewedSwitch[role="switch"]';
       const commitMsg = fileRows[0].querySelector(checkSelector);
       const fileAdded = fileRows[1].querySelector(checkSelector);
       const myFile = fileRows[2].querySelector(checkSelector);
 
-      assert.isTrue(commitMsg.checked);
-      assert.isFalse(fileAdded.checked);
-      assert.isTrue(myFile.checked);
+      assert.equal(commitMsg.getAttribute('aria-checked'), 'true');
+      assert.equal(fileAdded.getAttribute('aria-checked'), 'false');
+      assert.equal(myFile.getAttribute('aria-checked'), 'true');
 
       const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
-      const markReviewLabel = commitMsg.nextElementSibling;
+      const markReviewLabel = fileRows[0].querySelector('.markReviewed');
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
 
-      const clickSpy = sandbox.spy(element, '_handleFileListClick');
+      const clickSpy = sandbox.spy(element, '_reviewedClick');
       MockInteractions.tap(markReviewLabel);
-      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
-      assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
+      // assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+      // assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
       assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+      assert.isTrue(reviewSpy.calledOnce);
 
       MockInteractions.tap(markReviewLabel);
       assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
       assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
       assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
       assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+      assert.isTrue(reviewSpy.calledTwice);
+
+      assert.isFalse(toggleExpandSpy.called);
     });
 
     test('_computeFileStatusLabel', () => {
@@ -906,13 +913,6 @@
       assert.isTrue(clickSpy.calledTwice);
       assert.isTrue(toggleExpandSpy.calledOnce);
       assert.isFalse(reviewStub.called);
-
-      // Click the reviewed checkbox, resulting in a call to _reviewFile, but
-      // no additional call to _toggleFileExpanded.
-      row.querySelector('.markReviewed').click();
-      assert.isTrue(clickSpy.calledThrice);
-      assert.isTrue(toggleExpandSpy.calledOnce);
-      assert.isTrue(reviewStub.calledOnce);
     });
 
     test('_handleFileListClick editMode', () => {
@@ -975,12 +975,12 @@
           dom(element.root).querySelectorAll('.row:not(.header-row)');
       // Because the label surrounds the input, the tap event is triggered
       // there first.
-      const showHideLabel = fileRows[0].querySelector('label.show-hide');
       const showHideCheck = fileRows[0].querySelector(
-          'input.show-hide[type="checkbox"]');
-      assert.isNotOk(showHideCheck.checked);
+          'span.show-hide[role="switch"]');
+      const showHideLabel = showHideCheck.querySelector('.show-hide-icon');
+      assert.equal(showHideCheck.getAttribute('aria-checked'), 'false');
       MockInteractions.tap(showHideLabel);
-      assert.isOk(showHideCheck.checked);
+      assert.equal(showHideCheck.getAttribute('aria-checked'), 'true');
       assert.notEqual(
           element._expandedFiles.findIndex(f => f.path === 'myfile.txt'),
           -1);
@@ -1001,7 +1001,7 @@
 
       // Tap on a file to generate the diff.
       const row = dom(element.root)
-          .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
+          .querySelectorAll('.row:not(.header-row) span.show-hide')[0];
 
       MockInteractions.tap(row);
       flushAsynchronousOperations();
@@ -1079,20 +1079,22 @@
       const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
       const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
           'handleDiffUpdate');
+      const reInitStub = sandbox.stub(element.$.diffCursor,
+          'reInitAndUpdateStops');
 
       const path = 'path/to/my/file.txt';
       element._filesByPath = {[path]: {}};
       element.expandAllDiffs();
       flushAsynchronousOperations();
       assert.isTrue(element._showInlineDiffs);
-      assert.isTrue(cursorUpdateStub.calledOnce);
+      assert.isTrue(reInitStub.calledOnce);
       assert.equal(collapseStub.lastCall.args[0].length, 0);
 
       element.collapseAllDiffs();
       flushAsynchronousOperations();
       assert.equal(element._expandedFiles.length, 0);
       assert.isFalse(element._showInlineDiffs);
-      assert.isTrue(cursorUpdateStub.calledTwice);
+      assert.isTrue(cursorUpdateStub.calledOnce);
       assert.equal(collapseStub.lastCall.args[0].length, 1);
     });
 
@@ -1105,6 +1107,7 @@
         reload() {
           done();
         },
+        prefetchDiff() {},
         cancel() {},
         getCursorStops() { return []; },
         addEventListener(eventName, callback) {
@@ -1162,6 +1165,7 @@
       const diffs = [{
         path: 'p0',
         style: {},
+        prefetchDiff() {},
         reload() {
           assert.equal(callCount++, 2);
           return Promise.resolve();
@@ -1169,6 +1173,7 @@
       }, {
         path: 'p1',
         style: {},
+        prefetchDiff() {},
         reload() {
           assert.equal(callCount++, 1);
           return Promise.resolve();
@@ -1176,6 +1181,7 @@
       }, {
         path: 'p2',
         style: {},
+        prefetchDiff() {},
         reload() {
           assert.equal(callCount++, 0);
           return Promise.resolve();
@@ -1198,6 +1204,7 @@
       const diffs = [{
         path: 'p0',
         style: {},
+        prefetchDiff() {},
         reload() {
           assert.equal(reviewStub.callCount, 2);
           assert.equal(callCount++, 2);
@@ -1206,6 +1213,7 @@
       }, {
         path: 'p1',
         style: {},
+        prefetchDiff() {},
         reload() {
           assert.equal(reviewStub.callCount, 1);
           assert.equal(callCount++, 1);
@@ -1214,6 +1222,7 @@
       }, {
         path: 'p2',
         style: {},
+        prefetchDiff() {},
         reload() {
           assert.equal(reviewStub.callCount, 0);
           assert.equal(callCount++, 0);
@@ -1236,6 +1245,7 @@
       const diffs = [{
         path: 'p',
         style: {},
+        prefetchDiff() {},
         reload() { return Promise.resolve(); },
       }];
 
@@ -1565,6 +1575,7 @@
       });
       stub('gr-diff-host', {
         reload() { return Promise.resolve(); },
+        prefetchDiff() {},
       });
 
       // Element must be wrapped in an element with direct access to the
@@ -1641,7 +1652,7 @@
       assert.isTrue(diffStops[10].classList.contains('target-row'));
       assert.isFalse(diffStops[11].classList.contains('target-row'));
 
-      // The file cusor is now at 1.
+      // The file cursor is now at 1.
       assert.equal(element.$.fileCursor.index, 1);
       MockInteractions.keyUpOn(element, 73, null, 'i');
       flushAsynchronousOperations();
@@ -1652,7 +1663,7 @@
       const diffStopsFirst = diffs[0].getCursorStops();
       const diffStopsSecond = diffs[1].getCursorStops();
 
-      // The line on the first diff is stil selected
+      // The line on the first diff is still selected
       assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
       assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
     });
@@ -1683,7 +1694,7 @@
       assert.isFalse(diffStops[10].classList.contains('target-row'));
       assert.isTrue(diffStops[11].classList.contains('target-row'));
 
-      // The file cusor is still at 0.
+      // The file cursor is still at 0.
       assert.equal(element.$.fileCursor.index, 0);
     });
 
@@ -1707,7 +1718,6 @@
       test('n key with some files expanded and no shift key', () => {
         MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
         flushAsynchronousOperations();
-        assert.equal(nextChunkStub.callCount, 1);
 
         // Handle N key should return before calling diff cursor functions.
         MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
@@ -1715,21 +1725,21 @@
         assert.isFalse(nextCommentStub.called);
 
         // This is also called in diffCursor.moveToFirstChunk.
-        assert.equal(nextChunkStub.callCount, 2);
+        assert.equal(nextChunkStub.callCount, 1);
         assert.equal(element.filesExpanded, 'some');
       });
 
       test('n key with some files expanded and shift key', () => {
         MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
         flushAsynchronousOperations();
-        assert.equal(nextChunkStub.callCount, 1);
+        assert.equal(nextChunkStub.callCount, 0);
 
         MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
         assert.isTrue(nKeySpy.called);
         assert.isTrue(nextCommentStub.called);
 
         // This is also called in diffCursor.moveToFirstChunk.
-        assert.equal(nextChunkStub.callCount, 1);
+        assert.equal(nextChunkStub.callCount, 0);
         assert.equal(element.filesExpanded, 'some');
       });
 
@@ -1742,7 +1752,7 @@
         assert.isFalse(nextCommentStub.called);
 
         // This is also called in diffCursor.moveToFirstChunk.
-        assert.equal(nextChunkStub.callCount, 2);
+        assert.equal(nextChunkStub.callCount, 1);
         assert.isTrue(element._showInlineDiffs);
       });
 
@@ -1755,7 +1765,7 @@
         assert.isTrue(nextCommentStub.called);
 
         // This is also called in diffCursor.moveToFirstChunk.
-        assert.equal(nextChunkStub.callCount, 1);
+        assert.equal(nextChunkStub.callCount, 0);
         assert.isTrue(element._showInlineDiffs);
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index bd262ec..c42c734 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -26,7 +24,7 @@
 import {htmlTemplate} from './gr-included-in-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrIncludedInDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
index 2faac52..4670e6a 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -67,7 +67,7 @@
     }
   </style>
   <header>
-    <h1 id="title">Included In:</h1>
+    <h1 id="title" class="heading-1">Included In:</h1>
     <span class="closeButtonContainer">
       <gr-button id="closeButton" link="" on-click="_handleCloseTap"
         >Close</gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 8541840..f5742ea 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-selector/iron-selector.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../../styles/gr-voting-styles.js';
@@ -25,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-label-score-row_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabelScoreRow extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -150,6 +148,13 @@
     // Needed because when the selected item changes, it first changes to
     // nothing and then to the new item.
     if (!e.target.selectedItem) { return; }
+    for (const item of this.$.labelSelector.items) {
+      if (e.target.selectedItem === item) {
+        item.setAttribute('aria-checked', 'true');
+      } else {
+        item.removeAttribute('aria-checked');
+      }
+    }
     this._selectedValueText = e.target.selectedItem.getAttribute('title');
     // Needed to update the style of the selected button.
     this.updateStyles();
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
index 77148ad..fb0f9e8 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
@@ -53,7 +53,6 @@
           --button-background-color,
           var(--table-header-background-color)
         );
-        color: var(--primary-text-color);
         padding: 0 var(--spacing-m);
         @apply --vote-chip-styles;
       }
@@ -94,7 +93,9 @@
       }
     }
   </style>
-  <span class="labelNameCell">[[label.name]]</span>
+  <span class="labelNameCell" id="labelName" aria-hidden="true"
+    >[[label.name]]</span
+  >
   <div class="buttonsCell">
     <template
       is="dom-repeat"
@@ -109,9 +110,12 @@
       selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
       hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
       on-selected-item-changed="_setSelectedValueText"
+      role="radiogroup"
+      aria-labelledby="labelName"
     >
       <template is="dom-repeat" items="[[_items]]" as="value">
         <gr-button
+          role="radio"
           vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
           has-tooltip=""
           data-name$="[[label.name]]"
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 6e0a90d..ca4fac3 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -104,6 +104,20 @@
     sandbox.restore();
   });
 
+  function checkAriaCheckedValid() {
+    const items = element.$.labelSelector.items;
+    const selectedItem = element.selectedItem;
+    for (let i = 0; i < items.length; i++) {
+      const item = items[i];
+      if (items[i] === selectedItem) {
+        assert.isTrue(item.hasAttribute('aria-checked'), `item ${i}`);
+        assert.equal(item.getAttribute('aria-checked'), 'true', `item ${i}`);
+      } else {
+        assert.isFalse(item.hasAttribute('aria-checked'), `item ${i}`);
+      }
+    }
+  }
+
   test('label picker', () => {
     const labelsChangedHandler = sandbox.stub();
     element.addEventListener('labels-changed', labelsChangedHandler);
@@ -120,6 +134,7 @@
     const detail = labelsChangedHandler.args[0][0].detail;
     assert.equal(detail.name, 'Verified');
     assert.equal(detail.value, '-1');
+    checkAriaCheckedValid();
   });
 
   test('_computeVoteAttribute', () => {
@@ -163,6 +178,7 @@
             .textContent.trim(), '+1');
     assert.strictEqual(
         element.$.selectedValueLabel.textContent.trim(), 'good');
+    checkAriaCheckedValid();
   });
 
   test('do not display tooltips on touch devices', () => {
@@ -243,6 +259,7 @@
     assert.strictEqual(selector.selected, ' 0');
     assert.strictEqual(
         element.$.selectedValueLabel.textContent.trim(), 'No score');
+    checkAriaCheckedValid();
   });
 
   test('without permitted labels', () => {
@@ -268,7 +285,7 @@
     assert.isTrue(element.$.labelSelector.hidden);
   });
 
-  test('asymetrical labels', done => {
+  test('asymmetrical labels', done => {
     element.permittedLabels = {
       'Code-Review': [
         '-2',
@@ -339,6 +356,7 @@
     };
     flushAsynchronousOperations();
     assert.strictEqual(element.selectedValue, '-1');
+    checkAriaCheckedValid();
   });
 
   test('default_value is null if not permitted', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index 2d6825b..35ccc3c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-label-score-row/gr-label-score-row.js';
 import '../../../styles/shared-styles.js';
@@ -24,7 +22,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-label-scores_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabelScores extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 906ed34..e065008 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/iron-icon/iron-icon.js';
 import '../../shared/gr-account-label/gr-account-label.js';
@@ -26,16 +25,19 @@
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-voting-styles.js';
 import '../gr-comment-list/gr-comment-list.js';
+import {appContext} from '../../../services/app-context.js';
+import {ExperimentIds} from '../../../services/flags.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-message_html.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
 const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrMessage extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -62,6 +64,8 @@
 
   static get properties() {
     return {
+      /** @type {?} */
+      change: Object,
       changeNum: Number,
       /** @type {?} */
       message: Object,
@@ -69,6 +73,11 @@
         type: Object,
         computed: '_computeAuthor(message)',
       },
+      /**
+       * TODO(taoalpha): remove once the change log experiment is launched
+       *
+       * @type {Object} - a map on file and comments on it
+       */
       comments: {
         type: Object,
       },
@@ -122,11 +131,13 @@
       _messageContentCollapsed: {
         type: String,
         computed:
-            '_computeMessageContentCollapsed(message.message, message.tag)',
+            '_computeMessageContentCollapsed(message.message, message.tag,' +
+            ' message.commentThreads)',
       },
       _commentCountText: {
         type: Number,
-        computed: '_computeCommentCountText(comments)',
+        computed: '_computeCommentCountText(comments,'
+            + ' message.commentThreads.length, _isCleanerLogExperimentEnabled)',
       },
       _loggedIn: {
         type: Boolean,
@@ -140,6 +151,7 @@
         type: Boolean,
         value: false,
       },
+      _isCleanerLogExperimentEnabled: Boolean,
     };
   }
 
@@ -149,6 +161,11 @@
     ];
   }
 
+  constructor() {
+    super();
+    this.flagsService = appContext.flagsService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -159,6 +176,8 @@
   /** @override */
   ready() {
     super.ready();
+    this._isCleanerLogExperimentEnabled = this.flagsService
+        .isEnabled(ExperimentIds.CLEANER_CHANGELOG);
     this.$.restAPI.getConfig().then(config => {
       this.config = config;
     });
@@ -178,30 +197,75 @@
     }
   }
 
-  _computeCommentCountText(comments) {
-    if (!comments) return undefined;
-    let count = 0;
-    for (const file in comments) {
-      if (comments.hasOwnProperty(file)) {
-        const commentArray = comments[file] || [];
-        count += commentArray.length;
+  _computeCommentCountText(
+      comments, threadsLength, isCleanerLogExperimentEnabled) {
+    // TODO(taoalpha): clean up after cleaner-changelog experiment launched
+    if (isCleanerLogExperimentEnabled) {
+      if (threadsLength === 0) {
+        return undefined;
+      } else if (threadsLength === 1) {
+        return '1 comment';
+      } else {
+        return `${threadsLength} comments`;
+      }
+    } else {
+      if (!comments) return undefined;
+      let count = 0;
+      for (const file in comments) {
+        if (comments.hasOwnProperty(file)) {
+          const commentArray = comments[file] || [];
+          count += commentArray.length;
+        }
+      }
+      if (count === 0) {
+        return undefined;
+      } else if (count === 1) {
+        return '1 comment';
+      } else {
+        return `${count} comments`;
       }
     }
-    if (count === 0) {
-      return undefined;
-    } else if (count === 1) {
-      return '1 comment';
-    } else {
-      return `${count} comments`;
-    }
+  }
+
+  _onThreadListModified() {
+    // TODO(taoalpha): this won't propagate the changes to the files
+    // should consider replacing this with either top level events
+    // or gerrit level events
+
+    // emit the event so change-view can also get updated with latest changes
+    this.fire('comment-refresh');
   }
 
   _computeMessageContentExpanded(content, tag) {
     return this._computeMessageContent(content, tag, true);
   }
 
-  _computeMessageContentCollapsed(content, tag) {
-    return this._computeMessageContent(content, tag, false);
+  _patchsetCommentSummary(commentThreads) {
+    const id = this.message.id;
+    if (!id) return '';
+    const patchsetThreads = commentThreads.filter(thread =>
+      thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS);
+    for (const thread of patchsetThreads) {
+      // Find if there was a patchset level comment created through the reply
+      // dialog and use it to determine the summary
+      if (thread.comments[0].change_message_id === id) {
+        return thread.comments[0].message;
+      }
+    }
+    // Find if there is a reply to some patchset comment left
+    for (const thread of patchsetThreads) {
+      for (const comment of thread.comments) {
+        if (comment.change_message_id === id) { return comment.message; }
+      }
+    }
+    return '';
+  }
+
+  _computeMessageContentCollapsed(content, tag, commentThreads) {
+    const summary =
+      this._computeMessageContent(content, tag, false);
+    if (summary || !commentThreads) return summary;
+    return this._patchsetCommentSummary(commentThreads);
   }
 
   _computeMessageContent(content, tag, isExpanded) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index 753fd38..de4a72a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -66,6 +66,7 @@
       margin: 0 -4px;
     }
     .collapsed gr-comment-list,
+    .collapsed gr-thread-list,
     .collapsed .replyBtn,
     .collapsed .deleteBtn,
     .collapsed .hideOnCollapsed,
@@ -131,7 +132,7 @@
     }
     .score {
       border-radius: var(--border-radius);
-      color: var(--primary-text-color);
+      color: var(--vote-text-color);
       display: inline-block;
       padding: 0 var(--spacing-s);
       text-align: center;
@@ -217,40 +218,56 @@
             content="[[_messageContentExpanded]]"
             config="[[_projectConfig.commentlinks]]"
           ></gr-formatted-text>
-          <template is="dom-if" if="[[_messageContentExpanded]]">
-            <div
-              class="replyActionContainer"
-              hidden$="[[!showReplyButton]]"
-              hidden=""
-            >
-              <gr-button
-                class="replyBtn"
-                link=""
-                small=""
-                on-click="_handleReplyTap"
-              >
-                Reply
-              </gr-button>
-              <gr-button
-                disabled$="[[_isDeletingChangeMsg]]"
-                class="deleteBtn"
-                hidden$="[[!_isAdmin]]"
+          <template is="dom-if" if="[[_expanded]]">
+            <template is="dom-if" if="[[_messageContentExpanded]]">
+              <div
+                class="replyActionContainer"
+                hidden$="[[!showReplyButton]]"
                 hidden=""
-                link=""
-                small=""
-                on-click="_handleDeleteMessage"
               >
-                Delete
-              </gr-button>
-            </div>
+                <gr-button
+                  class="replyBtn"
+                  link=""
+                  small=""
+                  on-click="_handleReplyTap"
+                >
+                  Reply
+                </gr-button>
+                <gr-button
+                  disabled$="[[_isDeletingChangeMsg]]"
+                  class="deleteBtn"
+                  hidden$="[[!_isAdmin]]"
+                  hidden=""
+                  link=""
+                  small=""
+                  on-click="_handleDeleteMessage"
+                >
+                  Delete
+                </gr-button>
+              </div>
+            </template>
+            <template is="dom-if" if="[[!_isCleanerLogExperimentEnabled]]">
+              <gr-comment-list
+                comments="[[comments]]"
+                change-num="[[changeNum]]"
+                patch-num="[[message._revision_number]]"
+                project-name="[[projectName]]"
+                project-config="[[_projectConfig]]"
+              ></gr-comment-list>
+            </template>
+            <template is="dom-if" if="[[_isCleanerLogExperimentEnabled]]">
+              <gr-thread-list
+                change="[[change]]"
+                hidden$="[[!message.commentThreads.length]]"
+                threads="[[message.commentThreads]]"
+                change-num="[[changeNum]]"
+                logged-in="[[_loggedIn]]"
+                hide-toggle-buttons
+                on-thread-list-modified="_onThreadListModified"
+              >
+              </gr-thread-list>
+            </template>
           </template>
-          <gr-comment-list
-            comments="[[comments]]"
-            change-num="[[changeNum]]"
-            patch-num="[[message._revision_number]]"
-            project-name="[[projectName]]"
-            project-config="[[_projectConfig]]"
-          ></gr-comment-list>
         </div>
       </template>
       <template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
@@ -263,7 +280,7 @@
                 items="[[update.reviewers]]"
                 as="reviewer"
               >
-                <gr-account-chip account="[[reviewer]]"> </gr-account-chip>
+                <gr-account-link account="[[reviewer]]"> </gr-account-link>
               </template>
             </div>
           </template>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 78c2229..332ca52 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -87,7 +87,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       flushAsynchronousOperations();
@@ -105,7 +105,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       element.addEventListener('change-message-deleted', e => {
@@ -252,6 +252,8 @@
         const original = 'Uploaded patch set 1.';
         const tag = 'autogenerated:gerrit:newPatchSet';
         let actual = element._computeMessageContent(original, tag, true);
+        assert.equal(actual, element._computeMessageContentCollapsed(
+            original, tag, []));
         assert.equal(actual, original);
         actual = element._computeMessageContent(original, tag, false);
         assert.equal(actual, original);
@@ -263,6 +265,8 @@
         const expected = 'Patch Set 26 was rebased';
         let actual = element._computeMessageContent(original, tag, true);
         assert.equal(actual, expected);
+        assert.equal(actual, element._computeMessageContentCollapsed(
+            original, tag, []));
         actual = element._computeMessageContent(original, tag, false);
         assert.equal(actual, expected);
       });
@@ -273,6 +277,8 @@
         const expected = 'This change is ready for review.';
         let actual = element._computeMessageContent(original, tag, true);
         assert.equal(actual, expected);
+        assert.equal(actual, element._computeMessageContentCollapsed(
+            original, tag, []));
         actual = element._computeMessageContent(original, tag, false);
         assert.equal(actual, expected);
       });
@@ -378,7 +384,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       flushAsynchronousOperations();
@@ -391,6 +397,68 @@
     });
   });
 
+  suite('patchset comment summary', () => {
+    setup(() => {
+      element = fixture('basic');
+      element.message = {id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3'};
+    });
+
+    test('single patchset comment posted', () => {
+      const threads = [{
+        comments: [{
+          __path: '/PATCHSET_LEVEL',
+          change_message_id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3',
+          patch_set: 1,
+          id: 'e365b138_bed65caa',
+          updated: '2020-05-15 13:35:56.000000000',
+          message: 'testing the load',
+          unresolved: false,
+          path: '/PATCHSET_LEVEL',
+          collapsed: false,
+        }],
+        patchNum: 1,
+        path: '/PATCHSET_LEVEL',
+        rootId: 'e365b138_bed65caa',
+      }];
+      assert.equal(element._computeMessageContentCollapsed(
+          '', undefined, threads), 'testing the load');
+      assert.equal(element._computeMessageContent('', undefined, false), '');
+    });
+
+    test('single patchset comment with reply', () => {
+      const threads = [{
+        comments: [{
+          __path: '/PATCHSET_LEVEL',
+          patch_set: 1,
+          id: 'e365b138_bed65caa',
+          updated: '2020-05-15 13:35:56.000000000',
+          message: 'testing the load',
+          unresolved: false,
+          path: '/PATCHSET_LEVEL',
+          collapsed: false,
+        }, {
+          __path: '/PATCHSET_LEVEL',
+          change_message_id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3',
+          patch_set: 1,
+          id: 'd6efcc85_4cbbb6f4',
+          in_reply_to: 'e365b138_bed65caa',
+          updated: '2020-05-15 16:55:28.000000000',
+          message: 'n',
+          unresolved: false,
+          path: '/PATCHSET_LEVEL',
+          __draft: true,
+          collapsed: true,
+        }],
+        patchNum: 1,
+        path: '/PATCHSET_LEVEL',
+        rootId: 'e365b138_bed65caa',
+      }];
+      assert.equal(element._computeMessageContentCollapsed(
+          '', undefined, threads), 'n');
+      assert.equal(element._computeMessageContent('', undefined, false), '');
+    });
+  });
+
   suite('when logged in but not admin', () => {
     setup(done => {
       stub('gr-rest-api-interface', {
@@ -414,7 +482,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       flushAsynchronousOperations();
@@ -444,7 +512,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'not empty',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
       flushAsynchronousOperations();
       replyEl = element.shadowRoot.querySelector('.replyActionContainer');
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
index c888fcf..ae3a7d2 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
@@ -14,11 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
 import '../gr-message/gr-message.js';
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -28,7 +26,9 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-messages-list-experimental_html.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
+import {MessageTag} from '../../../constants/constants.js';
+import {appContext} from '../../../services/app-context.js';
 
 /**
  * The content of the enum is also used in the UI for the button text.
@@ -41,7 +41,115 @@
 };
 
 /**
- * @extends Polymer.Element
+ * Computes message author's comments for this change message. The backend
+ * sets comment.change_message_id for matching, so this computation is fairly
+ * straightforward.
+ */
+function computeThreads(message, allMessages, changeComments) {
+  if ([message, allMessages, changeComments].some(arg => arg === undefined)) {
+    return [];
+  }
+  if (message._index === undefined) {
+    return [];
+  }
+
+  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 === message.id;
+        // 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;
+      })
+  );
+}
+
+/**
+ * If messages have the same tag, then that influences grouping and whether
+ * a message is initally hidden or not, see isImportant(). So we are applying
+ * some "magic" rules here in order to hide exactly the right messages.
+ *
+ * 1. If a message does not have a tag, but is associated with robot comments,
+ * then it gets a tag.
+ *
+ * 2. Use the same tag for some of Gerrit's standard events, if they should be
+ * considered one group, e.g. normal and wip patchset uploads.
+ *
+ * 3. Everything beyond the ~ character is cut off from the tag. That gives
+ * tools control over which messages will be hidden.
+ */
+function computeTag(message) {
+  if (!message.tag) {
+    const threads = message.commentThreads || [];
+    const comments = threads.map(
+        t => t.comments.find(c => c.change_message_id === message.id));
+    const isRobot = comments.some(c => c && !!c.robot_id);
+    return isRobot ? 'autogenerated:has-robot-comments' : undefined;
+  }
+
+  if (message.tag === MessageTag.TAG_NEW_WIP_PATCHSET) {
+    return MessageTag.TAG_NEW_PATCHSET;
+  }
+  if (message.tag === MessageTag.TAG_UNSET_ASSIGNEE) {
+    return MessageTag.TAG_SET_ASSIGNEE;
+  }
+  if (message.tag === MessageTag.TAG_UNSET_PRIVATE) {
+    return MessageTag.TAG_SET_PRIVATE;
+  }
+  if (message.tag === MessageTag.TAG_SET_WIP) {
+    return MessageTag.TAG_SET_READY;
+  }
+
+  return message.tag.replace(/~.*/, '');
+}
+
+/**
+ * Try to set a revision number that makes sense, if none is set. Just copy
+ * over the revision number of the next older message. This is mostly relevant
+ * for reviewer updates. Other messages should typically have the revision
+ * number already set.
+ */
+function computeRevision(message, allMessages) {
+  if (message._revision_number > 0) return message._revision_number;
+  let revision = 0;
+  for (const m of allMessages) {
+    if (m.date > message.date) break;
+    if (m._revision_number > revision) revision = m._revision_number;
+  }
+  return revision > 0 ? revision : undefined;
+}
+
+/**
+ * Unimportant messages are initially hidden.
+ *
+ * Human messages are always important. They have an undefined tag.
+ *
+ * Autogenerated messages are unimportant, if there is a message with the same
+ * tag and a higher revision number.
+ */
+function computeIsImportant(message, allMessages) {
+  if (!message.tag) return true;
+
+  const hasSameTag = m => m.tag === message.tag;
+  const revNumber = message._revision_number || 0;
+  const hasHigherRevisionNumber = m => m._revision_number > revNumber;
+  return !allMessages.filter(hasSameTag).some(hasHigherRevisionNumber);
+}
+
+export const TEST_ONLY = {
+  computeThreads,
+  computeTag,
+  computeRevision,
+  computeIsImportant,
+};
+
+/**
+ * @extends PolymerElement
  */
 class GrMessagesListExperimental extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -54,6 +162,8 @@
 
   static get properties() {
     return {
+      /** @type {?} */
+      change: Object,
       changeNum: Number,
       /**
        * These are just the change messages. They are combined with reviewer
@@ -95,17 +205,18 @@
         computed: '_computeExpandAllTitle(_expandAllState)',
       },
 
-      _hideAutomated: {
+      _showAllActivity: {
         type: Boolean,
         value: false,
-        observer: '_hideAutomatedChanged',
+        observer: '_observeShowAllActivity',
       },
       /**
        * The merged array of change messages and reviewer updates.
        */
       _combinedMessages: {
         type: Array,
-        computed: '_computeCombinedMessages(messages, reviewerUpdates)',
+        computed: '_computeCombinedMessages(messages, reviewerUpdates, '
+            + 'changeComments)',
         observer: '_combinedMessagesChanged',
       },
 
@@ -116,16 +227,21 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   scrollToMessage(messageID) {
     const selector = `[data-message-id="${messageID}"]`;
     const el = this.shadowRoot.querySelector(selector);
 
-    if (!el && !this._hideAutomated) {
+    if (!el && this._showAllActivity) {
       console.warn(`Failed to scroll to message: ${messageID}`);
       return;
     }
     if (!el) {
-      this._hideAutomated = false;
+      this._showAllActivity = true;
       setTimeout(() => this.scrollToMessage(messageID));
       return;
     }
@@ -141,15 +257,7 @@
     this._highlightEl(el);
   }
 
-  _isAutomated(message) {
-    const isReviewerUpdate =
-        !!(message.reviewer || message.type === 'REVIEWER_UPDATE');
-    const isAutoGenerated =
-        !!(message.tag && message.tag.startsWith('autogenerated'));
-    return isReviewerUpdate || isAutoGenerated;
-  }
-
-  _hideAutomatedChanged(hideAutomated) {
+  _observeShowAllActivity(showAllActivity) {
     // We have to call render() such that the dom-repeat filter picks up the
     // change.
     this.$.messageRepeat.render();
@@ -159,15 +267,17 @@
    * Filter for the dom-repeat of combinedMessages.
    */
   _isMessageVisible(message) {
-    return !(this._hideAutomated && this._isAutomated(message));
+    return this._showAllActivity || message.isImportant;
   }
 
   /**
-   * Merges change messages and reviewer updates into one array.
+   * Merges change messages and reviewer updates into one array. Also processes
+   * all messages and updates, aligns or massages some of the properties.
    */
-  _computeCombinedMessages(messages, reviewerUpdates) {
-    messages = messages || [];
-    reviewerUpdates = reviewerUpdates || [];
+  _computeCombinedMessages(messages, reviewerUpdates, changeComments) {
+    const params = [messages, reviewerUpdates, changeComments];
+    if (params.some(o => o === undefined)) return [];
+
     let mi = 0;
     let ri = 0;
     let combinedMessages = [];
@@ -186,8 +296,8 @@
         combinedMessages = combinedMessages.concat(messages.slice(mi));
         break;
       }
-      mDate = mDate || util.parseDate(messages[mi].date);
-      rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
+      mDate = mDate || parseDate(messages[mi].date);
+      rDate = rDate || parseDate(reviewerUpdates[ri].date);
       if (rDate < mDate) {
         combinedMessages.push(reviewerUpdates[ri++]);
         rDate = null;
@@ -200,6 +310,15 @@
       if (m.expanded === undefined) {
         m.expanded = false;
       }
+      m.commentThreads = computeThreads(m, combinedMessages, changeComments);
+      m._revision_number = computeRevision(m, combinedMessages);
+      m.tag = computeTag(m);
+    });
+    // computeIsImportant() depends on tags and revision numbers already being
+    // updated for all messages, so we have to compute this in its own forEach
+    // loop.
+    combinedMessages.forEach(m => {
+      m.isImportant = computeIsImportant(m, combinedMessages);
     });
     return combinedMessages;
   }
@@ -228,8 +347,8 @@
   _highlightEl(el) {
     const highlightedEls =
         dom(this.root).querySelectorAll('.highlighted');
-    for (const highlighedEl of highlightedEls) {
-      highlighedEl.classList.remove('highlighted');
+    for (const highlightedEl of highlightedEls) {
+      highlightedEl.classList.remove('highlighted');
     }
     function handleAnimationEnd() {
       el.removeEventListener('animationend', handleAnimationEnd);
@@ -258,44 +377,12 @@
     this.scrollToMessage(e.detail.id);
   }
 
-  _hasAutomatedMessages(messages) {
-    if (!messages) { return false; }
-    for (const message of messages) {
-      if (this._isAutomated(message)) {
-        return true;
-      }
-    }
-    return false;
+  _isVisibleShowAllActivityToggle(messages = []) {
+    return messages.some(m => !m.isImportant);
   }
 
-  /**
-   * Computes message author's file comments for change's message. The backend
-   * sets comment.change_message_id for matching, so this computation is fairly
-   * straightforward.
-   *
-   * @param {!Object} changeComments changeComment object, which includes
-   *     a method to get all published comments (including robot comments),
-   *     which returns a Hash of arrays of comments, filename as key.
-   * @param {!Object} message
-   * @return {!Object} Hash of arrays of comments, filename as key.
-   */
-  _computeCommentsForMessage(changeComments, message) {
-    if ([changeComments, message].some(arg => arg === undefined)) {
-      return {};
-    }
-    const comments = changeComments.getAllPublishedComments();
-    if (message._index === undefined || !comments || !this.messages) {
-      return {};
-    }
-    const idFilter = comment => comment.change_message_id === message.id;
-
-    const msgComments = {};
-    for (const file in comments) {
-      if (!comments.hasOwnProperty(file)) { continue; }
-      const filtered = comments[file].filter(idFilter);
-      if (filtered.length) msgComments[file] = filtered;
-    }
-    return msgComments;
+  _computeHiddenEntriesCount(messages = []) {
+    return messages.filter(m => !m.isImportant).length;
   }
 
   /**
@@ -311,7 +398,7 @@
         acc[val] = (acc[val] || 0) + 1;
         return acc;
       }, {all: combinedMessages.length});
-      this.$.reporting.reportInteraction('messages-count', tagsCounted);
+      this.reporting.reportInteraction('messages-count', tagsCounted);
     }
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
index 540418b..56cb960 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
@@ -45,29 +45,55 @@
       align-items: center;
       display: flex;
     }
+    .hiddenEntries {
+      color: var(--deemphasized-text-color);
+    }
     gr-message:not(:last-of-type) {
       border-bottom: 1px solid var(--border-color);
     }
-    gr-message:nth-child(2n) {
+    gr-message {
       background-color: var(--background-color-secondary);
     }
-    gr-message:nth-child(2n + 1) {
-      background-color: var(--background-color-tertiary);
+    .experimentMessage {
+      padding: var(--spacing-s) var(--spacing-m);
+      background-color: var(--emphasis-color);
+      border-radius: var(--border-radius);
+    }
+    .experimentMessage iron-icon {
+      vertical-align: top;
     }
   </style>
   <div class="header">
-    <span
-      id="automatedMessageToggleContainer"
-      class="container"
-      hidden$="[[!_hasAutomatedMessages(messages)]]"
-    >
-      <paper-toggle-button
-        id="automatedMessageToggle"
-        checked="{{_hideAutomated}}"
-      ></paper-toggle-button
-      >Only comments
-      <span class="transparent separator"></span>
-    </span>
+    <div id="showAllActivityToggleContainer" class="container">
+      <template
+        is="dom-if"
+        if="[[_isVisibleShowAllActivityToggle(_combinedMessages)]]"
+      >
+        <paper-toggle-button
+          class="showAllActivityToggle"
+          checked="{{_showAllActivity}}"
+          aria-labelledby="showAllEntriesLabel"
+          role="switch"
+        ></paper-toggle-button>
+        <div id="showAllEntriesLabel">
+          <span>Show all entries</span>
+          <span class="hiddenEntries" hidden$="[[_showAllActivity]]">
+            ([[_computeHiddenEntriesCount(_combinedMessages)]] hidden)
+          </span>
+        </div>
+        <span class="transparent separator"></span>
+      </template>
+    </div>
+    <div class="experimentMessage">
+      <iron-icon icon="gr-icons:pets"></iron-icon>
+      <span>You're currently viewing an experimental Change Log view.</span>
+      <a
+        target="_blank"
+        href="https://www.gerritcodereview.com/2020-05-06-change-log-experiment.html"
+      >
+        Learn more
+      </a>
+    </div>
     <gr-button
       id="collapse-messages"
       link=""
@@ -85,9 +111,9 @@
     filter="_isMessageVisible"
   >
     <gr-message
+      change="[[change]]"
       change-num="[[changeNum]]"
       message="[[message]]"
-      comments="[[_computeCommentsForMessage(changeComments, message)]]"
       project-name="[[projectName]]"
       show-reply-button="[[showReplyButtons]]"
       on-message-anchor-tap="_handleAnchorClick"
@@ -95,5 +121,4 @@
       data-message-id$="[[message.id]]"
     ></gr-message>
   </template>
-  <gr-reporting id="reporting" category="message-list"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
index 9c22ab5..c520b05 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
@@ -46,8 +46,11 @@
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import './gr-messages-list-experimental.js';
-import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {TEST_ONLY} from './gr-messages-list-experimental.js';
+import {MessageTag} from '../../../constants/constants.js';
+
 const randomMessage = function(opt_params) {
   const params = opt_params || {};
   const author1 = {
@@ -61,14 +64,10 @@
     message: params.message || Math.random().toString(),
     _revision_number: params._revision_number || 1,
     author: params.author || author1,
+    tag: params.tag,
   };
 };
 
-const randomAutomated = function(opt_params) {
-  return Object.assign({tag: 'autogenerated:gerrit:replace'},
-      randomMessage(opt_params));
-};
-
 suite('gr-messages-list-experimental tests', () => {
   let element;
   let messages;
@@ -89,61 +88,53 @@
     email: 'marvin@sirius.org',
   };
 
+  const createComment = function() {
+    return {
+      id: '1a2b3c4d',
+      message: 'some random test text',
+      change_message_id: '8a7b6c5d',
+      updated: '2016-01-01 01:02:03.000000000',
+      line: 1,
+      patch_set: 1,
+      author,
+    };
+  };
+
   const comments = {
     file1: [
       {
-        message: 'message text',
+        ...createComment(),
         change_message_id: MESSAGE_ID_0,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: '6505d749_f0bec0aa',
-        line: 62,
-        id: '6505d749_10ed44b2',
-        patch_set: 2,
         author: {
           email: 'some@email.com',
           _account_id: 123,
         },
       },
       {
-        message: 'message text',
+        ...createComment(),
+        id: '2b3c4d5e',
         change_message_id: MESSAGE_ID_1,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: 'c5912363_6b820105',
-        line: 42,
-        id: '450a935e_0f1c05db',
-        patch_set: 2,
-        author,
       },
       {
-        message: 'message text',
+        ...createComment(),
+        id: '2b3c4d5e',
         change_message_id: MESSAGE_ID_1,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: '6505d749_f0bec0aa',
-        line: 62,
-        id: '6505d749_10ed44b2',
-        patch_set: 2,
-        author,
       },
       {
-        message: 'message text',
-        change_message_id: MESSAGE_ID_2,
-        updated: '2016-09-27 00:18:03.000000000',
-        line: 64,
+        ...createComment(),
         id: '34ed05d749_10ed44b2',
-        patch_set: 2,
-        author,
+        change_message_id: MESSAGE_ID_2,
       },
     ],
     file2: [
       {
-        message: 'message text',
+        ...createComment(),
         change_message_id: MESSAGE_ID_1,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: 'c5912363_4b7d450a',
-        line: 132,
         id: '450a935e_4f260d25',
-        patch_set: 2,
-        author,
       },
     ],
   };
@@ -215,9 +206,11 @@
       assert.isTrue([...getMessages()].filter(m => m._expanded).length == 0);
     });
 
-    test('hide messages does not appear when no automated messages', () => {
+    test('showAllActivity does not appear when all msgs are important', () => {
       assert.isOk(element.shadowRoot
-          .querySelector('#automatedMessageToggleContainer[hidden]'));
+          .querySelector('#showAllActivityToggleContainer'));
+      assert.isNotOk(element.shadowRoot
+          .querySelector('.showAllActivityToggle'));
     });
 
     test('scroll to message', () => {
@@ -265,7 +258,7 @@
               ._expanded);
     });
 
-    test('messages', () => {
+    test('associating messages with comments', () => {
       const messages = [].concat(
           randomMessage(),
           {
@@ -286,25 +279,156 @@
           }
       );
       element.messages = messages;
-      const isAuthor = function(author, comment) {
-        return comment.author._account_id === author._account_id;
-      };
-      const isMarvin = isAuthor.bind(null, author);
       flushAsynchronousOperations();
       const messageElements = getMessages();
       assert.equal(messageElements.length, messages.length);
       assert.deepEqual(messageElements[1].message, messages[1]);
       assert.deepEqual(messageElements[2].message, messages[2]);
-      assert.deepEqual(messageElements[1].comments.file1,
-          comments.file1.filter(isMarvin).filter(
-              c => c.change_message_id === messages[1].id));
-      assert.deepEqual(messageElements[1].comments.file2,
-          comments.file2.filter(isMarvin).filter(
-              c => c.change_message_id === messages[1].id));
-      assert.deepEqual(messageElements[2].comments.file1,
-          comments.file1.filter(isMarvin).filter(
-              c => c.change_message_id === messages[2].id));
-      assert.isUndefined(messageElements[2].comments.file2);
+    });
+
+    test('threads', () => {
+      const messages = [
+        {
+          _index: 5,
+          _revision_number: 4,
+          message: 'Uploaded patch set 4.',
+          date: '2016-09-28 13:36:33.000000000',
+          author,
+          id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+        },
+      ];
+      element.messages = messages;
+      flushAsynchronousOperations();
+      const messageElements = getMessages();
+      // threads
+      assert.equal(
+          messageElements[0].message.commentThreads.length,
+          3);
+      // first thread contains 1 comment
+      assert.equal(
+          messageElements[0].message.commentThreads[0].comments.length,
+          1);
+    });
+
+    test('updateTag human message', () => {
+      const m = randomMessage();
+      assert.equal(TEST_ONLY.computeTag(m), undefined);
+    });
+
+    test('updateTag nothing to change', () => {
+      const m = randomMessage();
+      const tag = 'something-normal';
+      m.tag = tag;
+      assert.equal(TEST_ONLY.computeTag(m), tag);
+    });
+
+    test('updateTag TAG_NEW_WIP_PATCHSET', () => {
+      const m = randomMessage();
+      m.tag = MessageTag.TAG_NEW_WIP_PATCHSET;
+      assert.equal(TEST_ONLY.computeTag(m), MessageTag.TAG_NEW_PATCHSET);
+    });
+
+    test('updateTag remove postfix', () => {
+      const m = randomMessage();
+      m.tag = 'something~withpostfix';
+      assert.equal(TEST_ONLY.computeTag(m), 'something');
+    });
+
+    test('updateTag with robot comments', () => {
+      const m = randomMessage();
+      m.commentThreads = [{
+        comments: [{
+          robot_id: 'id314',
+          change_message_id: m.id,
+        }],
+      }];
+      assert.notEqual(TEST_ONLY.computeTag(m), undefined);
+    });
+
+    test('setRevisionNumber nothing to change', () => {
+      const m1 = randomMessage();
+      const m2 = randomMessage();
+      assert.equal(TEST_ONLY.computeRevision(m1, [m1, m2]), 1);
+      assert.equal(TEST_ONLY.computeRevision(m2, [m1, m2]), 1);
+    });
+
+    test('setRevisionNumber reviewer updates', () => {
+      const m1 = randomMessage(
+          {
+            tag: MessageTag.TAG_REVIEWER_UPDATE,
+            date: '2020-01-01 10:00:00.000000000',
+          });
+      m1._revision_number = undefined;
+      const m2 = randomMessage(
+          {
+            date: '2020-01-02 10:00:00.000000000',
+          });
+      m2._revision_number = 1;
+      const m3 = randomMessage(
+          {
+            tag: MessageTag.TAG_REVIEWER_UPDATE,
+            date: '2020-01-03 10:00:00.000000000',
+          });
+      m3._revision_number = undefined;
+      const m4 = randomMessage(
+          {
+            date: '2020-01-04 10:00:00.000000000',
+          });
+      m4._revision_number = 2;
+      const m5 = randomMessage(
+          {
+            tag: MessageTag.TAG_REVIEWER_UPDATE,
+            date: '2020-01-05 10:00:00.000000000',
+          });
+      m5._revision_number = undefined;
+      const allMessages = [m1, m2, m3, m4, m5];
+      assert.equal(TEST_ONLY.computeRevision(m1, allMessages), undefined);
+      assert.equal(TEST_ONLY.computeRevision(m2, allMessages), 1);
+      assert.equal(TEST_ONLY.computeRevision(m3, allMessages), 1);
+      assert.equal(TEST_ONLY.computeRevision(m4, allMessages), 2);
+      assert.equal(TEST_ONLY.computeRevision(m5, allMessages), 2);
+    });
+
+    test('isImportant human message', () => {
+      const m = randomMessage();
+      assert.isTrue(TEST_ONLY.computeIsImportant(m, []));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m, [m]));
+    });
+
+    test('isImportant even with a tag', () => {
+      const m1 = randomMessage();
+      const m2 = randomMessage({tag: 'autogenerated:gerrit1'});
+      const m3 = randomMessage({tag: 'autogenerated:gerrit2'});
+      assert.isTrue(TEST_ONLY.computeIsImportant(m2, []));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2, m3]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m2, [m1, m2, m3]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
+    });
+
+    test('isImportant filters same tag and older revision', () => {
+      const m1 = randomMessage({tag: 'auto', _revision_number: 2});
+      const m2 = randomMessage({tag: 'auto', _revision_number: 1});
+      const m3 = randomMessage({tag: 'auto'});
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m2, [m2]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m2, [m1, m2]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m3]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m3]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2, m3]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m2, [m1, m2, m3]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
+    });
+
+    test('isImportant is evaluated after tag update', () => {
+      const m1 = randomMessage(
+          {tag: MessageTag.TAG_NEW_PATCHSET, _revision_number: 1});
+      const m2 = randomMessage(
+          {tag: MessageTag.TAG_NEW_WIP_PATCHSET, _revision_number: 2});
+      element.messages = [m1, m2];
+      flushAsynchronousOperations();
+      assert.isFalse(m1.isImportant);
+      assert.isTrue(m2.isImportant);
     });
 
     test('messages without author do not throw', () => {
@@ -329,18 +453,6 @@
     let sandbox;
     let commentApiWrapper;
 
-    const getMessages = function() {
-      return dom(element.root).querySelectorAll('gr-message');
-    };
-    const getHiddenMessages = function() {
-      return dom(element.root).querySelectorAll('gr-message[hidden]');
-    };
-
-    const randomMessageReviewer = {
-      reviewer: {},
-      date: '2016-01-13 20:30:33.038000',
-    };
-
     setup(() => {
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
@@ -351,8 +463,11 @@
       });
 
       sandbox = sinon.sandbox.create();
-      messages = _.times(2, randomAutomated);
-      messages.push(randomMessageReviewer);
+      messages = [
+        randomMessage(),
+        randomMessage({tag: 'auto', _revision_number: 2}),
+        randomMessage({tag: 'auto', _revision_number: 3}),
+      ];
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
@@ -371,41 +486,34 @@
     });
 
     test('hide autogenerated button is not hidden', () => {
-      assert.isNotOk(element.shadowRoot
-          .querySelector('#automatedMessageToggle[hidden]'));
+      const toggle = dom(element.root).querySelector('.showAllActivityToggle');
+      assert.isOk(toggle);
     });
 
-    test('autogenerated messages are not hidden initially', () => {
-      const allHiddenMessageEls = getHiddenMessages();
-
-      // There are no hidden messages.
-      assert.isFalse(!!allHiddenMessageEls.length);
+    test('one unimportant message is hidden initially', () => {
+      const displayedMsgs = dom(element.root).querySelectorAll('gr-message');
+      assert.equal(displayedMsgs.length, 2);
     });
 
-    test('autogenerated messages hidden after comments only toggle', () => {
-      let allHiddenMessageEls = getHiddenMessages();
-
-      element._hideAutomated = false;
-      MockInteractions.tap(element.$.automatedMessageToggle);
+    test('unimportant messages hidden after toggle', () => {
+      element._showAllActivity = true;
+      const toggle = dom(element.root).querySelector('.showAllActivityToggle');
+      assert.isOk(toggle);
+      MockInteractions.tap(toggle);
       flushAsynchronousOperations();
-      const allMessageEls = getMessages();
-      allHiddenMessageEls = getHiddenMessages();
-
-      // Autogenerated messages are now hidden.
-      assert.equal(allHiddenMessageEls.length, allMessageEls.length);
+      const displayedMsgs = dom(element.root).querySelectorAll('gr-message');
+      assert.equal(displayedMsgs.length, 2);
     });
 
-    test('autogenerated messages not hidden after comments only toggle',
-        () => {
-          let allHiddenMessageEls = getHiddenMessages();
-
-          element._hideAutomated = true;
-          MockInteractions.tap(element.$.automatedMessageToggle);
-          allHiddenMessageEls = getHiddenMessages();
-
-          // Autogenerated messages are now hidden.
-          assert.isFalse(!!allHiddenMessageEls.length);
-        });
+    test('unimportant messages shown after toggle', () => {
+      element._showAllActivity = false;
+      const toggle = dom(element.root).querySelector('.showAllActivityToggle');
+      assert.isOk(toggle);
+      MockInteractions.tap(toggle);
+      flushAsynchronousOperations();
+      const displayedMsgs = dom(element.root).querySelectorAll('gr-message');
+      assert.equal(displayedMsgs.length, 3);
+    });
 
     test('_computeLabelExtremes', () => {
       const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 8df8566..adf9fd3 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../gr-message/gr-message.js';
 import '../../../styles/shared-styles.js';
@@ -28,7 +25,8 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-messages-list_html.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
+import {appContext} from '../../../services/app-context.js';
 
 const MAX_INITIAL_SHOWN_MESSAGES = 20;
 const MESSAGES_INCREMENT = 5;
@@ -49,7 +47,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrMessagesList extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -122,6 +120,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   scrollToMessage(messageID) {
     let el = this.shadowRoot
         .querySelector('[data-message-id="' + messageID + '"]');
@@ -188,8 +191,8 @@
         result = result.concat(messages.slice(mi));
         break;
       }
-      mDate = mDate || util.parseDate(messages[mi].date);
-      rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
+      mDate = mDate || parseDate(messages[mi].date);
+      rDate = rDate || parseDate(reviewerUpdates[ri].date);
       if (rDate < mDate) {
         result.push(reviewerUpdates[ri++]);
         rDate = null;
@@ -238,8 +241,8 @@
   _highlightEl(el) {
     const highlightedEls =
         dom(this.root).querySelectorAll('.highlighted');
-    for (const highlighedEl of highlightedEls) {
-      highlighedEl.classList.remove('highlighted');
+    for (const highlightedEl of highlightedEls) {
+      highlightedEl.classList.remove('highlighted');
     }
     function handleAnimationEnd() {
       el.removeEventListener('animationend', handleAnimationEnd);
@@ -300,14 +303,14 @@
     const messages = this.messages || [];
     const index = message._index;
     const authorId = message.author && message.author._account_id;
-    const mDate = util.parseDate(message.date).getTime();
+    const mDate = parseDate(message.date).getTime();
     // NB: Messages array has oldest messages first.
     let nextMDate;
     if (index > 0) {
       for (let i = index - 1; i >= 0; i--) {
         if (messages[i] && messages[i].author &&
             messages[i].author._account_id === authorId) {
-          nextMDate = util.parseDate(messages[i].date).getTime();
+          nextMDate = parseDate(messages[i].date).getTime();
           break;
         }
       }
@@ -321,7 +324,7 @@
             fileComments[i].author._account_id !== authorId) {
           continue;
         }
-        const cDate = util.parseDate(fileComments[i].updated).getTime();
+        const cDate = parseDate(fileComments[i].updated).getTime();
         if (cDate <= mDate) {
           if (nextMDate && cDate <= nextMDate) {
             continue;
@@ -401,7 +404,7 @@
 
   _handleShowAllTap() {
     this._visibleMessages = this._processedMessages;
-    this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
+    this.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
   }
 
   _handleIncrementShownMessages() {
@@ -411,7 +414,7 @@
     const newMessages = this._processedMessages.slice(-(len + delta), -len);
     // Add newMessages to the beginning of _visibleMessages
     this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
-    this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
+    this.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
   }
 
   _processedMessagesChanged(messages) {
@@ -425,7 +428,7 @@
         acc[val] = (acc[val] || 0) + 1;
         return acc;
       }, {all: messages.length});
-      this.$.reporting.reportInteraction('messages-count', tagsCounted);
+      this.reporting.reportInteraction('messages-count', tagsCounted);
     }
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
index 94ae1b0..5adfc53 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
@@ -79,8 +79,10 @@
       <paper-toggle-button
         id="automatedMessageToggle"
         checked="{{_hideAutomated}}"
+        aria-labelledby="onlyCommentsLabel"
+        role="switch"
       ></paper-toggle-button
-      >Only comments
+      ><span id="onlyCommentsLabel">Only comments</span>
       <span class="transparent separator"></span>
     </span>
     <gr-button
@@ -128,5 +130,4 @@
       data-message-id$="[[message.id]]"
     ></gr-message>
   </template>
-  <gr-reporting id="reporting" category="message-list"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 80896aa..4a0f3f1 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -46,7 +46,7 @@
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import './gr-messages-list.js';
-import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 const randomMessage = function(opt_params) {
   const params = opt_params || {};
@@ -400,9 +400,15 @@
       assert.deepEqual(messageElements[1].message, messages[1]);
       assert.deepEqual(messageElements[2].message, messages[2]);
       assert.deepEqual(messageElements[1].comments.file1,
-          comments.file1.filter(isMarvin));
+          comments.file1.filter(isMarvin).map(c => {
+            return {...c,
+              path: 'file1'};
+          }));
       assert.deepEqual(messageElements[1].comments.file2,
-          comments.file2.filter(isMarvin));
+          comments.file2.filter(isMarvin).map(c => {
+            return {...c,
+              path: 'file2'};
+          }));
       assert.deepEqual(messageElements[2].comments, {});
     });
 
@@ -550,7 +556,7 @@
     });
 
     test('initially show only 20 messages', () => {
-      sandbox.stub(element.$.reporting, 'reportInteraction',
+      sandbox.stub(element.reporting, 'reportInteraction',
           (eventName, details) => {
             assert.equal(typeof(eventName), 'string');
             if (details) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 631542a..8d72d21 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -27,9 +25,10 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRelatedChangesList extends mixinBehaviors( [
   PatchSetBehavior,
@@ -276,7 +275,7 @@
 
   _computeLinkClass(change) {
     const statuses = [];
-    if (change.status == this.ChangeStatus.ABANDONED) {
+    if (change.status == ChangeStatus.ABANDONED) {
       statuses.push('strikethrough');
     }
     if (change.submittable) {
@@ -293,7 +292,7 @@
       classes.push('indirectAncestor');
     } else if (change.submittable) {
       classes.push('submittable');
-    } else if (change.status == this.ChangeStatus.NEW) {
+    } else if (change.status == ChangeStatus.NEW) {
       classes.push('hidden');
     }
     return classes.join(' ');
@@ -301,9 +300,9 @@
 
   _computeChangeStatus(change) {
     switch (change.status) {
-      case this.ChangeStatus.MERGED:
+      case ChangeStatus.MERGED:
         return 'Merged';
-      case this.ChangeStatus.ABANDONED:
+      case ChangeStatus.ABANDONED:
         return 'Abandoned';
     }
     if (change._revision_number != change._current_revision_number) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
index 687dbd7..2321deb 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
@@ -21,9 +21,6 @@
     :host {
       display: block;
     }
-    h3 {
-      margin: var(--spacing-m) 0 0;
-    }
     section {
       margin-bottom: 1.4em; /* Same as line height for collapse purposes */
     }
@@ -79,7 +76,7 @@
       color: #1b5e20;
     }
     .submittableCheck {
-      color: var(--vote-text-color-recommended);
+      color: var(--positive-green-text-color);
       display: none;
     }
     .submittableCheck.submittable {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 419e95b..e37c84a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../shared/gr-account-chip/gr-account-chip.js';
 import '../../shared/gr-textarea/gr-textarea.js';
@@ -43,6 +40,9 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
+import {ExperimentIds} from '../../../services/flags.js';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -80,7 +80,7 @@
 const SEND_REPLY_TIMING_LABEL = 'SendReply';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrReplyDialog extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -134,6 +134,8 @@
   constructor() {
     super();
     this.FocusTarget = FocusTarget;
+    this.reporting = appContext.reportingService;
+    this.flagsService = appContext.flagsService;
   }
 
   static get properties() {
@@ -259,6 +261,12 @@
         type: Array,
         observer: '_handleHeightChanged',
       },
+      // Track if the message typed in the reply dialog will be created as a
+      // resolved/unresolved patchset level comment
+      _isResolvedPatchsetLevelComment: {
+        type: Boolean,
+        value: true,
+      },
     };
   }
 
@@ -287,11 +295,26 @@
     this.addEventListener('comment-editing-changed', e => {
       this._commentEditing = e.detail;
     });
+
+    // Plugins on reply-reviewers endpoint can take advantage of these
+    // events to add / remove reviewers
+
+    this.addEventListener('add-reviewer', e => {
+      // Only support account type, see more from:
+      // elements/shared/gr-account-list/gr-account-list.js#addAccountItem
+      this.$.reviewers.addAccountItem({account: e.detail.reviewer});
+    });
+
+    this.addEventListener('remove-reviewer', e => {
+      this.$.reviewers.removeAccount(e.detail.reviewer);
+    });
   }
 
   /** @override */
   ready() {
     super.ready();
+    this._isPatchsetCommentsExperimentEnabled = this.flagsService
+        .isEnabled(ExperimentIds.PATCHSET_COMMENTS);
     this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
   }
 
@@ -477,7 +500,7 @@
   }
 
   send(includeComments, startReview) {
-    this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
+    this.reporting.time(SEND_REPLY_TIMING_LABEL);
     const labels = this.$.labelScores.getLabelValues();
 
     const obj = {
@@ -490,7 +513,17 @@
     }
 
     if (this.draft != null) {
-      obj.message = this.draft;
+      if (this._isPatchsetCommentsExperimentEnabled) {
+        const comment = {
+          message: this.draft,
+          unresolved: !this._isResolvedPatchsetLevelComment,
+        };
+        obj.comments = {
+          [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [comment],
+        };
+      } else {
+        obj.message = this.draft;
+      }
     }
 
     const accountAdditions = {};
@@ -623,11 +656,11 @@
   }
 
   _computeHideDraftList(draftCommentThreads) {
-    return draftCommentThreads.length === 0;
+    return !draftCommentThreads || draftCommentThreads.length === 0;
   }
 
   _computeDraftsTitle(draftCommentThreads) {
-    const total = draftCommentThreads.length;
+    const total = draftCommentThreads ? draftCommentThreads.length : 0;
     if (total == 0) { return ''; }
     if (total == 1) { return '1 Draft'; }
     if (total > 1) { return total + ' Drafts'; }
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
index 54fd47a..79f38a6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -97,20 +97,14 @@
     }
     .textareaContainer,
     #textarea,
-    gr-endpoint-decorator {
+    gr-endpoint-decorator[name='reply-text'] {
       display: flex;
       width: 100%;
     }
-    gr-endpoint-decorator[name='reply-label-scores'] {
-      display: block;
-    }
     .previewContainer gr-formatted-text {
       background: var(--table-header-background-color);
       padding: var(--spacing-l);
     }
-    .draftsContainer h3 {
-      margin-top: var(--spacing-xs);
-    }
     #checkingStatusLabel,
     #notLatestLabel {
       margin-left: var(--spacing-l);
@@ -137,23 +131,31 @@
     #pluginMessage:empty {
       display: none;
     }
+    .preview-formatting {
+      margin-left: var(--spacing-m);
+    }
   </style>
   <div class="container" tabindex="-1">
     <section class="peopleContainer">
-      <div class="peopleList">
-        <div class="peopleListLabel">Reviewers</div>
-        <gr-account-list
-          id="reviewers"
-          accounts="{{_reviewers}}"
-          removable-values="[[change.removable_reviewers]]"
-          filter="[[filterReviewerSuggestion]]"
-          pending-confirmation="{{_reviewerPendingConfirmation}}"
-          placeholder="Add reviewer..."
-          on-account-text-changed="_handleAccountTextEntry"
-          suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
-        >
-        </gr-account-list>
-      </div>
+      <gr-endpoint-decorator name="reply-reviewers">
+        <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+        <div class="peopleList">
+          <div class="peopleListLabel">Reviewers</div>
+          <gr-account-list
+            id="reviewers"
+            accounts="{{_reviewers}}"
+            removable-values="[[change.removable_reviewers]]"
+            filter="[[filterReviewerSuggestion]]"
+            pending-confirmation="{{_reviewerPendingConfirmation}}"
+            placeholder="Add reviewer..."
+            on-account-text-changed="_handleAccountTextEntry"
+            suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
+          >
+          </gr-account-list>
+          <gr-endpoint-slot name="right"></gr-endpoint-slot>
+        </div>
+        <gr-endpoint-slot name="below"></gr-endpoint-slot>
+      </gr-endpoint-decorator>
       <div class="peopleList">
         <div class="peopleListLabel">CC</div>
         <gr-account-list
@@ -210,7 +212,17 @@
       </gr-endpoint-decorator>
     </section>
     <section class="previewContainer">
-      <label>
+      <template is="dom-if" if="[[_isPatchsetCommentsExperimentEnabled]]">
+        <label>
+          <input
+            id="resolvedPatchsetLevelCommentCheckbox"
+            type="checkbox"
+            checked="{{_isResolvedPatchsetLevelComment::change}}"
+          />
+          Resolved
+        </label>
+      </template>
+      <label class="preview-formatting">
         <input type="checkbox" checked="{{_previewFormatting::change}}" />
         Preview formatting
       </label>
@@ -318,5 +330,4 @@
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-storage id="storage"></gr-storage>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 5a61864..576ee3e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -36,6 +36,9 @@
 import '../../../test/common-test-setup.js';
 import './gr-reply-dialog.js';
 import {mockPromise} from '../../../test/test-utils.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
+import {appContext} from '../../../services/app-context.js';
+
 function cloneableResponse(status, text) {
   return {
     ok: false,
@@ -82,6 +85,8 @@
       getChangeSuggestedReviewers() { return Promise.resolve([]); },
     });
 
+    sandbox.stub(appContext.flagsService, 'isEnabled').returns(true);
+
     element = fixture('basic');
     element.change = {
       _number: changeNum,
@@ -172,7 +177,12 @@
               'Code-Review': 0,
               'Verified': 0,
             },
-            message: 'I wholeheartedly disapprove',
+            comments: {
+              [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+                message: 'I wholeheartedly disapprove',
+                unresolved: false,
+              }],
+            },
             reviewers: [],
           });
           assert.isFalse(element.$.commentList.hidden);
@@ -189,6 +199,45 @@
     });
   });
 
+  test('toggle resolved checkbox', done => {
+    // Async tick is needed because iron-selector content is distributed and
+    // distributed content requires an observer to be set up.
+    // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+    const checkboxEl = element.shadowRoot.querySelector(
+        '#resolvedPatchsetLevelCommentCheckbox');
+    MockInteractions.tap(checkboxEl);
+    flush(() => {
+      flush(() => {
+        element.draft = 'I wholeheartedly disapprove';
+
+        stubSaveReview(review => {
+          assert.deepEqual(review, {
+            drafts: 'PUBLISH_ALL_REVISIONS',
+            labels: {
+              'Code-Review': 0,
+              'Verified': 0,
+            },
+            comments: {
+              [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+                message: 'I wholeheartedly disapprove',
+                unresolved: true,
+              }],
+            },
+            reviewers: [],
+          });
+          done();
+        });
+
+        // This is needed on non-Blink engines most likely due to the ways in
+        // which the dom-repeat elements are stamped.
+        flush(() => {
+          MockInteractions.tap(element.shadowRoot
+              .querySelector('.send'));
+        });
+      });
+    });
+  });
+
   test('keep draft comments with reply', done => {
     MockInteractions.tap(element.shadowRoot.querySelector('#includeComments'));
     assert.equal(element._includeComments, false);
@@ -207,7 +256,12 @@
               'Code-Review': 0,
               'Verified': 0,
             },
-            message: 'I wholeheartedly disapprove',
+            comments: {
+              [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+                message: 'I wholeheartedly disapprove',
+                unresolved: false,
+              }],
+            },
             reviewers: [],
           });
           assert.isTrue(element.$.commentList.hidden);
@@ -233,7 +287,12 @@
           'Code-Review': -1,
           'Verified': -1,
         },
-        message: 'I wholeheartedly disapprove',
+        comments: {
+          [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+            message: 'I wholeheartedly disapprove',
+            unresolved: false,
+          }],
+        },
         reviewers: [],
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index c933c7c..88e3160 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-account-chip/gr-account-chip.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -27,7 +25,7 @@
 import {htmlTemplate} from './gr-reviewer-list_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrReviewerList extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index b8079eff..bd91990 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-comment-thread/gr-comment-thread.js';
@@ -24,15 +22,16 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-thread-list_html.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 
 import {NO_THREADS_MSG} from '../../../constants/messages.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 /**
  * Fired when a comment is saved or deleted
  *
  * @event thread-list-modified
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrThreadList extends GestureEventListeners(
     LegacyElementMixin(
@@ -51,12 +50,6 @@
       _sortedThreads: {
         type: Array,
       },
-      _filteredThreads: {
-        type: Array,
-        computed: '_computeFilteredThreads(_sortedThreads, ' +
-          '_unresolvedOnly, _draftsOnly,' +
-          'onlyShowRobotCommentsWithHumanReply)',
-      },
       _unresolvedOnly: {
         type: Boolean,
         value: false,
@@ -82,124 +75,199 @@
     };
   }
 
-  static get observers() { return ['_computeSortedThreads(threads.*)']; }
+  static get observers() {
+    return ['_updateSortedThreads(threads, threads.splices)'];
+  }
 
   _computeShowDraftToggle(loggedIn) {
     return loggedIn ? 'show' : '';
   }
 
+  _compareThreads(c1, c2) {
+    if (c1.thread.path !== c2.thread.path) {
+      // '/PATCHSET' will not come before '/COMMIT' when sorting
+      // alphabetically so move it to the front explicitly
+      if (c1.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+        return -1;
+      }
+      if (c2.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+        return 1;
+      }
+      return c1.thread.path.localeCompare(c2.thread.path);
+    }
+
+    // Patchset comments have no line/range associated with them
+    if (c1.thread.line !== c2.thread.line) {
+      if (!c1.thread.line || !c2.thread.line) {
+        // one of them is a file level comment, show first
+        return c1.thread.line ? 1 : -1;
+      }
+      return c1.thread.line < c2.thread.line ? -1 : 1;
+    }
+
+    if (c1.thread.patchNum !== c2.thread.patchNum) {
+      return c1.thread.patchNum > c2.thread.patchNum ? -1 : 1;
+    }
+
+    if (c2.unresolved !== c1.unresolved) {
+      if (!c1.unresolved) { return 1; }
+      if (!c2.unresolved) { return -1; }
+    }
+
+    if (c2.hasDraft !== c1.hasDraft) {
+      if (!c1.hasDraft) { return 1; }
+      if (!c2.hasDraft) { return -1; }
+    }
+
+    const c1Date = c1.__date || parseDate(c1.updated);
+    const c2Date = c2.__date || parseDate(c2.updated);
+    const dateCompare = c2Date - c1Date;
+    if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
+      return 0;
+    }
+    return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+  }
+
   /**
+   * Observer on threads and update _sortedThreads when needed.
    * Order as follows:
-   *  - Unresolved threads with drafts (reverse chronological)
-   *  - Unresolved threads without drafts (reverse chronological)
-   *  - Resolved threads with drafts (reverse chronological)
-   *  - Resolved threads without drafts (reverse chronological)
+   *  - Patchset level threads (descending based on patchset number)
+   *    - unresolved
+          - comments with drafts
+          - comments without drafts
+   *    - resolved
+          - comments with drafts
+          - comments without drafts
+   *  - File name
+   *    - Line number
+   *      - Unresolved (descending based on patchset number)
+   *        - comments with drafts
+   *        - comments without drafts
+   *      - Resolved (descending based on patchset number)
+   *        - comments with drafts
+   *        - comments without drafts
    *
-   * @param {!Object} changeRecord
+   * @param {Array<Object>} threads
+   * @param {!Object} spliceRecord
    */
-  _computeSortedThreads(changeRecord) {
-    const threads = changeRecord.base;
-    if (!threads) { return []; }
-    this._updateSortedThreads(threads);
+  _updateSortedThreads(threads, spliceRecord) {
+    if (!threads) {
+      this._sortedThreads = [];
+      return;
+    }
+    // We only want to sort on thread additions / removals to avoid
+    // re-rendering on modifications (add new reply / edit draft etc)
+    //  https://polymer-library.polymer-project.org/3.0/docs/devguide/observers#array-observation
+    const isArrayMutation = spliceRecord &&
+      (spliceRecord.indexSplices.addedCount !== 0
+        || spliceRecord.indexSplices.removed.length);
+
+    if (this._sortedThreads
+        && this._sortedThreads.length === threads.length
+        && !isArrayMutation) {
+      // Instead of replacing the _sortedThreads which will trigger a re-render,
+      // we override all threads inside of it
+
+      for (const thread of threads) {
+        const idxInSortedThreads = this._sortedThreads
+            .findIndex(t => t.rootId === thread.rootId);
+        this.set(`_sortedThreads.${idxInSortedThreads}`, {...thread});
+      }
+      return;
+    }
+
+    const threadsWithInfo = threads
+        .map(thread => this._getThreadWithStatusInfo(thread));
+    this._sortedThreads = threadsWithInfo.sort((t1, t2) =>
+      this._compareThreads(t1, t2)).map(threadInfo => threadInfo.thread);
   }
 
-  // TODO(taoalpha): should allow only sort once during initialization
-  // to avoid flickering
-  _updateSortedThreads(threads) {
-    this._sortedThreads =
-        threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
-          // threads will be sorted by:
-          // - unresolved first
-          // - with drafts
-          // - file path
-          // - line
-          // - updated time
-          if (c2.unresolved || c1.unresolved) {
-            if (!c1.unresolved) { return 1; }
-            if (!c2.unresolved) { return -1; }
-          }
-
-          if (c2.hasDraft || c1.hasDraft) {
-            if (!c1.hasDraft) { return 1; }
-            if (!c2.hasDraft) { return -1; }
-          }
-
-          // TODO: Update here once we introduce patchset level comments
-          // they may not have or have a special line or path attribute
-
-          if (c1.thread.path !== c2.thread.path) {
-            return c1.thread.path.localeCompare(c2.thread.path);
-          }
-
-          // File level comments (no `line` property)
-          // should always show before any lines
-          if ([c1, c2].some(c => c.thread.line === undefined)) {
-            if (!c1.thread.line) { return -1; }
-            if (!c2.thread.line) { return 1; }
-          } else if (c1.thread.line !== c2.thread.line) {
-            return c1.thread.line - c2.thread.line;
-          }
-
-          const c1Date = c1.__date || util.parseDate(c1.updated);
-          const c2Date = c2.__date || util.parseDate(c2.updated);
-          const dateCompare = c2Date - c1Date;
-          if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
-            return 0;
-          }
-          return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
-        });
-  }
-
-  _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly,
+  _isFirstThreadWithFileName(sortedThreads, thread, unresolvedOnly, draftsOnly,
       onlyShowRobotCommentsWithHumanReply) {
-    // Polymer 2: check for undefined
+    const threads = sortedThreads.filter(t => this._shouldShowThread(
+        t, unresolvedOnly, draftsOnly,
+        onlyShowRobotCommentsWithHumanReply));
+    const index = threads.findIndex(t => t.rootId === thread.rootId);
+    if (index === -1) {
+      return false;
+    }
+    return index === 0 || (threads[index - 1].path !== threads[index].path);
+  }
+
+  _shouldRenderSeparator(sortedThreads, thread, unresolvedOnly, draftsOnly,
+      onlyShowRobotCommentsWithHumanReply) {
+    const threads = sortedThreads.filter(t => this._shouldShowThread(
+        t, unresolvedOnly, draftsOnly,
+        onlyShowRobotCommentsWithHumanReply));
+    const index = threads.findIndex(t => t.rootId === thread.rootId);
+    if (index === -1) {
+      return false;
+    }
+    return index > 0 && this._isFirstThreadWithFileName(sortedThreads,
+        thread, unresolvedOnly, draftsOnly,
+        onlyShowRobotCommentsWithHumanReply);
+  }
+
+  _shouldShowThread(thread, unresolvedOnly, draftsOnly,
+      onlyShowRobotCommentsWithHumanReply) {
     if ([
-      sortedThreads,
+      thread,
       unresolvedOnly,
       draftsOnly,
       onlyShowRobotCommentsWithHumanReply,
-    ].some(arg => arg === undefined)) {
-      return undefined;
+    ].includes(undefined)) {
+      return false;
     }
 
-    return sortedThreads.filter(c => {
-      if (draftsOnly) {
-        return c.hasDraft;
-      } else if (unresolvedOnly) {
-        return c.unresolved;
-      } else {
-        const comments = c && c.thread && c.thread.comments;
-        let robotComment = false;
-        let humanReplyToRobotComment = false;
-        comments.forEach(comment => {
-          if (comment.robot_id) {
-            robotComment = true;
-          } else if (robotComment) {
-            // Robot comment exists and human comment exists after it
-            humanReplyToRobotComment = true;
-          }
-        });
-        if (robotComment && onlyShowRobotCommentsWithHumanReply) {
-          return humanReplyToRobotComment;
-        }
-        return c;
-      }
-    }).map(threadInfo => threadInfo.thread);
+    if (!draftsOnly
+        && !unresolvedOnly
+        && !onlyShowRobotCommentsWithHumanReply) {
+      return true;
+    }
+
+    const threadInfo = this._getThreadWithStatusInfo(thread);
+
+    if (threadInfo.isEditing) {
+      return true;
+    }
+
+    if (threadInfo.hasRobotComment
+       && onlyShowRobotCommentsWithHumanReply
+       && !threadInfo.hasHumanReplyToRobotComment) {
+      return false;
+    }
+
+    let filtersCheck = true;
+    if (draftsOnly && unresolvedOnly) {
+      filtersCheck = threadInfo.hasDraft && threadInfo.unresolved;
+    } else if (draftsOnly) {
+      filtersCheck = threadInfo.hasDraft;
+    } else if (unresolvedOnly) {
+      filtersCheck = threadInfo.unresolved;
+    }
+
+    return filtersCheck;
   }
 
-  _getThreadWithSortInfo(thread) {
-    const lastComment = thread.comments[thread.comments.length - 1] || {};
-
-    const lastNonDraftComment =
-        (lastComment.__draft && thread.comments.length > 1) ?
-          thread.comments[thread.comments.length - 2] :
-          lastComment;
+  _getThreadWithStatusInfo(thread) {
+    const comments = thread.comments;
+    const lastComment = comments[comments.length - 1] || {};
+    let hasRobotComment = false;
+    let hasHumanReplyToRobotComment = false;
+    comments.forEach(comment => {
+      if (comment.robot_id) {
+        hasRobotComment = true;
+      } else if (hasRobotComment) {
+        hasHumanReplyToRobotComment = true;
+      }
+    });
 
     return {
       thread,
-      // Use the unresolved bit for the last non draft comment. This is what
-      // anybody other than the current user would see.
-      unresolved: !!lastNonDraftComment.unresolved,
+      hasRobotComment,
+      hasHumanReplyToRobotComment,
+      unresolved: !!lastComment.unresolved,
+      isEditing: !!lastComment.__editing,
       hasDraft: !!lastComment.__draft,
       updated: lastComment.updated || lastComment.__date,
     };
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
index fd34b2d..bb87db8 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
@@ -53,24 +53,24 @@
     .draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
       display: block;
     }
+    .thread-separator {
+      border-top: 1px solid var(--border-color);
+      margin-top: var(--spacing-xl);
+    }
   </style>
   <template is="dom-if" if="[[!hideToggleButtons]]">
     <div class="header">
       <div class="toggleItem">
-        <paper-toggle-button
-          id="unresolvedToggle"
-          checked="{{_unresolvedOnly}}"
-        ></paper-toggle-button>
-        Only unresolved threads
+        <paper-toggle-button id="unresolvedToggle" checked="{{_unresolvedOnly}}"
+          >Only unresolved threads</paper-toggle-button
+        >
       </div>
       <div
         class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]"
       >
-        <paper-toggle-button
-          id="draftToggle"
-          checked="{{_draftsOnly}}"
-        ></paper-toggle-button>
-        Only threads with drafts
+        <paper-toggle-button id="draftToggle" checked="{{_draftsOnly}}"
+          >Only threads with drafts</paper-toggle-button
+        >
       </div>
     </div>
   </template>
@@ -80,25 +80,37 @@
     </template>
     <template
       is="dom-repeat"
-      items="[[_filteredThreads]]"
+      items="[[_sortedThreads]]"
       as="thread"
-      initial-count="5"
+      initial-count="10"
       target-framerate="60"
     >
-      <gr-comment-thread
-        show-file-path=""
-        change-num="[[changeNum]]"
-        comments="[[thread.comments]]"
-        comment-side="[[thread.commentSide]]"
-        project-name="[[change.project]]"
-        is-on-parent="[[_isOnParent(thread.commentSide)]]"
-        line-num="[[thread.line]]"
-        patch-num="[[thread.patchNum]]"
-        path="[[thread.path]]"
-        root-id="{{thread.rootId}}"
-        on-thread-changed="_handleCommentsChanged"
-        on-thread-discard="_handleThreadDiscard"
-      ></gr-comment-thread>
+      <template
+        is="dom-if"
+        if="[[_shouldShowThread(thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+      >
+        <template
+          is="dom-if"
+          if="[[_shouldRenderSeparator(_sortedThreads, thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+        >
+          <div class="thread-separator"></div>
+        </template>
+        <gr-comment-thread
+          show-file-path=""
+          change-num="[[changeNum]]"
+          comments="[[thread.comments]]"
+          comment-side="[[thread.commentSide]]"
+          show-file-name="[[_isFirstThreadWithFileName(_sortedThreads, thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+          project-name="[[change.project]]"
+          is-on-parent="[[_isOnParent(thread.commentSide)]]"
+          line-num="[[thread.line]]"
+          patch-num="[[thread.patchNum]]"
+          path="[[thread.path]]"
+          root-id="{{thread.rootId}}"
+          on-thread-changed="_handleCommentsChanged"
+          on-thread-discard="_handleThreadDiscard"
+        ></gr-comment-thread>
+      </template>
     </template>
   </div>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 4b00d5a..3b29ea7 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -36,15 +36,22 @@
 import './gr-thread-list.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {NO_THREADS_MSG} from '../../../constants/messages.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
+
 suite('gr-thread-list tests', () => {
   let element;
   let sandbox;
   let threadElements;
 
-  setup(() => {
+  function getVisibleThreads() {
+    return [...dom(element.root)
+        .querySelectorAll('gr-comment-thread')]
+        .filter(e => e.style.display !== 'none');
+  }
+
+  setup(done => {
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
-    element.onlyShowRobotCommentsWithHumanReply = true;
     element.threads = [
       {
         comments: [
@@ -69,7 +76,7 @@
             in_reply_to: 'ecf0b9fa_fe1a5f62',
             updated: '2018-02-13 22:48:48.018000000',
             message: 'draft',
-            unresolved: false,
+            unresolved: true,
             __draft: true,
             __draftID: '0.m683trwff68',
             __editing: false,
@@ -118,7 +125,7 @@
             id: '8caddf38_44770ec1',
             updated: '2018-02-13 22:48:40.000000000',
             message: 'Another unresolved comment',
-            unresolved: true,
+            unresolved: false,
           },
         ],
         patchNum: 2,
@@ -173,6 +180,40 @@
       {
         comments: [
           {
+            id: 'patchset_level_1',
+            path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+            updated: '2018-02-15 22:48:48.018000000',
+            message: 'patchset comment 1',
+            unresolved: false,
+            __editing: false,
+            patch_set: '2',
+          },
+        ],
+        patchNum: 2,
+        path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+        rootId: 'patchset_level_1',
+        start_datetime: '2018-02-09 18:49:18.000000000',
+      },
+      {
+        comments: [
+          {
+            id: 'patchset_level_2',
+            path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+            updated: '2018-02-15 22:48:48.018000000',
+            message: 'patchset comment 2',
+            unresolved: false,
+            __editing: false,
+            patch_set: '3',
+          },
+        ],
+        patchNum: 3,
+        path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+        rootId: 'patchset_level_2',
+        start_datetime: '2018-02-09 18:49:18.000000000',
+      },
+      {
+        comments: [
+          {
             __path: '/COMMIT_MSG',
             author: {
               _account_id: 1000000,
@@ -233,9 +274,13 @@
         start_datetime: '2019-03-08 18:49:18.000000000',
       },
     ];
-    flushAsynchronousOperations();
-    threadElements = dom(element.root)
-        .querySelectorAll('gr-comment-thread');
+
+    // use flush to render all (bypass initial-count set on dom-repeat)
+    flush(() => {
+      threadElements = dom(element.root)
+          .querySelectorAll('gr-comment-thread');
+      done();
+    });
   });
 
   teardown(() => {
@@ -252,88 +297,291 @@
     'none');
   });
 
-  test('there are five threads by default', () => {
+  test('show all threads by default', () => {
     assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 5);
+        .querySelectorAll('gr-comment-thread').length, element.threads.length);
+    assert.equal(getVisibleThreads().length, element.threads.length);
+  });
+
+  test('showing file name takes visible threads into account', () => {
+    assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
+        element._sortedThreads[2], element._unresolvedOnly, element._draftsOnly,
+        element.onlyShowRobotCommentsWithHumanReply), true);
+    element._unresolvedOnly = true;
+    assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
+        element._sortedThreads[2], element._unresolvedOnly, element._draftsOnly,
+        element.onlyShowRobotCommentsWithHumanReply), false);
+  });
+
+  test('onlyShowRobotCommentsWithHumanReply ', () => {
+    element.onlyShowRobotCommentsWithHumanReply = true;
+    flushAsynchronousOperations();
+    assert.equal(
+        getVisibleThreads().length,
+        element.threads.length - 1);
+    assert.isNotOk(getVisibleThreads().find(th => th.rootId === 'rc1'));
+  });
+
+  suite('_compareThreads', () => {
+    test('patchset comes before any other file', () => {
+      const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS}};
+      const t2 = {thread: {path: SpecialFilePath.COMMIT_MESSAGE}};
+
+      t1.patchNum = t2.patchNum = 1;
+      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+
+      // assigning values to properties such that t2 should come first
+      t1.patchNum = 1;
+      t2.patchNum = 2;
+      t1.unresolved = t1.hasDraft = false;
+      t2.unresolved = t2.unresolved = true;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+    });
+
+    test('file path is compared lexicographically', () => {
+      const t1 = {thread: {path: 'a.txt'}};
+      const t2 = {thread: {path: 'b.txt'}};
+      t1.patchNum = t2.patchNum = 1;
+      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+
+      t1.patchNum = 1;
+      t2.patchNum = 2;
+      t1.unresolved = t1.hasDraft = false;
+      t2.unresolved = t2.unresolved = true;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+    });
+
+    test('patchset comments sorted by reverse patchset', () => {
+      const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+        patchNum: 1}};
+      const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+        patchNum: 2}};
+      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), 1);
+      assert.equal(element._compareThreads(t2, t1), -1);
+
+      t1.unresolved = t1.hasDraft = false;
+      t2.unresolved = t2.unresolved = true;
+      assert.equal(element._compareThreads(t1, t2), 1);
+      assert.equal(element._compareThreads(t2, t1), -1);
+    });
+
+    test('patchset comments with same patchset picks unresolved first', () => {
+      const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+        patchNum: 1}, unresolved: true};
+      const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+        patchNum: 1}, unresolved: false};
+      t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+    });
+
+    test('file level comment before line', () => {
+      const t1 = {thread: {path: 'a.txt', line: 2}};
+      const t2 = {thread: {path: 'a.txt'}};
+      t1.patchNum = t2.patchNum = 1;
+      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), 1);
+      assert.equal(element._compareThreads(t2, t1), -1);
+
+      // give preference to t1 in unresolved/draft properties
+      t1.unresolved = t1.hasDraft = true;
+      t2.unresolved = t2.unresolved = false;
+      assert.equal(element._compareThreads(t1, t2), 1);
+      assert.equal(element._compareThreads(t2, t1), -1);
+    });
+
+    test('comments sorted by line', () => {
+      const t1 = {thread: {path: 'a.txt', line: 2}};
+      const t2 = {thread: {path: 'a.txt', line: 3}};
+      t1.patchNum = t2.patchNum = 1;
+      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+
+      t1.unresolved = t1.hasDraft = false;
+      t2.unresolved = t2.unresolved = true;
+      assert.equal(element._compareThreads(t1, t2), -1);
+      assert.equal(element._compareThreads(t2, t1), 1);
+    });
+
+    test('comments on same line sorted by reverse patchset', () => {
+      const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1}};
+      const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 2}};
+      t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+      assert.equal(element._compareThreads(t1, t2), 1);
+      assert.equal(element._compareThreads(t2, t1), -1);
+
+      // give preference to t1 in unresolved/draft properties
+      t1.unresolved = t1.hasDraft = true;
+      t2.unresolved = t2.unresolved = false;
+      assert.equal(element._compareThreads(t1, t2), 1);
+      assert.equal(element._compareThreads(t2, t1), -1);
+    });
+
+    test('comments on same line & patchset sorted by unresolved first',
+        () => {
+          const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+            unresolved: true};
+          const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+            unresolved: false};
+          t1.patchNum = t2.patchNum = 1;
+          assert.equal(element._compareThreads(t1, t2), -1);
+          assert.equal(element._compareThreads(t2, t1), 1);
+
+          t2.hasDraft = true;
+          t1.hasDraft = false;
+          assert.equal(element._compareThreads(t1, t2), -1);
+          assert.equal(element._compareThreads(t2, t1), 1);
+        });
+
+    test('comments on same line & patchset & unresolved sorted by draft',
+        () => {
+          const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+            unresolved: true, hasDraft: false};
+          const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+            unresolved: true, hasDraft: true};
+          t1.patchNum = t2.patchNum = 1;
+          assert.equal(element._compareThreads(t1, t2), 1);
+          assert.equal(element._compareThreads(t2, t1), -1);
+        });
   });
 
   test('_computeSortedThreads', () => {
-    assert.equal(element._sortedThreads.length, 7);
-    // Draft and unresolved for commit-msg at line 5
-    assert.equal(element._sortedThreads[0].thread.rootId,
-        'ecf0b9fa_fe1a5f62');
-    // /COMMIT_MSG
-    // unresolved no draft and file level
-    assert.equal(element._sortedThreads[1].thread.rootId,
-        '8caddf38_44770ec1');
-    // unresolved no draft at line 4
-    assert.equal(element._sortedThreads[2].thread.rootId,
-        'scaddf38_44770ec1');
-    // unresolved no draft at line 5
-    assert.equal(element._sortedThreads[3].thread.rootId,
-        'rc1');
-    // Unresolved no draft at line 7
-    assert.equal(element._sortedThreads[4].thread.rootId,
-        'rc2');
-    // resolved and draft on COMMIT_MSG
-    assert.equal(element._sortedThreads[5].thread.rootId,
-        'zcf0b9fa_fe1a5f62');
-    // resolved and on file test.txt
-    assert.equal(element._sortedThreads[6].thread.rootId,
-        '09a9fb0a_1484e6cf');
+    assert.equal(element._sortedThreads.length, 9);
+    const expectedSortedRootIds = [
+      'patchset_level_2', // Posted on Patchset 3
+      'patchset_level_1', // Posted on Patchset 2
+      '8caddf38_44770ec1', // File level on COMMIT_MSG
+      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+      'rc2', // Line 7 on COMMIT_MSG
+      '09a9fb0a_1484e6cf', // File level on test.txt
+    ];
+    element._sortedThreads.forEach((thread, index) => {
+      assert.equal(thread.rootId, expectedSortedRootIds[index]);
+    });
   });
 
-  test('filtered threads do not contain robot comments without reply', () => {
-    const thread = element.threads.find(thread => thread.rootId === 'rc1');
-    assert.equal(element._filteredThreads.includes(thread), false);
-  });
-
-  test('filtered threads contains robot comments with reply', () => {
-    const thread = element.threads.find(thread => thread.rootId === 'rc2');
-    assert.equal(element._filteredThreads.includes(thread), true);
-  });
-
-  test('thread removal', () => {
+  test('thread removal and sort again', () => {
     threadElements[1].dispatchEvent(
         new CustomEvent('thread-discard', {
           detail: {rootId: 'rc2'},
           composed: true, bubbles: true,
         }));
     flushAsynchronousOperations();
-    assert.equal(element._sortedThreads.length, 6);
-    assert.equal(element._sortedThreads[0].thread.rootId,
-        'ecf0b9fa_fe1a5f62');
-    // /COMMIT_MSG
-    // unresolved no draft and file level
-    assert.equal(element._sortedThreads[1].thread.rootId,
-        '8caddf38_44770ec1');
-    // unresolved no draft at line 4
-    assert.equal(element._sortedThreads[2].thread.rootId,
-        'scaddf38_44770ec1');
-    // unresolved no draft at line 5
-    assert.equal(element._sortedThreads[3].thread.rootId,
-        'rc1');
-    // resolved and draft
-    assert.equal(element._sortedThreads[4].thread.rootId,
-        'zcf0b9fa_fe1a5f62');
-    // resolved and on file test.txt
-    assert.equal(element._sortedThreads[5].thread.rootId,
-        '09a9fb0a_1484e6cf');
+    assert.equal(element._sortedThreads.length, 8);
+    const expectedSortedRootIds = [
+      'patchset_level_2',
+      'patchset_level_1',
+      '8caddf38_44770ec1', // File level on COMMIT_MSG
+      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+      '09a9fb0a_1484e6cf', // File level on test.txt
+    ];
+    element._sortedThreads.forEach((thread, index) => {
+      assert.equal(thread.rootId, expectedSortedRootIds[index]);
+    });
+  });
+
+  test('modification on thread shold not trigger sort again', () => {
+    const currentSortedThreads = [...element._sortedThreads];
+    for (const thread of currentSortedThreads) {
+      thread.comments = [...thread.comments];
+    }
+    const modifiedThreads = [...element.threads];
+    modifiedThreads[5] = {...modifiedThreads[5]};
+    modifiedThreads[5].comments = [...modifiedThreads[5].comments, {
+      ...modifiedThreads[5].comments[0],
+      unresolved: false,
+    }];
+    element.threads = modifiedThreads;
+    assert.notDeepEqual(currentSortedThreads, element._sortedThreads);
+
+    // exact same order as in _computeSortedThreads
+    const expectedSortedRootIds = [
+      'patchset_level_2',
+      'patchset_level_1',
+      '8caddf38_44770ec1', // File level on COMMIT_MSG
+      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+      'rc2', // Line 7 on COMMIT_MSG
+      '09a9fb0a_1484e6cf', // File level on test.txt
+    ];
+    element._sortedThreads.forEach((thread, index) => {
+      assert.equal(thread.rootId, expectedSortedRootIds[index]);
+    });
+  });
+
+  test('reset sortedThreads when threads set to undefiend', () => {
+    element.threads = undefined;
+    assert.deepEqual(element._sortedThreads, []);
+  });
+
+  test('non-equal length of sortThreads and threads' +
+    ' should trigger sort again', () => {
+    const modifiedThreads = [...element.threads];
+    const currentSortedThreads = [...element._sortedThreads];
+    element._sortedThreads = [];
+    element.threads = modifiedThreads;
+    assert.deepEqual(currentSortedThreads, element._sortedThreads);
+
+    // exact same order as in _computeSortedThreads
+    const expectedSortedRootIds = [
+      'patchset_level_2',
+      'patchset_level_1',
+      '8caddf38_44770ec1', // File level on COMMIT_MSG
+      'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+      'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+      'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+      'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+      'rc2', // Line 7 on COMMIT_MSG
+      '09a9fb0a_1484e6cf', // File level on test.txt
+    ];
+    element._sortedThreads.forEach((thread, index) => {
+      assert.equal(thread.rootId, expectedSortedRootIds[index]);
+    });
   });
 
   test('toggle unresolved only shows unresolved comments', () => {
     MockInteractions.tap(element.shadowRoot.querySelector(
         '#unresolvedToggle'));
     flushAsynchronousOperations();
-    assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 5);
+    assert.equal(getVisibleThreads().length, 4);
   });
 
   test('toggle drafts only shows threads with draft comments', () => {
     MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
     flushAsynchronousOperations();
-    assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 2);
+    assert.equal(getVisibleThreads().length, 2);
+  });
+
+  test('toggle drafts and unresolved should ignore comments in editing', () => {
+    const modifiedThreads = [...element.threads];
+    modifiedThreads[5] = {...modifiedThreads[5]};
+    modifiedThreads[5].comments = [...modifiedThreads[5].comments];
+    modifiedThreads[5].comments.push({
+      ...modifiedThreads[5].comments[0],
+      __editing: true,
+    });
+    element.threads = modifiedThreads;
+    MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
+    MockInteractions.tap(element.shadowRoot.querySelector(
+        '#unresolvedToggle'));
+    flushAsynchronousOperations();
+    assert.equal(getVisibleThreads().length, 2);
   });
 
   test('toggle drafts and unresolved only shows threads with drafts and ' +
@@ -342,8 +590,7 @@
     MockInteractions.tap(element.shadowRoot.querySelector(
         '#unresolvedToggle'));
     flushAsynchronousOperations();
-    assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 2);
+    assert.equal(getVisibleThreads().length, 1);
   });
 
   test('modification events are consumed and displatched', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 9171908..0658996 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-shell-command/gr-shell-command.js';
@@ -36,7 +34,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrUploadHelpDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 2645c63..903552a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -27,10 +26,10 @@
 import {htmlTemplate} from './gr-account-dropdown_html.js';
 import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
 
-const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
+const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountDropdown extends mixinBehaviors( [
   DisplayNameBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 6814d89..99c4cb3 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-error-dialog_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrErrorDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 4b5969a..9f6d050 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -16,15 +16,7 @@
  */
 /* Import to get Gerrit interface */
 /* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-
-import '../../../scripts/bundled-polymer.js';
 import '../gr-error-dialog/gr-error-dialog.js';
-import '../gr-reporting/gr-reporting.js';
 import '../../shared/gr-alert/gr-alert.js';
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -37,6 +29,7 @@
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {authService} from '../../shared/gr-rest-api-interface/gr-auth.js';
 import {gerritEventEmitter} from '../../shared/gr-event-emitter/gr-event-emitter.js';
+import {appContext} from '../../../services/app-context.js';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -47,7 +40,7 @@
 const AUTHENTICATION_REQUIRED = 'Authentication required\n';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrErrorManager extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -98,6 +91,8 @@
 
     /** @type {?Function} */
     this._authErrorHandlerDeregistrationHook;
+
+    this.reporting = appContext.reportingService;
   }
 
   /** @override */
@@ -205,7 +200,6 @@
         showSignInButton: !isLoggedIn,
       });
     });
-    return;
   }
 
   _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) {
@@ -406,7 +400,7 @@
   }
 
   _showErrorDialog(message, opt_options) {
-    this.$.reporting.reportErrorDialog(message);
+    this.reporting.reportErrorDialog(message);
     this.$.errorDialog.text = message;
     this.$.errorDialog.showSignInButton =
         opt_options && opt_options.showSignInButton;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
index 4d32f24..2fa9b95 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
@@ -35,5 +35,4 @@
   >
   </gr-overlay>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 8272c6e..b52bb7d 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -24,11 +24,6 @@
 
 <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-error-manager.js';
-void (0);
-</script>
 
 <test-fixture id="basic">
   <template>
@@ -338,7 +333,7 @@
           }));
       assert.equal(window.fetch.callCount, 1);
       flush(() => {
-        // here needs two flush as there are two chanined
+        // here needs two flush as there are two chained
         // promises on server-error handler and flush only flushes one
         assert.equal(window.fetch.callCount, 2);
         flush(() => {
@@ -396,7 +391,7 @@
           }));
       assert.equal(window.fetch.callCount, 1);
       flush(() => {
-        // here needs two flush as there are two chanined
+        // here needs two flush as there are two chained
         // promises on server-error handler and flush only flushes one
         assert.equal(window.fetch.callCount, 2);
         flush(() => {
@@ -490,7 +485,7 @@
       const openStub = sandbox.stub(element.$.errorOverlay, 'open');
       const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
       const reportStub = sandbox.stub(
-          element.$.reporting,
+          element.reporting,
           'reportErrorDialog'
       );
 
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index 5d7ec27..5c54011 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-key-binding-display_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrKeyBindingDisplay extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index beb0f7e..0c0daea 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-button/gr-button.js';
 import '../gr-key-binding-display/gr-key-binding-display.js';
 import '../../../styles/shared-styles.js';
@@ -29,7 +27,7 @@
 const {ShortcutSection} = KeyboardShortcutBinder;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrKeyboardShortcutsDialog extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
index 78b576e..e850fbe 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -55,7 +55,7 @@
     }
   </style>
   <header>
-    <h3>Keyboard shortcuts</h3>
+    <h3 class="heading-3">Keyboard shortcuts</h3>
     <gr-button link="" on-click="_handleCloseTap">Close</gr-button>
   </header>
   <main>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 3294f5f..684b472 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-icons/gr-icons.js';
@@ -86,7 +84,7 @@
 ]);
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrMainHeader extends mixinBehaviors( [
   AdminNavBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
index 19e833c..e50b408 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
@@ -199,6 +199,7 @@
       ></gr-endpoint-decorator>
       <gr-smart-search
         id="search"
+        label="Search for changes"
         search-query="{{searchQuery}}"
       ></gr-smart-search>
       <gr-endpoint-decorator
@@ -221,6 +222,8 @@
           class="settingsButton"
           href$="[[_generateSettingsLink()]]"
           title="Settings"
+          aria-label="Settings"
+          role="button"
         >
           <iron-icon icon="gr-icons:settings"></iron-icon>
         </a>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
index 2b87548..69e2989 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -86,7 +86,7 @@
 const EDIT_PATCHNUM = 'edit';
 const PARENT_PATCHNUM = 'PARENT';
 
-const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;
+const USER_PLACEHOLDER_PATTERN = /\${user}/g;
 
 // NOTE: These queries are tested in Java. Any changes made to definitions
 // here require corresponding changes to:
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
deleted file mode 100644
index e140d6c..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ /dev/null
@@ -1,437 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reporting</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-reporting></gr-reporting>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-reporting.js';
-suite('gr-reporting tests', () => {
-  let element;
-  let sandbox;
-  let clock;
-  let fakePerformance;
-
-  const NOW_TIME = 100;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    clock = sinon.useFakeTimers(NOW_TIME);
-    element = fixture('basic');
-    element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
-    fakePerformance = {
-      navigationStart: 1,
-      loadEventEnd: 2,
-    };
-    fakePerformance.toJSON = () => fakePerformance;
-    sinon.stub(element, 'performanceTiming',
-        {get() { return fakePerformance; }});
-    sandbox.stub(element, 'reporter');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-    clock.restore();
-  });
-
-  test('appStarted', () => {
-    sandbox.stub(element, 'now').returns(42);
-    element.appStarted();
-    assert.isTrue(
-        element.reporter.calledWithMatch(
-            'timing-report', 'UI Latency', 'App Started', 42
-        ));
-    assert.isTrue(
-        element.reporter.calledWithExactly(
-            'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
-            fakePerformance.loadEventEnd - fakePerformance.navigationStart,
-            undefined, true)
-    );
-  });
-
-  test('WebComponentsReady', () => {
-    sandbox.stub(element, 'now').returns(42);
-    element.timeEnd('WebComponentsReady');
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'WebComponentsReady', 42
-    ));
-  });
-
-  test('beforeLocationChanged', () => {
-    element._baselines['garbage'] = 'monster';
-    sandbox.stub(element, 'time');
-    element.beforeLocationChanged();
-    assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
-    assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
-    assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
-    assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
-    assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
-    assert.isFalse(element._baselines.hasOwnProperty('garbage'));
-  });
-
-  test('changeDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.changeDisplayed();
-    assert.isFalse(element.timeEnd.calledWith('ChangeDisplayed'));
-    assert.isTrue(element.timeEnd.calledWith('StartupChangeDisplayed'));
-    element.changeDisplayed();
-    assert.isTrue(element.timeEnd.calledWith('ChangeDisplayed'));
-  });
-
-  test('changeFullyLoaded', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.changeFullyLoaded();
-    assert.isFalse(
-        element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
-    assert.isTrue(
-        element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
-    element.changeFullyLoaded();
-    assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
-  });
-
-  test('diffViewDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.diffViewDisplayed();
-    assert.isFalse(element.timeEnd.calledWith('DiffViewDisplayed'));
-    assert.isTrue(element.timeEnd.calledWith('StartupDiffViewDisplayed'));
-    element.diffViewDisplayed();
-    assert.isTrue(element.timeEnd.calledWith('DiffViewDisplayed'));
-  });
-
-  test('fileListDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.fileListDisplayed();
-    assert.isFalse(
-        element.timeEnd.calledWithExactly('FileListDisplayed'));
-    assert.isTrue(
-        element.timeEnd.calledWithExactly('StartupFileListDisplayed'));
-    element.fileListDisplayed();
-    assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed'));
-  });
-
-  test('dashboardDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.dashboardDisplayed();
-    assert.isFalse(element.timeEnd.calledWith('DashboardDisplayed'));
-    assert.isTrue(element.timeEnd.calledWith('StartupDashboardDisplayed'));
-    element.dashboardDisplayed();
-    assert.isTrue(element.timeEnd.calledWith('DashboardDisplayed'));
-  });
-
-  test('dashboardDisplayed details', () => {
-    sandbox.spy(element, 'timeEnd');
-    sandbox.stub(window, 'performance', {
-      memory: {
-        usedJSHeapSize: 1024 * 1024,
-      },
-      measure: () => {},
-    });
-    sandbox.stub(element, 'now').returns(42);
-    element.reportRpcTiming('/changes/*~*/comments', 500);
-    element.dashboardDisplayed();
-    assert.isTrue(
-        element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
-            {rpcList: [
-              {
-                anonymizedUrl: '/changes/*~*/comments',
-                elapsed: 500,
-              },
-            ],
-            screenSize: {
-              width: window.screen.width,
-              height: window.screen.height,
-            },
-            viewport: {
-              width: document.documentElement.clientWidth,
-              height: document.documentElement.clientHeight,
-            },
-            usedJSHeapSizeMb: 1,
-            }
-        ));
-  });
-
-  test('time and timeEnd', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(0);
-    element.time('foo');
-    nowStub.returns(1);
-    element.time('bar');
-    nowStub.returns(2);
-    element.timeEnd('bar');
-    nowStub.returns(3);
-    element.timeEnd('foo');
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'foo', 3
-    ));
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'bar', 1
-    ));
-  });
-
-  test('timer object', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(100);
-    const timer = element.getTimer('foo-bar');
-    nowStub.returns(150);
-    timer.end();
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'foo-bar', 50));
-  });
-
-  test('timer object double call', () => {
-    const timer = element.getTimer('foo-bar');
-    timer.end();
-    assert.isTrue(element.reporter.calledOnce);
-    assert.throws(() => {
-      timer.end();
-    }, 'Timer for "foo-bar" already ended.');
-  });
-
-  test('timer object maximum', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(100);
-    const timer = element.getTimer('foo-bar').withMaximum(100);
-    nowStub.returns(150);
-    timer.end();
-    assert.isTrue(element.reporter.calledOnce);
-
-    timer.reset();
-    nowStub.returns(260);
-    timer.end();
-    assert.isTrue(element.reporter.calledOnce);
-  });
-
-  test('recordDraftInteraction', () => {
-    const key = 'TimeBetweenDraftActions';
-    const nowStub = sandbox.stub(element, 'now').returns(100);
-    const timingStub = sandbox.stub(element, '_reportTiming');
-    element.recordDraftInteraction();
-    assert.isFalse(timingStub.called);
-
-    nowStub.returns(200);
-    element.recordDraftInteraction();
-    assert.isTrue(timingStub.calledOnce);
-    assert.equal(timingStub.lastCall.args[0], key);
-    assert.equal(timingStub.lastCall.args[1], 100);
-
-    nowStub.returns(350);
-    element.recordDraftInteraction();
-    assert.isTrue(timingStub.calledTwice);
-    assert.equal(timingStub.lastCall.args[0], key);
-    assert.equal(timingStub.lastCall.args[1], 150);
-
-    nowStub.returns(370 + 2 * 60 * 1000);
-    element.recordDraftInteraction();
-    assert.isFalse(timingStub.calledThrice);
-  });
-
-  test('timeEndWithAverage', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(0);
-    nowStub.returns(1000);
-    element.time('foo');
-    nowStub.returns(1100);
-    element.timeEndWithAverage('foo', 'bar', 10);
-    assert.isTrue(element.reporter.calledTwice);
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'foo', 100));
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'bar', 10));
-  });
-
-  test('reportExtension', () => {
-    element.reportExtension('foo');
-    assert.isTrue(element.reporter.calledWithExactly(
-        'lifecycle', 'Extension detected', 'foo'
-    ));
-  });
-
-  test('reportInteraction', () => {
-    element.reporter.restore();
-    sandbox.spy(element, '_reportEvent');
-    element.pluginsLoaded(); // so we don't cache
-    element.reportInteraction('button-click', {name: 'sendReply'});
-    assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-        {
-          type: 'interaction',
-          name: 'button-click',
-          eventDetails: JSON.stringify({name: 'sendReply'}),
-        }
-    ));
-  });
-
-  test('report start time', () => {
-    element.reporter.restore();
-    sandbox.stub(element, 'now').returns(42);
-    sandbox.spy(element, '_reportEvent');
-    const dispatchStub = sandbox.spy(document, 'dispatchEvent');
-    element.pluginsLoaded();
-    element.time('timeAction');
-    element.timeEnd('timeAction');
-    assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-        {
-          type: 'timing-report',
-          category: 'UI Latency',
-          name: 'timeAction',
-          value: 0,
-          eventStart: 42,
-        }
-    ));
-    assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
-  });
-
-  suite('plugins', () => {
-    setup(() => {
-      element.reporter.restore();
-      sandbox.stub(element, '_reportEvent');
-    });
-
-    test('pluginsLoaded reports time', () => {
-      sandbox.stub(element, 'now').returns(42);
-      element.pluginsLoaded();
-      assert.isTrue(element._reportEvent.calledWithMatch(
-          {
-            type: 'timing-report',
-            category: 'UI Latency',
-            name: 'PluginsLoaded',
-            value: 42,
-          }
-      ));
-    });
-
-    test('pluginsLoaded reports plugins', () => {
-      element.pluginsLoaded(['foo', 'bar']);
-      assert.isTrue(element._reportEvent.calledWithMatch(
-          {
-            type: 'lifecycle',
-            category: 'Plugins installed',
-            eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
-          }
-      ));
-    });
-
-    test('caches reports if plugins are not loaded', () => {
-      element.timeEnd('foo');
-      assert.isFalse(element._reportEvent.called);
-    });
-
-    test('reports if plugins are loaded', () => {
-      element.pluginsLoaded();
-      assert.isTrue(element._reportEvent.called);
-    });
-
-    test('reports if metrics plugin xyz is loaded', () => {
-      element.pluginLoaded('metrics-xyz');
-      assert.isTrue(element._reportEvent.called);
-    });
-
-    test('reports cached events preserving order', () => {
-      element.time('foo');
-      element.time('bar');
-      element.timeEnd('foo');
-      element.pluginsLoaded();
-      element.timeEnd('bar');
-      assert.isTrue(element._reportEvent.getCall(0).calledWithMatch(
-          {type: 'timing-report', category: 'UI Latency', name: 'foo'}
-      ));
-      assert.isTrue(element._reportEvent.getCall(1).calledWithMatch(
-          {type: 'timing-report', category: 'UI Latency',
-            name: 'PluginsLoaded'}
-      ));
-      assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-          {type: 'lifecycle', category: 'Plugins installed'}
-      ));
-      assert.isTrue(element._reportEvent.getCall(3).calledWithMatch(
-          {type: 'timing-report', category: 'UI Latency', name: 'bar'}
-      ));
-    });
-  });
-
-  test('search', () => {
-    element.locationChanged('_handleSomeRoute');
-    assert.isTrue(element.reporter.calledWithExactly(
-        'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
-  });
-
-  suite('exception logging', () => {
-    let fakeWindow;
-    let reporter;
-
-    const emulateThrow = function(msg, url, line, column, error) {
-      return fakeWindow.onerror(msg, url, line, column, error);
-    };
-
-    setup(() => {
-      reporter = sandbox.stub(GrReporting.prototype, 'reporter');
-      fakeWindow = {
-        handlers: {},
-        addEventListener(type, handler) {
-          this.handlers[type] = handler;
-        },
-      };
-      sandbox.stub(console, 'error');
-      window.GrReporting._catchErrors(fakeWindow);
-    });
-
-    test('is reported', () => {
-      const error = new Error('bar');
-      error.stack = undefined;
-      emulateThrow('bar', 'http://url', 4, 2, error);
-      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
-      const payload = reporter.lastCall.args[3];
-      assert.deepEqual(payload, {
-        url: 'http://url',
-        line: 4,
-        column: 2,
-        error,
-      });
-    });
-
-    test('is reported with 3 lines of stack', () => {
-      const error = new Error('bar');
-      emulateThrow('bar', 'http://url', 4, 2, error);
-      const expectedStack = error.stack.split('\n').slice(0, 3)
-          .join('\n');
-      assert.isTrue(reporter.calledWith('error', 'exception',
-          expectedStack));
-    });
-
-    test('prevent default event handler', () => {
-      assert.isTrue(emulateThrow());
-    });
-
-    test('unhandled rejection', () => {
-      fakeWindow.handlers['unhandledrejection']({
-        reason: {
-          message: 'bar',
-        },
-      });
-      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
-    });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 11465ba..c1cf169 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-reporting/gr-reporting.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -28,6 +25,7 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
 
 const RoutePattern = {
   ROOT: '/',
@@ -145,7 +143,7 @@
   DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
 
   // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
-  DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
+  DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(#\d+)?$/,
 
   // Matches non-project-relative
   // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
@@ -161,7 +159,7 @@
 
   // Matches /c/<changeNum>/ /<URL tail>
   // Catches improperly encoded URLs (context: Issue 7100)
-  IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
+  IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/ \/(.+)$/,
 
   PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
 
@@ -197,7 +195,7 @@
 
 const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
 
-const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+const REPO_TOKEN_PATTERN = /\${(project|repo)}/g;
 
 // Polymer makes `app` intrinsically defined on the window by virtue of the
 // custom element having the id "app", but it is made explicit here.
@@ -210,15 +208,13 @@
 
 // Setup listeners outside of the router component initialization.
 (function() {
-  const reporting = document.createElement('gr-reporting');
-
   window.addEventListener('WebComponentsReady', () => {
-    reporting.timeEnd('WebComponentsReady');
+    appContext.reportingService.timeEnd('WebComponentsReady');
   });
 })();
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRouter extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -247,6 +243,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   start() {
     if (!this._app) { return; }
     this._startRouter();
@@ -716,7 +717,7 @@
         (ctx, next) => this._loadUserMiddleware(ctx, next),
         (ctx, next) => this._queryStringMiddleware(ctx, next),
         data => {
-          this.$.reporting.locationChanged(handlerName);
+          this.reporting.locationChanged(handlerName);
           const promise = opt_authRedirect ?
             this._redirectIfNotLoggedIn(data) : Promise.resolve();
           promise.then(() => { this[handlerName](data); });
@@ -738,7 +739,7 @@
 
     page.exit('*', (ctx, next) => {
       if (!this._isRedirecting) {
-        this.$.reporting.beforeLocationChanged();
+        this.reporting.beforeLocationChanged();
       }
       this._isRedirecting = false;
       this._isInitialLoad = false;
@@ -1089,7 +1090,7 @@
       project,
       dashboard: decodeURIComponent(data.params[1]),
     });
-    this.$.reporting.setRepoName(project);
+    this.reporting.setRepoName(project);
   }
 
   _handleGroupInfoRoute(data) {
@@ -1170,7 +1171,7 @@
       detail: GerritNav.RepoDetailView.COMMANDS,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handleRepoAccessRoute(data) {
@@ -1180,7 +1181,7 @@
       detail: GerritNav.RepoDetailView.ACCESS,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handleRepoDashboardsRoute(data) {
@@ -1190,7 +1191,7 @@
       detail: GerritNav.RepoDetailView.DASHBOARDS,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handleBranchListOffsetRoute(data) {
@@ -1296,7 +1297,7 @@
       view: GerritNav.View.REPO,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handlePluginListOffsetRoute(data) {
@@ -1359,7 +1360,7 @@
       queryMap: ctx.queryMap,
     };
 
-    this.$.reporting.setRepoName(params.project);
+    this.reporting.setRepoName(params.project);
     this._redirectOrNavigate(params);
   }
 
@@ -1379,7 +1380,7 @@
       params.leftSide = address.leftSide;
       params.lineNum = address.lineNum;
     }
-    this.$.reporting.setRepoName(params.project);
+    this.reporting.setRepoName(params.project);
     this._redirectOrNavigate(params);
   }
 
@@ -1430,7 +1431,7 @@
       lineNum: ctx.hash,
       view: GerritNav.View.EDIT,
     });
-    this.$.reporting.setRepoName(project);
+    this.reporting.setRepoName(project);
   }
 
   _handleChangeEditRoute(ctx) {
@@ -1443,7 +1444,7 @@
       view: GerritNav.View.CHANGE,
       edit: true,
     });
-    this.$.reporting.setRepoName(project);
+    this.reporting.setRepoName(project);
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
index 07f067e..8aa0835 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
@@ -18,5 +18,4 @@
 
 export const htmlTemplate = html`
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index f638f14..39d00069 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
@@ -114,7 +113,7 @@
 const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSearchBar extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -172,6 +171,14 @@
         type: Number,
         value: 1,
       },
+      /**
+       * Invisible label for input element. This label is exposed to
+       * screen readers by nested element
+       */
+      label: {
+        type: String,
+        value: '',
+      },
     };
   }
 
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
index e26f8a3..b0ef7af 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
@@ -30,6 +30,7 @@
   </style>
   <form>
     <gr-autocomplete
+      label="[[label]]"
       show-search-icon=""
       id="searchInput"
       text="{{_inputVal}}"
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 3b37e09..9e5fdfe 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -26,12 +26,6 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script src="/node_modules/page/page.js"></script>
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-search-bar.js';
-void (0);
-</script>
-
 <test-fixture id="basic">
   <template>
     <gr-search-bar></gr-search-bar>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index dcece30..2d1212b 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-search-bar/gr-search-bar.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -31,7 +29,7 @@
 const ME_EXPRESSION = 'me';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSmartSearch extends mixinBehaviors( [
   DisplayNameBehavior,
@@ -64,6 +62,14 @@
           return this._fetchAccounts.bind(this);
         },
       },
+      /**
+       * Invisible label for input element. This label is exposed to
+       * screen readers by nested element
+       */
+      label: {
+        type: String,
+        value: '',
+      },
     };
   }
 
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
index bb741ce..d490308 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
@@ -20,6 +20,7 @@
   <style include="shared-styles"></style>
   <gr-search-bar
     id="search"
+    label="[[label]]"
     value="{{searchQuery}}"
     on-handle-search="_handleSearch"
     project-suggestions="[[_projectSuggestions]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
index e2dda2f..16dee09 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -29,7 +27,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrApplyFixDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -120,10 +118,9 @@
         .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
         .then(res => {
           if (res != null) {
-            const previews = Object.keys(res).map(key => {
+            this._currentPreviews = Object.keys(res).map(key => {
               return {filepath: key, preview: res[key]};
             });
-            this._currentPreviews = previews;
           }
         })
         .catch(err => {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
index 8874f71..65958c8 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -21,12 +21,6 @@
 
 <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../../test/common-test-setup.js';
-import './gr-apply-fix-dialog.js';
-void (0);
-</script>
 
 <test-fixture id='basic'>
   <template>
@@ -36,7 +30,6 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import '../../../test/common-test-setup.js';
 import './gr-apply-fix-dialog.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 3f7da5a..040b0bd 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-comment-api_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 
 const PARENT = 'PARENT';
 
@@ -47,12 +45,33 @@
     this._getParentIndex =
       PatchSetBehavior.getParentIndex;
 
-    this._comments = comments || {};
-    this._robotComments = robotComments || {};
-    this._drafts = drafts || {};
+    this._comments = this._addPath(comments);
+    this._robotComments = this._addPath(robotComments);
+    this._drafts = this._addPath(drafts);
     this._changeNum = changeNum;
   }
 
+  /**
+   * Add path info to every comment as CommentInfo returned
+   * from server does not have that.
+   *
+   * TODO(taoalpha): should consider changing BE to send path
+   * back within CommentInfo
+   *
+   * @param {Object} - map between file path and comments
+   */
+  _addPath(comments = {}) {
+    const updatedComments = {};
+    for (const filePath of Object.keys(comments)) {
+      const allCommentsForPath = comments[filePath] || [];
+      if (allCommentsForPath.length) {
+        updatedComments[filePath] = allCommentsForPath
+            .map(comment => { return {...comment, path: filePath}; });
+      }
+    }
+    return updatedComments;
+  }
+
   get comments() {
     return this._comments;
   }
@@ -160,19 +179,17 @@
     const paths = this.getPaths();
     const publishedComments = {};
     for (const path of Object.keys(paths)) {
-      let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
-      if (opt_includeDrafts) {
-        const drafts = this.getAllDraftsForPath(path, opt_patchNum)
-            .map(d => Object.assign({__draft: true}, d));
-        commentsToAdd = commentsToAdd.concat(drafts);
-      }
-      publishedComments[path] = commentsToAdd;
+      publishedComments[path] = this.getAllCommentsForPath(
+          path,
+          opt_patchNum,
+          opt_includeDrafts
+      );
     }
     return publishedComments;
   }
 
   /**
-   * Gets all the comments and robot comments for the given change.
+   * Gets all the drafts for the given change.
    *
    * @param {number=} opt_patchNum
    * @return {!Object}
@@ -189,6 +206,9 @@
   /**
    * Get the comments (robot comments) for a path and optional patch num.
    *
+   * This method will always return a new shallow copy of all comments,
+   * so manipulation on one copy won't affect other copies.
+   *
    * @param {!string} path
    * @param {number=} opt_patchNum
    * @param {boolean=} opt_includeDrafts
@@ -200,14 +220,15 @@
     const robotComments = this._robotComments[path] || [];
     let allComments = comments.concat(robotComments);
     if (opt_includeDrafts) {
-      const drafts = this.getAllDraftsForPath(path)
-          .map(d => Object.assign({__draft: true}, d));
+      const drafts = this.getAllDraftsForPath(path);
       allComments = allComments.concat(drafts);
     }
-    if (!opt_patchNum) { return allComments; }
-    return (allComments || []).filter(c =>
-      this._patchNumEquals(c.patch_set, opt_patchNum)
-    );
+    if (opt_patchNum) {
+      allComments = allComments.filter(c =>
+        this._patchNumEquals(c.patch_set, opt_patchNum)
+      );
+    }
+    return allComments.map(c => { return {...c}; });
   }
 
   /**
@@ -215,7 +236,7 @@
    *
    * // TODO(taoalpha): maybe merge in *ForPath
    *
-   * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {!{path: string, basePath?: string, patchNum?: number}} file
    * @param {boolean=} opt_includeDrafts
    * @return {!Array}
    */
@@ -224,10 +245,10 @@
         file.path, file.patchNum, opt_includeDrafts
     );
 
-    if (file.oldPath) {
+    if (file.basePath) {
       allComments = allComments.concat(
           this.getAllCommentsForPath(
-              file.oldPath, file.patchNum, opt_includeDrafts
+              file.basePath, file.patchNum, opt_includeDrafts
           )
       );
     }
@@ -238,17 +259,22 @@
   /**
    * Get the drafts for a path and optional patch num.
    *
+   * This will return a shallow copy of all drafts every time,
+   * so changes on any copy will not affect other copies.
+   *
    * @param {!string} path
    * @param {number=} opt_patchNum
    * @return {!Array}
    */
   getAllDraftsForPath(path,
       opt_patchNum) {
-    const comments = this._drafts[path] || [];
-    if (!opt_patchNum) { return comments; }
-    return (comments || []).filter(c =>
-      this._patchNumEquals(c.patch_set, opt_patchNum)
-    );
+    let comments = this._drafts[path] || [];
+    if (opt_patchNum) {
+      comments = comments.filter(c =>
+        this._patchNumEquals(c.patch_set, opt_patchNum)
+      );
+    }
+    return comments.map(c => { return {...c, __draft: true}; });
   }
 
   /**
@@ -256,14 +282,14 @@
    *
    * // TODO(taoalpha): maybe merge in *ForPath
    *
-   * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {!{path: string, basePath?: string, patchNum?: number}} file
    * @return {!Array}
    */
   getAllDraftsForFile(file) {
     let allDrafts = this.getAllDraftsForPath(file.path, file.patchNum);
-    if (file.oldPath) {
+    if (file.basePath) {
       allDrafts = allDrafts.concat(
-          this.getAllDraftsForPath(file.oldPath, file.patchNum)
+          this.getAllDraftsForPath(file.basePath, file.patchNum)
       );
     }
     return allDrafts;
@@ -298,7 +324,8 @@
 
     drafts.forEach(d => { d.__draft = true; });
 
-    const all = comments.concat(drafts).concat(robotComments);
+    const all = comments.concat(drafts).concat(robotComments)
+        .map(c => { return {...c}; });
 
     const baseComments = all.filter(c =>
       this._isInBaseOfPatchRange(c, patchRange));
@@ -324,7 +351,7 @@
    *
    * // TODO(taoalpha): maybe merge *ForPath so find all comments in one pass
    *
-   * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {!{path: string, basePath?: string, patchNum?: number}} file
    * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum
    *     and basePatchNum properties to represent the range.
    * @param {Object=} opt_projectConfig Optional project config object to
@@ -335,13 +362,13 @@
     const comments = this.getCommentsBySideForPath(
         file.path, patchRange, opt_projectConfig
     );
-    if (file.oldPath) {
-      const commentsForOldPath = this.getCommentsBySideForPath(
-          file.oldPath, patchRange, opt_projectConfig
+    if (file.basePath) {
+      const commentsForBasePath = this.getCommentsBySideForPath(
+          file.basePath, patchRange, opt_projectConfig
       );
       // merge in the left and right
-      comments.left = comments.left.concat(commentsForOldPath.left);
-      comments.right = comments.right.concat(commentsForOldPath.right);
+      comments.left = comments.left.concat(commentsForBasePath.left);
+      comments.right = comments.right.concat(commentsForBasePath.right);
     }
     return comments;
   }
@@ -376,7 +403,7 @@
   /**
    * Computes a string counting the number of commens in a given file.
    *
-   * @param {{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {{path: string, basePath?: string, patchNum?: number}} file
    * @return {number}
    */
   computeCommentCount(file) {
@@ -391,7 +418,7 @@
    * Computes a string counting the number of draft comments in the entire
    * change, optionally filtered by path and/or patchNum.
    *
-   * @param {?{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {?{path: string, basePath?: string, patchNum?: number}} file
    * @return {number}
    */
   computeDraftCount(file) {
@@ -405,7 +432,7 @@
   /**
    * Computes a number of unresolved comment threads in a given file and path.
    *
-   * @param {{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {{path: string, basePath?: string, patchNum?: number}} file
    * @return {number}
    */
   computeUnresolvedNum(file) {
@@ -443,7 +470,7 @@
         .sort(
             (c1, c2) => {
               const dateDiff =
-                  util.parseDate(c1.updated) - util.parseDate(c2.updated);
+                  parseDate(c1.updated) - parseDate(c2.updated);
               if (dateDiff) {
                 return dateDiff;
               }
@@ -514,12 +541,9 @@
       return true;
     }
     // If the base of the range is not the parent of the patch:
-    if (range.basePatchNum !== PARENT &&
-      comment.side !== PARENT &&
-      this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
-      return true;
-    }
-    return false;
+    return range.basePatchNum !== PARENT &&
+        comment.side !== PARENT &&
+        this._patchNumEquals(comment.patch_set, range.basePatchNum);
   }
 
   /**
@@ -550,7 +574,7 @@
 }
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCommentApi extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index 29262e3..172a342 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -370,16 +370,24 @@
       test('getAllCommentsForPath', () => {
         let path = 'file/one';
         let comments = element._changeComments.getAllCommentsForPath(path);
-        assert.deepEqual(comments.length, 4);
+        assert.equal(comments.length, 4);
         path = 'file/two';
         comments = element._changeComments.getAllCommentsForPath(path, 2);
-        assert.deepEqual(comments.length, 1);
+        assert.equal(comments.length, 1);
+        const aCopyOfComments = element._changeComments
+            .getAllCommentsForPath(path, 2);
+        assert.deepEqual(comments, aCopyOfComments);
+        assert.notEqual(comments[0], aCopyOfComments[0]);
       });
 
       test('getAllDraftsForPath', () => {
         const path = 'file/one';
         const drafts = element._changeComments.getAllDraftsForPath(path);
-        assert.deepEqual(drafts.length, 2);
+        assert.equal(drafts.length, 2);
+        const aCopyOfDrafts = element._changeComments
+            .getAllDraftsForPath(path);
+        assert.deepEqual(drafts, aCopyOfDrafts);
+        assert.notEqual(drafts[0], aCopyOfDrafts[0]);
       });
 
       test('computeUnresolvedNum', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
index cdd6d8f..86537e8 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -29,7 +27,7 @@
   [CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
 ]);
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCoverageLayer extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index b38543c..c9d5d78 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-coverage-layer/gr-coverage-layer.js';
 import '../gr-diff-processor/gr-diff-processor.js';
 import '../../shared/gr-hovercard/gr-hovercard.js';
@@ -46,7 +44,7 @@
 const COMMIT_MSG_LINE_LENGTH = 72;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffBuilderElement extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -213,8 +211,8 @@
       null;
   }
 
-  getContentByLine(lineNumber, opt_side, opt_root) {
-    return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
+  getContentTdByLine(lineNumber, opt_side, opt_root) {
+    return this._builder.getContentTdByLine(lineNumber, opt_side, opt_root);
   }
 
   _getDiffRowByChild(child) {
@@ -224,14 +222,14 @@
     return child;
   }
 
-  getContentByLineEl(lineEl) {
+  getContentTdByLineEl(lineEl) {
     if (!lineEl) return;
     const line = lineEl.getAttribute('data-value');
     const side = this.getSideByLineEl(lineEl);
     // Performance optimization because we already have an element in the
     // correct row
     const row = dom(this._getDiffRowByChild(lineEl));
-    return this.getContentByLine(line, side, row);
+    return this.getContentTdByLine(line, side, row);
   }
 
   getLineElByNumber(lineNumber, opt_side) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index e5eec8d..057401b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -52,7 +52,7 @@
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff-builder-element.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
@@ -96,44 +96,70 @@
     assert.isTrue(node.classList.contains('classes'));
   });
 
-  test('context control buttons', () => {
-    // Create 10 lines.
-    const lines = [];
-    for (let i = 0; i < 10; i++) {
-      const line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.beforeNumber = i + 1;
-      line.afterNumber = i + 1;
-      line.text = 'lorem upsum';
-      lines.push(line);
+  suite('context control', () => {
+    function createContextLine(options) {
+      const offset = options.offset || 0;
+      const numLines = options.count || 10;
+      const lines = [];
+      for (let i = 0; i < numLines; i++) {
+        const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+        line.beforeNumber = offset + i + 1;
+        line.afterNumber = offset + i + 1;
+        line.text = 'lorem upsum';
+        lines.push(line);
+      }
+
+      return {
+        contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
+      };
     }
 
-    const contextLine = {
-      contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
-    };
+    test('no +10 buttons for 10 or less lines', () => {
+      const contextLine = createContextLine({count: 10});
 
-    const section = {};
-    // Does not include +10 buttons when there are fewer than 11 lines.
-    let td = builder._createContextControl(section, contextLine);
-    let buttons = td.querySelectorAll('gr-button.showContext');
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
 
-    assert.equal(buttons.length, 1);
-    assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines');
+      assert.equal(buttons.length, 1);
+      assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines');
+    });
 
-    // Add another line.
-    const line = new GrDiffLine(GrDiffLine.Type.BOTH);
-    line.text = 'lorem upsum';
-    line.beforeNumber = 11;
-    line.afterNumber = 11;
-    contextLine.contextGroups[0].addLine(line);
+    test('context control at the top', () => {
+      const contextLine = createContextLine({offset: 0, count: 20});
 
-    // Includes +10 buttons when there are at least 11 lines.
-    td = builder._createContextControl(section, contextLine);
-    buttons = td.querySelectorAll('gr-button.showContext');
+      builder._numLinesLeft = 50;
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
 
-    assert.equal(buttons.length, 3);
-    assert.equal(dom(buttons[0]).textContent, '+10 above');
-    assert.equal(dom(buttons[1]).textContent, 'Show 11 common lines');
-    assert.equal(dom(buttons[2]).textContent, '+10 below');
+      assert.equal(buttons.length, 2);
+      assert.equal(dom(buttons[0]).textContent, 'Show 20 common lines');
+      assert.equal(dom(buttons[1]).textContent, '+10 below');
+    });
+
+    test('context control in the middle', () => {
+      const contextLine = createContextLine({offset: 10, count: 20});
+
+      builder._numLinesLeft = 50;
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
+
+      assert.equal(buttons.length, 3);
+      assert.equal(dom(buttons[0]).textContent, '+10 above');
+      assert.equal(dom(buttons[1]).textContent, 'Show 20 common lines');
+      assert.equal(dom(buttons[2]).textContent, '+10 below');
+    });
+
+    test('context control at the top', () => {
+      const contextLine = createContextLine({offset: 30, count: 20});
+
+      builder._numLinesLeft = 50;
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
+
+      assert.equal(buttons.length, 2);
+      assert.equal(dom(buttons[0]).textContent, '+10 above');
+      assert.equal(dom(buttons[1]).textContent, 'Show 20 common lines');
+    });
   });
 
   test('newlines 1', () => {
@@ -263,7 +289,6 @@
     expectTextLength('abc\t', 8, 8);
     expectTextLength('abc\t\t', 10, 20);
     expectTextLength('', 10, 0);
-    expectTextLength('', 10, 0);
     // 17 Thai combining chars.
     expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
     expectTextLength('abc\tde', 10, 12);
@@ -948,22 +973,24 @@
       assert.equal(actual.textContent, diff.content[1].b[0]);
     });
 
-    test('getContentByLineEl works both with button and td', () => {
+    test('getContentTdByLineEl works both with button and td', () => {
       const diffRow = element.diffElement.querySelectorAll('tr.diff-row')[2];
 
       const lineNumTdLeft = diffRow.querySelector('td.lineNum.left');
       const lineNumButtonLeft = lineNumTdLeft.querySelector('button');
-      const contentLeft = diffRow.querySelectorAll('.contentText')[0];
+      const contentTdLeft = diffRow.querySelectorAll('.content')[0];
 
       const lineNumTdRight = diffRow.querySelector('td.lineNum.right');
       const lineNumButtonRight = lineNumTdRight.querySelector('button');
-      const contentRight = diffRow.querySelectorAll('.contentText')[1];
+      const contentTdRight = diffRow.querySelectorAll('.content')[1];
 
-      assert.equal(element.getContentByLineEl(lineNumTdLeft), contentLeft);
-      assert.equal(element.getContentByLineEl(lineNumButtonLeft), contentLeft);
-      assert.equal(element.getContentByLineEl(lineNumTdRight), contentRight);
+      assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
       assert.equal(
-          element.getContentByLineEl(lineNumButtonRight), contentRight);
+          element.getContentTdByLineEl(lineNumButtonLeft), contentTdLeft);
+      assert.equal(
+          element.getContentTdByLineEl(lineNumTdRight), contentTdRight);
+      assert.equal(
+          element.getContentTdByLineEl(lineNumButtonRight), contentTdRight);
     });
 
     test('findLinesByRange', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 1fc0d4f..6983af0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -16,6 +16,7 @@
  */
 
 import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
 // MIME types for images we allow showing. Do not include SVG, it can contain
 // arbitrary JavaScript.
@@ -52,7 +53,7 @@
   // column limit.
   td.setAttribute('colspan', '4');
   const endpoint = this._createElement('gr-endpoint-decorator');
-  const endpointDomApi = Polymer.dom(endpoint);
+  const endpointDomApi = dom(endpoint);
   endpointDomApi.setAttribute('name', 'image-diff');
   endpointDomApi.appendChild(
       this._createEndpointParam('baseImage', this._baseImage));
@@ -106,7 +107,7 @@
 
 GrDiffBuilderImage.prototype._updateImageLabel = function(section, className,
     image) {
-  const label = Polymer.dom(section)
+  const label = dom(section)
       .querySelector('.' + className + ' span.label');
   this._setLabelText(label, image);
 };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 4b91000..c2279e2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -16,6 +16,7 @@
  */
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
 /**
  * In JS, unicode code points above 0xFFFF occupy two elements of a string.
@@ -41,6 +42,11 @@
 
 export function GrDiffBuilder(diff, prefs, outputEl, layers) {
   this._diff = diff;
+  this._numLinesLeft = this._diff.content ? this._diff.content.reduce(
+      (sum, chunk) => {
+        const left = chunk.a || chunk.ab;
+        return sum + (left ? left.length : 0);
+      }, 0) : 0;
   this._prefs = prefs;
   this._outputEl = outputEl;
   this.groups = [];
@@ -140,12 +146,18 @@
   return groups;
 };
 
-GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side,
-    opt_root) {
-  const root = Polymer.dom(opt_root || this._outputEl);
+GrDiffBuilder.prototype.getContentTdByLine = function(
+    lineNumber, opt_side, opt_root) {
+  const root = dom(opt_root || this._outputEl);
   const sideSelector = opt_side ? ('.' + opt_side) : '';
   return root.querySelector('td.lineNum[data-value="' + lineNumber +
-      '"]' + sideSelector + ' ~ td.content .contentText');
+    '"]' + sideSelector + ' ~ td.content');
+};
+
+GrDiffBuilder.prototype.getContentByLine = function(
+    lineNumber, opt_side, opt_root) {
+  return this.getContentTdByLine(lineNumber, opt_side, opt_root)
+      .querySelector('.contentText');
 };
 
 /**
@@ -221,16 +233,18 @@
 GrDiffBuilder.prototype._createContextControl = function(section, line) {
   if (!line.contextGroups) return null;
 
-  const numLines =
-      line.contextGroups[line.contextGroups.length - 1].lineRange.left.end -
-      line.contextGroups[0].lineRange.left.start + 1;
+  const leftStart = line.contextGroups[0].lineRange.left.start;
+  const leftEnd =
+      line.contextGroups[line.contextGroups.length - 1].lineRange.left.end;
+
+  const numLines = leftEnd - leftStart + 1;
 
   if (numLines === 0) return null;
 
   const td = this._createElement('td');
   const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
 
-  if (showPartialLinks) {
+  if (showPartialLinks && leftStart > 1) {
     td.appendChild(this._createContextButton(
         GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
   }
@@ -238,7 +252,7 @@
   td.appendChild(this._createContextButton(
       GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
 
-  if (showPartialLinks) {
+  if (showPartialLinks && leftEnd < this._numLinesLeft) {
     td.appendChild(this._createContextButton(
         GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
   }
@@ -259,7 +273,7 @@
   if (type === GrDiffBuilder.ContextButtonType.ALL) {
     const icon = this._createElement('iron-icon', 'showContext');
     icon.setAttribute('icon', 'gr-icons:unfold-more');
-    Polymer.dom(button).appendChild(icon);
+    dom(button).appendChild(icon);
 
     text = 'Show ' + numLines + ' common line';
     if (numLines > 1) { text += 's'; }
@@ -274,8 +288,8 @@
         0, numLines - context);
   }
   const textSpan = this._createElement('span', 'showContext');
-  Polymer.dom(textSpan).textContent = text;
-  Polymer.dom(button).appendChild(textSpan);
+  dom(textSpan).textContent = text;
+  dom(button).appendChild(textSpan);
 
   button.addEventListener('tap', e => {
     e.detail = {
@@ -301,15 +315,20 @@
   }
 
   if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
-    const button = this._createElement('button');
-    button.tabIndex = -1;
-    td.appendChild(button);
-
     // Both td and button need a number of classes/attributes for various
     // selectors to work.
     this._decorateLineEl(td, number, side);
-    td.classList.add('lineNum');
+
+    if (this._prefs.show_file_comment_button === false && number === 'FILE') {
+      return td;
+    }
+
+    const button = this._createElement('button');
+    td.appendChild(button);
+    button.tabIndex = -1;
     this._decorateLineEl(button, number, side);
+
+    td.classList.add('lineNum');
     button.classList.add('lineNumButton');
 
     button.textContent = number === 'FILE' ? 'File' : number;
@@ -349,22 +368,26 @@
   }
   td.classList.add(line.type);
 
-  const lineLimit =
-      !this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
+  if (line.beforeNumber !== 'FILE') {
+    const lineLimit =
+        !this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
+    const contentText =
+        this._formatText(line.text, this._prefs.tab_size, lineLimit);
 
-  const contentText =
-      this._formatText(line.text, this._prefs.tab_size, lineLimit);
-  if (opt_side) {
-    contentText.setAttribute('data-side', opt_side);
-  }
-
-  for (const layer of this.layers) {
-    if (typeof layer.annotate == 'function') {
-      layer.annotate(contentText, lineNumberEl, line);
+    if (opt_side) {
+      contentText.setAttribute('data-side', opt_side);
     }
-  }
 
-  td.appendChild(contentText);
+    for (const layer of this.layers) {
+      if (typeof layer.annotate == 'function') {
+        layer.annotate(contentText, lineNumberEl, line);
+      }
+    }
+
+    td.appendChild(contentText);
+  } else {
+    td.classList.add('file');
+  }
 
   return td;
 };
@@ -533,7 +556,7 @@
  * @return {HTMLTableDataCellElement}
  */
 GrDiffBuilder.prototype._getBlameByLineNum = function(lineNum) {
-  const root = Polymer.dom(this._outputEl);
+  const root = dom(this._outputEl);
   return root.querySelector(`td.blame[data-line-number="${lineNum}"]`);
 };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 0b9ae5b..acb8f0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
@@ -23,6 +22,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-cursor_html.js';
+import {ScrollMode} from '../../../constants/constants.js';
 
 const DiffSides = {
   LEFT: 'left',
@@ -34,15 +34,10 @@
   UNIFIED: 'UNIFIED_DIFF',
 };
 
-const ScrollBehavior = {
-  KEEP_VISIBLE: 'keep-visible',
-  NEVER: 'never',
-};
-
 const LEFT_SIDE_CLASS = 'target-side-left';
 const RIGHT_SIDE_CLASS = 'target-side-right';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDiffCursor extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
@@ -93,9 +88,9 @@
        * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
        * the viewport.
        */
-      _scrollBehavior: {
+      _scrollMode: {
         type: String,
-        value: ScrollBehavior.KEEP_VISIBLE,
+        value: ScrollMode.KEEP_VISIBLE,
       },
 
       _focusOnMove: {
@@ -294,10 +289,9 @@
   reInitCursor() {
     if (!this.diffRow) {
       // does not scroll during init unless requested
-      const scrollingBehaviorForInit = this.initialLineNumber ?
-        ScrollBehavior.KEEP_VISIBLE :
-        ScrollBehavior.NEVER;
-      this._scrollBehavior = scrollingBehaviorForInit;
+      this._scrollMode = this.initialLineNumber ?
+        ScrollMode.KEEP_VISIBLE :
+        ScrollMode.NEVER;
       if (this.initialLineNumber) {
         this.moveToLineNumber(this.initialLineNumber, this.side);
         this.initialLineNumber = null;
@@ -309,17 +303,22 @@
   }
 
   reInit() {
-    this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
+    this._scrollMode = ScrollMode.KEEP_VISIBLE;
   }
 
   _handleWindowScroll() {
     if (this._preventAutoScrollOnManualScroll) {
-      this._scrollBehavior = ScrollBehavior.NEVER;
+      this._scrollMode = ScrollMode.NEVER;
       this._focusOnMove = false;
       this._preventAutoScrollOnManualScroll = false;
     }
   }
 
+  reInitAndUpdateStops() {
+    this.reInit();
+    this._updateStops();
+  }
+
   handleDiffUpdate() {
     this._updateStops();
     this.reInitCursor();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
index 1ac47f6..e400792 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
@@ -19,7 +19,7 @@
 export const htmlTemplate = html`
   <gr-cursor-manager
     id="cursorManager"
-    scroll-behavior="[[_scrollBehavior]]"
+    scroll-mode="[[_scrollMode]]"
     cursor-target-class="target-row"
     focus-on-move="[[_focusOnMove]]"
     target="{{diffRow}}"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 77e5179..71fc341 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -44,7 +44,7 @@
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 suite('gr-diff-cursor tests', () => {
   let sandbox;
@@ -117,20 +117,20 @@
   });
 
   test('cursor scroll behavior', () => {
-    assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+    assert.equal(cursorElement._scrollMode, 'keep-visible');
 
     cursorElement._handleDiffRenderStart();
     assert.isTrue(cursorElement._focusOnMove);
 
     cursorElement._handleWindowScroll();
-    assert.equal(cursorElement._scrollBehavior, 'never');
+    assert.equal(cursorElement._scrollMode, 'never');
     assert.isFalse(cursorElement._focusOnMove);
 
     cursorElement._handleDiffRenderContent();
     assert.isTrue(cursorElement._focusOnMove);
 
     cursorElement.reInitCursor();
-    assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+    assert.equal(cursorElement._scrollMode, 'keep-visible');
   });
 
   test('moves to selected line', () => {
@@ -237,7 +237,7 @@
     cursorElement.moveToNextChunk();
 
     // Since this chunk only has content on the left side. we should have been
-    // automatically mvoed over.
+    // automatically moved over.
     const previousIndex = currentIndex;
     currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
     assert.equal(currentIndex, previousIndex + 1);
@@ -248,7 +248,7 @@
     let scrollBehaviorDuringMove;
     const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
     const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
-        () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+        () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
 
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
@@ -256,7 +256,7 @@
       assert.isFalse(moveToNumStub.called);
       assert.isTrue(moveToChunkStub.called);
       assert.equal(scrollBehaviorDuringMove, 'never');
-      assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+      assert.equal(cursorElement._scrollMode, 'keep-visible');
       done();
     }
     diffElement.addEventListener('render', renderHandler);
@@ -266,7 +266,7 @@
   test('initialLineNumber provided', done => {
     let scrollBehaviorDuringMove;
     const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
-        () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+        () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
     const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
@@ -276,7 +276,7 @@
       assert.equal(moveToNumStub.lastCall.args[0], 10);
       assert.equal(moveToNumStub.lastCall.args[1], 'right');
       assert.equal(scrollBehaviorDuringMove, 'keep-visible');
-      assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+      assert.equal(cursorElement._scrollMode, 'keep-visible');
       done();
     }
     diffElement.addEventListener('render', renderHandler);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
index 4006d13..c86760a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -14,6 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {sanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js';
 
 // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
 const ANNOTATION_TAG = 'HL';
@@ -84,7 +86,7 @@
     }
 
     const wrapper = document.createElement(tagName);
-    const sanitizer = window.Polymer.sanitizeDOMValue;
+    const sanitizer = sanitizeDOMValue;
     for (const [name, value] of Object.entries(attributes)) {
       wrapper.setAttribute(
           name, sanitizer ?
@@ -149,8 +151,8 @@
     } else {
       hl = document.createElement(ANNOTATION_TAG);
       hl.className = cssClass;
-      Polymer.dom(node.parentElement).replaceChild(hl, node);
-      Polymer.dom(hl).appendChild(node);
+      dom(node.parentElement).replaceChild(hl, node);
+      dom(hl).appendChild(node);
     }
     return hl;
   },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 53b416b..229ae43 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-selection-action-box/gr-selection-action-box.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -27,7 +25,7 @@
 import {GrRangeNormalizer} from './gr-range-normalizer.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffHighlight extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -292,11 +290,8 @@
     if (!line) {
       return;
     }
-    const contentText = this.diffBuilder.getContentByLineEl(lineEl);
-    if (!contentText) {
-      return;
-    }
-    const contentTd = contentText.parentElement;
+    const contentTd = this.diffBuilder.getContentTdByLineEl(lineEl);
+    const contentText = contentTd.querySelector('.contentText');
     if (!contentTd.contains(node)) {
       node = contentText;
       column = 0;
@@ -340,12 +335,9 @@
     }
     const start = range.start;
     const end = range.end;
-    if (start.side !== end.side ||
+    return !(start.side !== end.side ||
         end.line < start.line ||
-        (start.line === end.line && start.column === end.column)) {
-      return false;
-    }
-    return true;
+        (start.line === end.line && start.column === end.column));
   }
 
   _handleSelection(selection, isMouseUp) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
index 08b21499..afd4ac5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
@@ -23,7 +23,7 @@
     }
     gr-selection-action-box {
       /**
-         * Needs z-index to apear above wrapped content, since it's inseted
+         * Needs z-index to appear above wrapped content, since it's inserted
          * into DOM before it.
          */
       z-index: 10;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 86f1505..ec7e6d2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -267,9 +267,9 @@
         contentTd,
         contentText,
       });
-      builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
+      builder.getContentTdByLineEl.withArgs(lineEl).returns(contentTd);
       builder.getLineNumberByChild.withArgs(lineEl).returns(line);
-      builder.getContentByLine.withArgs(line, side).returns(contentText);
+      builder.getContentTdByLine.withArgs(line, side).returns(contentTd);
       builder.getSideByLineEl.withArgs(lineEl).returns(side);
       return contentText;
     };
@@ -296,8 +296,8 @@
       });
       diff = element.querySelector('#diffTable');
       builder = {
-        getContentByLine: sandbox.stub(),
-        getContentByLineEl: sandbox.stub(),
+        getContentTdByLine: sandbox.stub(),
+        getContentTdByLineEl: sandbox.stub(),
         getLineElByChild,
         getLineNumberByChild: sandbox.stub(),
         getSideByLineEl: sandbox.stub(),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 2ed69b6..cb299de 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-comment-thread/gr-comment-thread.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
@@ -30,9 +27,10 @@
 import {htmlTemplate} from './gr-diff-host_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {GrDiffBuilder} from '../gr-diff-builder/gr-diff-builder.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {DiffSide, rangesEqual} from '../gr-diff/gr-diff-utils.js';
+import {appContext} from '../../../services/app-context.js';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -85,7 +83,7 @@
  * Webcomponent fetching diffs and related data from restAPI and passing them
  * to the presentational gr-diff for rendering.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffHost extends mixinBehaviors( [
   PatchSetBehavior,
@@ -122,6 +120,9 @@
       },
       /** @type {?} */
       patchRange: Object,
+      /** @type {!Gerrit.FileRange} */
+      file: Object,
+      // TODO: deprecate path since that info is included in file
       path: String,
       prefs: {
         type: Object,
@@ -217,6 +218,11 @@
         notify: true,
       },
 
+      _fetchDiffPromise: {
+        type: Object,
+        value: null,
+      },
+
       /** @type {?Object} */
       _blame: {
         type: Object,
@@ -259,6 +265,11 @@
     ];
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -360,21 +371,21 @@
               const needsSyntaxHighlighting = event.detail &&
                     event.detail.contentRendered;
               if (needsSyntaxHighlighting) {
-                this.$.reporting.time(TimingLabel.SYNTAX);
-                this.$.syntaxLayer.process().then(() => {
-                  this.$.reporting.timeEnd(TimingLabel.SYNTAX);
-                  this.$.reporting.timeEnd(TimingLabel.TOTAL);
+                this.reporting.time(TimingLabel.SYNTAX);
+                this.$.syntaxLayer.process().finally(() => {
+                  this.reporting.timeEnd(TimingLabel.SYNTAX);
+                  this.reporting.timeEnd(TimingLabel.TOTAL);
                   resolve();
                 });
               } else {
-                this.$.reporting.timeEnd(TimingLabel.TOTAL);
+                this.reporting.timeEnd(TimingLabel.TOTAL);
                 resolve();
               }
               this.removeEventListener('render', callback);
               if (shouldReportMetric) {
                 // We report diffViewContentDisplayed only on reload caused
                 // by params changed - expected only on Diff Page.
-                this.$.reporting.diffViewContentDisplayed();
+                this.reporting.diffViewContentDisplayed();
               }
             };
             this.addEventListener('render', callback);
@@ -448,6 +459,7 @@
   /** Cancel any remaining diff builder rendering work. */
   cancel() {
     this.$.diff.cancel();
+    this.$.syntaxLayer.cancel();
   }
 
   /** @return {!Array<!HTMLElement>} */
@@ -528,8 +540,21 @@
         !this.noAutoRender;
   }
 
+  // TODO(milutin): Use rest-api with fetchCacheURL instead of this.
+  prefetchDiff() {
+    if (!!this.changeNum && !!this.patchRange && !!this.path
+        && this._fetchDiffPromise === null) {
+      this._fetchDiffPromise = this._getDiff();
+    }
+  }
+
   /** @return {!Promise<!Object>} */
   _getDiff() {
+    if (this._fetchDiffPromise !== null) {
+      const fetchDiffPromise = this._fetchDiffPromise;
+      this._fetchDiffPromise = null;
+      return fetchDiffPromise;
+    }
     // Wrap the diff request in a new promise so that the error handler
     // rejects the promise, allowing the error to be handled in the .catch.
     return new Promise((resolve, reject) => {
@@ -602,11 +627,11 @@
     // Report the due_to_rebase percentage in the "diff" category when
     // applicable.
     if (this.patchRange.basePatchNum === 'PARENT') {
-      this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
+      this.reporting.reportInteraction(EVENT_AGAINST_PARENT);
     } else if (percentRebaseDelta === 0) {
-      this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
+      this.reporting.reportInteraction(EVENT_ZERO_REBASE);
     } else {
-      this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
+      this.reporting.reportInteraction(EVENT_NONZERO_REBASE,
           {percentRebaseDelta});
     }
   }
@@ -661,7 +686,7 @@
     return comments.slice(0).sort((a, b) => {
       if (b.__draft && !a.__draft ) { return -1; }
       if (a.__draft && !b.__draft ) { return 1; }
-      return util.parseDate(a.updated) - util.parseDate(b.updated);
+      return parseDate(a.updated) - parseDate(b.updated);
     });
   }
 
@@ -726,7 +751,7 @@
         isOnParent);
     threadEl.addOrEditDraft(lineNum, range);
 
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
   }
 
   /**
@@ -775,14 +800,23 @@
     threadEl.commentSide = thread.commentSide;
     threadEl.isOnParent = !!thread.isOnParent;
     threadEl.parentIndex = this._parentIndex;
+    // Use path before renmaing when comment added on the left when comparing
+    // two patch sets (not against base)
+    if (this.file && this.file.basePath
+        && thread.commentSide === GrDiffBuilder.Side.LEFT
+        && !thread.isOnParent) {
+      threadEl.path = this.file.basePath;
+    } else {
+      threadEl.path = this.path;
+    }
     threadEl.changeNum = this.changeNum;
     threadEl.patchNum = thread.patchNum;
+    threadEl.showPatchset = false;
     threadEl.lineNum = thread.lineNum;
     const rootIdChangedListener = changeEvent => {
       thread.rootId = changeEvent.detail.value;
     };
     threadEl.addEventListener('root-id-changed', rootIdChangedListener);
-    threadEl.path = this.path;
     threadEl.projectName = this.projectName;
     threadEl.range = thread.range;
     const threadDiscardListener = e => {
@@ -891,6 +925,7 @@
       return;
     }
 
+    this._fetchDiffPromise = null;
     if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
         !noRenderOnPrefsChange) {
       this.reload();
@@ -941,7 +976,7 @@
   /**
    * Closure annotation for Polymer.prototype.push is off. Submitted PR:
    * https://github.com/Polymer/polymer/pull/4776
-   * but for not supressing annotations.
+   * but for not suppressing annotations.
    *
    * @suppress {checkTypes}
    */
@@ -1024,7 +1059,7 @@
   _listenToViewportRender() {
     const renderUpdateListener = start => {
       if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
-        this.$.reporting.diffViewDisplayed();
+        this.reporting.diffViewDisplayed();
         this.$.syntaxLayer.removeListener(renderUpdateListener);
       }
     };
@@ -1033,16 +1068,16 @@
   }
 
   _handleRenderStart() {
-    this.$.reporting.time(TimingLabel.TOTAL);
-    this.$.reporting.time(TimingLabel.CONTENT);
+    this.reporting.time(TimingLabel.TOTAL);
+    this.reporting.time(TimingLabel.CONTENT);
   }
 
   _handleRenderContent() {
-    this.$.reporting.timeEnd(TimingLabel.CONTENT);
+    this.reporting.timeEnd(TimingLabel.CONTENT);
   }
 
   _handleNormalizeRange(event) {
-    this.$.reporting.reportInteraction('normalize-range',
+    this.reporting.reportInteraction('normalize-range',
         {
           side: event.detail.side,
           lineNum: event.detail.lineNum,
@@ -1050,7 +1085,7 @@
   }
 
   _handleDiffContextExpanded(event) {
-    this.$.reporting.reportInteraction(
+    this.reporting.reportInteraction(
         'diff-context-expanded', {numLines: event.detail.numLines}
     );
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
index 4e425dc..aa05bb5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
@@ -53,5 +53,4 @@
   ></gr-syntax-layer>
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting" category="diff"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index 0cb2b5e..342c7ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -50,11 +50,9 @@
     stub('gr-rest-api-interface', {
       async getLoggedIn() { return getLoggedIn; },
     });
-    stub('gr-reporting', {
-      time: sandbox.stub(),
-      timeEnd: sandbox.stub(),
-    });
     element = fixture('basic');
+    sandbox.stub(element.reporting, 'time');
+    sandbox.stub(element.reporting, 'timeEnd');
   });
 
   teardown(() => {
@@ -307,9 +305,9 @@
     test('starts total and content timer on render-start', done => {
       element.dispatchEvent(
           new CustomEvent('render-start', {bubbles: true, composed: true}));
-      assert.isTrue(element.$.reporting.time.calledWithExactly(
+      assert.isTrue(element.reporting.time.calledWithExactly(
           'Diff Total Render'));
-      assert.isTrue(element.$.reporting.time.calledWithExactly(
+      assert.isTrue(element.reporting.time.calledWithExactly(
           'Diff Content Render'));
       done();
     });
@@ -317,11 +315,12 @@
     test('ends content timer on render-content', () => {
       element.dispatchEvent(
           new CustomEvent('render-content', {bubbles: true, composed: true}));
-      assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+      assert.isTrue(element.reporting.timeEnd.calledWithExactly(
           'Diff Content Render'));
     });
 
     test('ends total and syntax timer after syntax layer processing', done => {
+      sandbox.stub(element.reporting, 'diffViewContentDisplayed');
       let notifySyntaxProcessed;
       sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
           resolve => {
@@ -339,12 +338,11 @@
         notifySyntaxProcessed();
         // Assert after the notification task is processed.
         Promise.resolve().then(() => {
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+          assert.isTrue(element.reporting.timeEnd.calledWithExactly(
               'Diff Total Render'));
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+          assert.isTrue(element.reporting.timeEnd.calledWithExactly(
               'Diff Syntax Render'));
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
-              'StartupDiffViewOnlyContent'));
+          assert.isTrue(element.reporting.diffViewContentDisplayed.called);
           done();
         });
       });
@@ -357,8 +355,8 @@
       element.reload();
       // Multiple cascading microtasks are scheduled.
       setTimeout(() => {
-        assert.isTrue(element.$.reporting.timeEnd.calledOnce);
-        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+        assert.isTrue(element.reporting.timeEnd.calledOnce);
+        assert.isTrue(element.reporting.timeEnd.calledWithExactly(
             'Diff Total Render'));
         done();
       });
@@ -447,6 +445,19 @@
       });
     });
 
+    test('prefetch getDiff', done => {
+      const diffRestApiStub = sandbox.stub(element.$.restAPI, '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();
+      });
+    });
+
     test('_getDiff handles null diff responses', done => {
       stub('gr-rest-api-interface', {
         getDiff() { return Promise.resolve(null); },
@@ -1026,8 +1037,9 @@
 
     setup(() => {
       element = fixture('basic');
+      element.path = 'file.txt';
       element.patchRange = {basePatchNum: 1};
-      reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+      reportStub = sandbox.stub(element.reporting, 'reportInteraction');
     });
 
     test('null and content-less', () => {
@@ -1330,6 +1342,54 @@
     assert.equal(threads[1].patchNum, 3);
   });
 
+  test('thread should use old file path if first created' +
+   'on patch set (left) before renaming', () => {
+    const commentSide = 'left';
+    element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+    assert.isOk(element._getOrCreateThread('2', 3,
+        commentSide, undefined, /* isOnParent= */ false));
+
+    const threads = dom(element.$.diff)
+        .queryDistributedElements('gr-comment-thread');
+
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].path, element.file.basePath);
+  });
+
+  test('thread should use new file path if first created' +
+   'on patch set (right) after renaming', () => {
+    const commentSide = 'right';
+    element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+    assert.isOk(element._getOrCreateThread('2', 3,
+        commentSide, undefined, /* isOnParent= */ false));
+
+    const threads = dom(element.$.diff)
+        .queryDistributedElements('gr-comment-thread');
+
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].path, element.file.path);
+  });
+
+  test('thread should use new file path if first created' +
+   'on patch set (left) but is base', () => {
+    const commentSide = 'left';
+    element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+    assert.isOk(element._getOrCreateThread('2', 3,
+        commentSide, undefined, /* isOnParent= */ true));
+
+    const threads = dom(element.$.diff)
+        .queryDistributedElements('gr-comment-thread');
+
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].path, element.file.path);
+  });
+
   test('_filterThreadElsForLocation with no threads', () => {
     const line = {beforeNumber: 3, afterNumber: 5};
 
@@ -1443,7 +1503,7 @@
     });
   });
 
-  suite('syntax layer with syntax_highlgihting off', () => {
+  suite('syntax layer with syntax_highlighting off', () => {
     setup(() => {
       const prefs = {
         line_length: 10,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index acd9457..7c9d1ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -25,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-mode-selector_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDiffModeSelector extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 8f48507..b68c889 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-diff-preferences/gr-diff-preferences.js';
@@ -26,7 +24,7 @@
 import {htmlTemplate} from './gr-diff-preferences-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffPreferencesDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -36,7 +34,7 @@
 
   static get properties() {
     return {
-    /** @type {?} */
+      /** @type {?} */
       diffPrefs: Object,
 
       /**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js
new file mode 100644
index 0000000..07cca9a
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js
@@ -0,0 +1,50 @@
+/**
+ * @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';
+
+const basicFixture = fixtureFromElement('gr-diff-preferences-dialog');
+
+suite('gr-diff-preferences-dialog', () => {
+  let element;
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+  test('changes applies only on save', async () => {
+    const originalDiffPrefs = {
+      line_wrapping: true,
+    };
+    element.diffPrefs = originalDiffPrefs;
+
+    element.open();
+    await flush();
+    assert.isTrue(element.$.diffPreferences.$.lineWrappingInput.checked);
+
+    MockInteractions.tap(element.$.diffPreferences.$.lineWrappingInput);
+    await flush();
+    assert.isFalse(element.$.diffPreferences.$.lineWrappingInput.checked);
+    assert.isTrue(element._diffPrefsChanged);
+    assert.isTrue(element.diffPrefs.line_wrapping);
+    assert.isTrue(originalDiffPrefs.line_wrapping);
+
+    MockInteractions.tap(element.$.saveButton);
+    await flush();
+    // Original prefs must remains unchanged, dialog must expose a new object
+    assert.isTrue(originalDiffPrefs.line_wrapping);
+    assert.isFalse(element.diffPrefs.line_wrapping);
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 62ddfee..b9c97a9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -70,7 +68,7 @@
  *    that the part that is within the context or has comments is shown, while
  *    the rest is not.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffProcessor extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 50bfe107..aafd374 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -105,7 +105,6 @@
         group = groups[1];
         assert.equal(group.type, GrDiffGroup.Type.BOTH);
         assert.equal(group.lines.length, 2);
-        assert.equal(group.lines.length, 2);
 
         function beforeNumberFn(l) { return l.beforeNumber; }
         function afterNumberFn(l) { return l.afterNumber; }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 08967e8..cfbe481 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -26,7 +24,7 @@
 import {htmlTemplate} from './gr-diff-selection_html.js';
 import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
 import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.js';
-import {util} from '../../../scripts/util.js';
+import {querySelectorAll} from '../../../utils/dom-util.js';
 
 /**
  * Possible CSS classes indicating the state of selection. Dynamically added/
@@ -42,7 +40,7 @@
 const getNewCache = () => { return {left: null, right: null}; };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffSelection extends mixinBehaviors( [
   DomUtilBehavior,
@@ -205,7 +203,7 @@
   }
 
   _getSelection() {
-    const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
+    const diffHosts = querySelectorAll(document.body, 'gr-diff');
     if (!diffHosts.length) return window.getSelection();
 
     const curDiffHost = diffHosts.find(diffHost => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index e434e65..19abdaf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -14,12 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
@@ -48,6 +45,7 @@
 import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
+import {appContext} from '../../../services/app-context.js';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
 const MSG_LOADING_BLAME = 'Loading blame...';
@@ -67,7 +65,7 @@
 
 /**
  * @appliesMixin PatchSetMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffView extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -164,6 +162,12 @@
         value() { return {sortedFileList: [], changeFilesByPath: {}}; },
       },
 
+      /** @type {Gerrit.FileRange} */
+      _file: {
+        type: Object,
+        computed: '_getCurrentFile(_files, _path)',
+      },
+
       _path: {
         type: String,
         observer: '_pathChanged',
@@ -237,9 +241,9 @@
        * gr-diff-view has gr-fixed-panel on top. The panel can
        * intersect a main element and partially hides a content of
        * the main element. To correctly calculates visibility of an
-       * element, the cursor must know how much height occuped by a fixed
+       * element, the cursor must know how much height occupied by a fixed
        * panel.
-       * The scrollTopMargin defines margin occuped by fixed panel.
+       * The scrollTopMargin defines margin occupied by fixed panel.
        */
       _scrollTopMargin: {
         type: Number,
@@ -294,6 +298,8 @@
       [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
       [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
       [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
+      [this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+          '_handleToggleHideAllCommentThreads',
 
       // Final two are actually handled by gr-comment-thread.
       [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
@@ -301,6 +307,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   attached() {
     super.attached();
@@ -345,6 +356,21 @@
     return files.sortedFileList;
   }
 
+  /**
+   * @param {!Object} files
+   * @param {string} path
+   * @returns {!Gerrit.FileRange}
+   */
+  _getCurrentFile(files, path) {
+    if ([files, path].includes(undefined)) return;
+    const fileInfo = files.changeFilesByPath[path];
+    const fileRange = {path};
+    if (fileInfo && fileInfo.old_path) {
+      fileRange.basePath = fileInfo.old_path;
+    }
+    return fileRange;
+  }
+
   _getFiles(changeNum, patchRangeRecord, changeComments) {
     // Polymer 2: check for undefined
     if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
@@ -358,10 +384,7 @@
       if (!changeFiles) return;
       const commentedPaths = changeComments.getPaths(patchRange);
       const files = Object.assign({}, changeFiles);
-      Object.keys(commentedPaths).forEach(commentedPath => {
-        if (files.hasOwnProperty(commentedPath)) { return; }
-        files[commentedPath] = {status: 'U'};
-      });
+      this.addUnmodifiedFiles(files, commentedPaths);
       this._files = {
         sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
         changeFilesByPath: files,
@@ -647,7 +670,10 @@
   _goToEditFile() {
     // TODO(taoalpha): add a shortcut for editing
     const editUrl = GerritNav.getEditUrlForDiff(
-        this._change, this._path, this._patchRange.patchNum);
+        this._change,
+        this._path,
+        this._patchRange.patchNum
+    );
     return GerritNav.navigateToRelativeUrl(editUrl);
   }
 
@@ -716,6 +742,7 @@
     this._initCursor(this.params);
 
     this._changeNum = value.changeNum;
+    this.classList.remove('hideComments');
     this._path = value.path;
     this._patchRange = {
       patchNum: value.patchNum,
@@ -774,6 +801,8 @@
 
     promises.push(this._getChangeEdit(this._changeNum));
 
+    this.$.diffHost.cancel();
+    this.$.diffHost.clearDiffContent();
     this._loading = true;
     return Promise.all(promises)
         .then(r => {
@@ -790,9 +819,14 @@
           return this.$.diffHost.reload(true);
         })
         .then(() => {
-          this.$.reporting.diffViewFullyLoaded();
+          this.reporting.diffViewFullyLoaded();
           // If diff view displayed has not ended yet, it ends here.
-          this.$.reporting.diffViewDisplayed();
+          this.reporting.diffViewDisplayed();
+        })
+        .then(() => {
+          // If the blame was loaded for a previous file and user navigates to
+          // another file, then we load the blame for this file too
+          if (this._isBlameLoaded) this._loadBlame();
         });
   }
 
@@ -1129,7 +1163,7 @@
     const file = files[path];
     if (file && file.old_path) {
       this._commentsForDiff = this._changeComments.getCommentsBySideForFile(
-          {path, oldPath: file.old_path},
+          {path, basePath: file.old_path},
           patchRange,
           projectConfig);
 
@@ -1209,16 +1243,7 @@
     return 'Show blame';
   }
 
-  /**
-   * Load and display blame information if it has not already been loaded.
-   * Otherwise hide it.
-   */
-  _toggleBlame() {
-    if (this._isBlameLoaded) {
-      this.$.diffHost.clearBlame();
-      return;
-    }
-
+  _loadBlame() {
     this._isBlameLoading = true;
     this.dispatchEvent(new CustomEvent('show-alert', {
       detail: {message: MSG_LOADING_BLAME},
@@ -1237,12 +1262,30 @@
         });
   }
 
+  /**
+   * Load and display blame information if it has not already been loaded.
+   * Otherwise hide it.
+   */
+  _toggleBlame() {
+    if (this._isBlameLoaded) {
+      this.$.diffHost.clearBlame();
+      return;
+    }
+    this._loadBlame();
+  }
+
   _handleToggleBlame(e) {
     if (this.shouldSuppressKeyboardShortcut(e) ||
       this.modifierPressed(e)) { return; }
     this._toggleBlame();
   }
 
+  _handleToggleHideAllCommentThreads(e) {
+    if (this.shouldSuppressKeyboardShortcut(e) ||
+      this.modifierPressed(e)) { return; }
+    this.toggleClass('hideComments');
+  }
+
   _computeBlameLoaderClass(isImageDiff, path) {
     return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
index c74a192..3d30f77 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -196,6 +196,9 @@
         }
       }
     }
+    :host(.hideComments) {
+      --gr-comment-thread-display: none;
+    }
   </style>
   <gr-fixed-panel
     class$="[[_computeContainerClass(_editMode)]]"
@@ -391,6 +394,7 @@
     change-num="[[_changeNum]]"
     commit-range="[[_commitRange]]"
     patch-range="[[_patchRange]]"
+    file="[[_file]]"
     path="[[_path]]"
     prefs="[[_prefs]]"
     project-name="[[_change.project]]"
@@ -420,5 +424,4 @@
     scroll-top-margin="[[_scrollTopMargin]]"
   ></gr-diff-cursor>
   <gr-comment-api id="commentAPI"></gr-comment-api>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index f5275e2..21d1144 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -44,6 +44,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 suite('gr-diff-view tests', () => {
   suite('basic tests', () => {
@@ -70,6 +71,7 @@
     kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
     kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
     kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+    kb.bindShortcut(kb.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
     kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
     kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
     kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
@@ -134,7 +136,7 @@
     });
 
     test('params change triggers diffViewDisplayed()', () => {
-      sandbox.stub(element.$.reporting, 'diffViewDisplayed');
+      sandbox.stub(element.reporting, 'diffViewDisplayed');
       sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
       sandbox.spy(element, '_paramsChanged');
       element.params = {
@@ -146,7 +148,28 @@
       };
 
       return element._paramsChanged.returnValues[0].then(() => {
-        assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
+        assert.isTrue(element.reporting.diffViewDisplayed.calledOnce);
+      });
+    });
+
+    test('params change cases blame to load if it was set to true', () => {
+      // Blame loads for subsequent files if it was loaded for one file
+      element._isBlameLoaded = true;
+      sandbox.stub(element.reporting, 'diffViewDisplayed');
+      sandbox.stub(element, '_loadBlame');
+      sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
+      sandbox.spy(element, '_paramsChanged');
+      element.params = {
+        view: GerritNav.View.DIFF,
+        changeNum: '42',
+        patchNum: '2',
+        basePatchNum: '1',
+        path: '/COMMIT_MSG',
+      };
+
+      return element._paramsChanged.returnValues[0].then(() => {
+        assert.isTrue(element._isBlameLoaded);
+        assert.isTrue(element._loadBlame.calledOnce);
       });
     });
 
@@ -403,7 +426,8 @@
       };
       element._change = {
         _number: 42,
-        status: 'NEW',
+        project: 'gerrit',
+        status: ChangeStatus.NEW,
         revisions: {
           a: {_number: 1, commit: {parents: []}},
           b: {_number: 2, commit: {parents: []}},
@@ -416,6 +440,12 @@
         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();
       });
     });
@@ -445,14 +475,14 @@
     }
 
     test('edit visible only when logged and status NEW', async () => {
-      for (const changeStatus in element.ChangeStatus) {
-        if (!element.ChangeStatus.hasOwnProperty(changeStatus)) {
+      for (const changeStatus in ChangeStatus) {
+        if (!ChangeStatus.hasOwnProperty(changeStatus)) {
           continue;
         }
         assert.isFalse(await isEditVisibile({loggedIn: false, changeStatus}),
             `loggedIn: false, changeStatus: ${changeStatus}`);
 
-        if (changeStatus !== element.ChangeStatus.NEW) {
+        if (changeStatus !== ChangeStatus.NEW) {
           assert.isFalse(await isEditVisibile({loggedIn: true, changeStatus}),
               `loggedIn: true, changeStatus: ${changeStatus}`);
         } else {
@@ -464,17 +494,17 @@
 
     test('edit visible when logged and status NEW', async () => {
       assert.isTrue(await isEditVisibile(
-          {loggedIn: true, changeStatus: element.ChangeStatus.NEW}));
+          {loggedIn: true, changeStatus: ChangeStatus.NEW}));
     });
 
     test('edit hidden when logged and status ABANDONED', async () => {
       assert.isFalse(await isEditVisibile(
-          {loggedIn: true, changeStatus: element.ChangeStatus.ABANDONED}));
+          {loggedIn: true, changeStatus: ChangeStatus.ABANDONED}));
     });
 
     test('edit hidden when logged and status MERGED', async () => {
       assert.isFalse(await isEditVisibile(
-          {loggedIn: true, changeStatus: element.ChangeStatus.MERGED}));
+          {loggedIn: true, changeStatus: ChangeStatus.MERGED}));
     });
 
     suite('diff prefs hidden', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 1bb3842..0942646 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../gr-diff-builder/gr-diff-builder-element.js';
@@ -58,7 +56,7 @@
 
 const COMMIT_MSG_PATH = '/COMMIT_MSG';
 /**
- * 72 is the inofficial length standard for git commit messages.
+ * 72 is the unofficial length standard for git commit messages.
  * Derived from the fact that git log/show appends 4 ws in the beginning of
  * each line when displaying commit messages. To center the commit message
  * in an 80 char terminal a 4 ws border is added to the rightmost side:
@@ -69,7 +67,7 @@
 const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiff extends mixinBehaviors( [
   PatchSetBehavior,
@@ -435,7 +433,8 @@
     }
 
     return Array.from(
-        dom(this.root).querySelectorAll(':not(.contextControl) > .diff-row'));
+        dom(this.root).querySelectorAll(':not(.contextControl) > .diff-row'))
+        .filter(tr => tr.querySelector('button'));
   }
 
   /** @return {boolean} */
@@ -594,11 +593,7 @@
    * @param {!Object=} range
    */
   _createComment(lineEl, lineNum, side, range) {
-    const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
-    if (!contentText) {
-      return;
-    }
-    const contentEl = contentText.parentElement;
+    const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
     side = side ||
         this._getCommentSideByLineAndContent(lineEl, contentEl);
     const patchForNewThreads = this._getPatchNumByLineAndContent(
@@ -670,13 +665,10 @@
 
   /** @return {boolean} */
   _getIsParentCommentByLineAndContent(lineEl, contentEl) {
-    if ((lineEl.classList.contains(DiffSide.LEFT) ||
+    return (lineEl.classList.contains(DiffSide.LEFT) ||
         contentEl.classList.contains('remove')) &&
         (this.patchRange.basePatchNum === 'PARENT' ||
-        this.isMergeParent(this.patchRange.basePatchNum))) {
-      return true;
-    }
-    return false;
+            this.isMergeParent(this.patchRange.basePatchNum));
   }
 
   /** @return {string} */
@@ -836,11 +828,7 @@
         const commentSide = threadEl.getAttribute('comment-side');
         const lineEl = this.$.diffBuilder.getLineElByNumber(
             lineNumString, commentSide);
-        const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
-        if (!contentText) {
-          continue;
-        }
-        const contentEl = contentText.parentElement;
+        const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
         const threadGroupEl = this._getOrCreateThreadGroup(
             contentEl, commentSide);
         // Create a slot for the thread and attach it to the thread group.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
index 55abd38..279f968 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
@@ -90,6 +90,16 @@
     .content {
       background-color: var(--diff-blank-background-color);
     }
+    /*
+      The file line, which has no contentText, add some margin before the first
+      comment. We cannot add padding the container because we only want it if
+      there is at least one comment thread, and the slotting makes :empty not
+      work as expected.
+     */
+    .content.file slot:first-child::slotted(.comment-thread) {
+      display: block;
+      margin-top: var(--spacing-xs);
+    }
     .contentText {
       background-color: var(--view-background-color);
     }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
similarity index 92%
rename from polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index bb0366b..a10db97 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -1,46 +1,38 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/components/web-component-tester/data/a11ySuite.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff></gr-diff>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
 import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
+import {runA11yAudit} from '../../../test/a11y-test-utils.js';
+import '@polymer/paper-button/paper-button.js';
+
+const basicFixture = fixtureFromElement('gr-diff');
+
+suite('gr-diff a11y test', () => {
+  test('audit', async () => {
+    await runA11yAudit(basicFixture);
+  });
+});
 
 suite('gr-diff tests', () => {
   let element;
@@ -62,7 +54,7 @@
     };
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       sandbox.stub(element.$.highlights, 'handleSelectionChange');
     });
 
@@ -80,24 +72,24 @@
   });
 
   test('cancel', () => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
     element.cancel();
     assert.isTrue(cancelStub.calledOnce);
   });
 
   test('line limit with line_wrapping', () => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
     flushAsynchronousOperations();
-    assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+    assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
   });
 
   test('line limit without line_wrapping', () => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
     flushAsynchronousOperations();
-    assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
+    assert.isNotOk(getComputedStyleValue('--line-limit', element));
   });
 
   suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
@@ -105,7 +97,7 @@
     let contentEl;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       lineEl = document.createElement('td');
       contentEl = document.createElement('span');
     });
@@ -191,7 +183,7 @@
       stub('gr-rest-api-interface', {
         getLoggedIn() { return getLoggedInPromise; },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       return getLoggedInPromise;
     });
 
@@ -645,7 +637,7 @@
   suite('logged in', () => {
     let fakeLineEl;
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.loggedIn = true;
       element.patchRange = {};
 
@@ -740,7 +732,7 @@
 
   suite('diff header', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.diff = {
         meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
         meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
@@ -785,7 +777,7 @@
     let renderStub;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       renderStub = sandbox.stub(element.$.diffBuilder, 'render',
           () => {
             element.$.diffBuilder.dispatchEvent(
@@ -837,7 +829,7 @@
 
   suite('blame', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
     test('unsetting', () => {
@@ -850,8 +842,7 @@
     });
 
     test('setting', () => {
-      const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
-      element.blame = mockBlame;
+      element.blame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
       assert.isTrue(element.classList.contains('showBlame'));
     });
   });
@@ -864,7 +855,7 @@
       element.shadowRoot.querySelector('.newlineWarning').textContent;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.showNewlineWarningLeft = false;
       element.showNewlineWarningRight = false;
     });
@@ -920,7 +911,7 @@
     });
 
     test('_prefsEqual', () => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       assert.isTrue(element._prefsEqual(null, null));
       assert.isTrue(element._prefsEqual({}, {}));
       assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
@@ -942,7 +933,7 @@
     let renderStub;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.prefs = {};
       renderStub = sandbox.stub(element.$.diffBuilder, 'render')
           .returns(new Promise(() => {}));
@@ -991,7 +982,7 @@
   });
   const setupSampleDiff = function(params) {
     const {ignore_whitespace, content} = params;
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = {
       ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
       auto_hide_diff_table_header: true,
@@ -1038,9 +1029,8 @@
     }
     setupSampleDiff({content});
     assertDiffTableWithContent();
-    const diffCopy = Object.assign({}, element.diff);
-    element.diff = diffCopy;
-    // immediatelly cleaned up
+    element.diff = Object.assign({}, element.diff);
+    // immediately cleaned up
     assert.equal(element.$.diffTable.innerHTML, '');
     element._renderDiffTable();
     flushAsynchronousOperations();
@@ -1151,7 +1141,7 @@
   });
 
   test('`render` event has contentRendered field in detail', done => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = {};
     sandbox.stub(element.$.diffBuilder, 'render')
         .returns(Promise.resolve());
@@ -1161,7 +1151,21 @@
     });
     element._renderDiffTable();
   });
-});
 
-a11ySuite('basic');
-</script>
+  test('_prefsEqual', () => {
+    element = basicFixture.instantiate();
+    assert.isTrue(element._prefsEqual(null, null));
+    assert.isTrue(element._prefsEqual({}, {}));
+    assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
+    assert.isTrue(element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
+    const somePref = {abc: 'def', p: true};
+    assert.isTrue(element._prefsEqual(somePref, somePref));
+
+    assert.isFalse(element._prefsEqual({}, null));
+    assert.isFalse(element._prefsEqual(null, {}));
+    assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
+    assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
+    assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
+    assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 8bdc1a8..c00c0fb 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
 import '../../shared/gr-select/gr-select.js';
@@ -27,6 +25,7 @@
 import {htmlTemplate} from './gr-patch-range-select_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import {appContext} from '../../../services/app-context.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -38,7 +37,7 @@
  *
  * @property {string} patchNum
  * @property {string} basePatchNum
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrPatchRangeSelect extends mixinBehaviors( [
   PatchSetBehavior,
@@ -80,6 +79,11 @@
     ];
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   _getShaForPatch(patch) {
     return patch.sha.substring(0, 10);
   }
@@ -215,7 +219,7 @@
    * In addition, if the current basePatchNum is 'PARENT', all patchNums are
    * valid.
    *
-   * If the curent basePatchNum is a parent index, then only patches that have
+   * If the current basePatchNum is a parent index, then only patches that have
    * at least that many parents are valid.
    *
    * @param {number|string} basePatchNum The current selected base patch num.
@@ -287,8 +291,14 @@
     const target = dom(e).localTarget;
 
     if (target === this.$.patchNumDropdown) {
+      if (detail.patchNum === e.detail.value) return;
+      this.reporting.reportInteraction('right-patchset-changed',
+          {previous: detail.patchNum, current: e.detail.value});
       detail.patchNum = e.detail.value;
     } else {
+      if (detail.basePatchNum === e.detail.value) return;
+      this.reporting.reportInteraction('left-patchset-changed',
+          {previous: detail.basePatchNum, current: e.detail.value});
       detail.basePatchNum = e.detail.value;
     }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
index 1d4b440..cc11cfa 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
@@ -51,7 +51,7 @@
       }
     }
   </style>
-  <span class="patchRange">
+  <span class="patchRange" aria-label="patch range starts with">
     <gr-dropdown-list
       id="basePatchDropdown"
       value="[[basePatchNum]]"
@@ -67,8 +67,8 @@
       >
     </template>
   </span>
-  <span class="arrow">→</span>
-  <span class="patchRange">
+  <span aria-hidden="true" class="arrow">→</span>
+  <span class="patchRange" aria-label="patch range ends with">
     <gr-dropdown-list
       id="patchNumDropdown"
       value="[[patchNum]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 63e6fc6..218ca7e 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -45,7 +45,7 @@
 import '../gr-comment-api/gr-comment-api.js';
 import '../../shared/revision-info/revision-info.js';
 import './gr-patch-range-select.js';
-import '../gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
 suite('gr-patch-range-select tests', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index e6c49be..70aa1e7 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -24,12 +22,12 @@
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
 // Polymer 1 adds # before array's key, while Polymer 2 doesn't
-const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
+const HOVER_PATH_PATTERN = /^(commentRanges\.#?\d+)\.hovering$/;
 
 const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
 const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrRangedCommentLayer extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index f16db3b..79f937c 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-tooltip/gr-tooltip.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -25,7 +23,7 @@
 import {htmlTemplate} from './gr-selection-action-box_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSelectionActionBox extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index f1e930f..1a0bbd9 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-lib-loader/gr-lib-loader.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -135,12 +133,12 @@
 };
 
 const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
-const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
+const CPP_WCHAR_PATTERN = /L'(\\)?.'/g;
 const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
 const GO_BACKSLASH_LITERAL = '\'\\\\\'';
 const GLOBAL_LT_PATTERN = /</g;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSyntaxLayer extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -251,7 +249,7 @@
   process() {
     // Cancel any still running process() calls, because they append to the
     // same _baseRanges and _revisionRanges fields.
-    this._cancel();
+    this.cancel();
 
     // Discard existing ranges.
     this._baseRanges = [];
@@ -321,7 +319,7 @@
   /**
    * Cancel any asynchronous syntax processing jobs.
    */
-  _cancel() {
+  cancel() {
     if (this._processHandle != null) {
       this.cancelAsync(this._processHandle);
       this._processHandle = null;
@@ -332,7 +330,7 @@
   }
 
   _diffChanged() {
-    this._cancel();
+    this.cancel();
     this._baseRanges = [];
     this._revisionRanges = [];
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index ccdbe8b..638b188 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -33,7 +33,7 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-syntax-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index 4d66699..01268b1 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-list-view/gr-list-view.js';
@@ -28,7 +26,7 @@
 import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDocumentationSearch extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index 09f4abf..9acfd72 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-default-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDefaultEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index 7ca849f..d3a09fe 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-button/gr-button.js';
@@ -35,7 +33,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditControls extends mixinBehaviors( [
   PatchSetBehavior,
@@ -229,7 +227,7 @@
 
   _handleDeleteConfirm(e) {
     // Get the dialog before the api call as the event will change during bubbling
-    // which will make Polymer.dom(e).path an emtpy array in polymer 2
+    // which will make Polymer.dom(e).path an empty array in polymer 2
     const dialog = this._getDialogFromEvent(e);
     this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
         .then(res => {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index 8a24e23..b144bad 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../../styles/shared-styles.js';
@@ -25,7 +23,7 @@
 import {htmlTemplate} from './gr-edit-file-controls_html.js';
 import {GrEditConstants} from '../gr-edit-constants.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEditFileControls extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index d2ffa56..886908a 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
 import '../../shared/gr-button/gr-button.js';
@@ -43,7 +41,7 @@
 const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditorView extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 53c1e54..ecb40a5 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../scripts/bundled-polymer.js';
 import '../styles/shared-styles.js';
 import '../styles/themes/app-theme.js';
 import './admin/gr-admin-view/gr-admin-view.js';
@@ -25,13 +24,13 @@
 import './core/gr-error-manager/gr-error-manager.js';
 import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js';
 import './core/gr-main-header/gr-main-header.js';
-import './core/gr-reporting/gr-reporting.js';
 import './core/gr-router/gr-router.js';
 import './core/gr-smart-search/gr-smart-search.js';
 import './diff/gr-diff-view/gr-diff-view.js';
 import './edit/gr-editor-view/gr-editor-view.js';
 import './plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import './plugins/gr-endpoint-param/gr-endpoint-param.js';
+import './plugins/gr-endpoint-slot/gr-endpoint-slot.js';
 import './plugins/gr-external-style/gr-external-style.js';
 import './plugins/gr-plugin-host/gr-plugin-host.js';
 import './settings/gr-cla-view/gr-cla-view.js';
@@ -48,9 +47,10 @@
 import {BaseUrlBehavior} from '../behaviors/base-url-behavior/base-url-behavior.js';
 import {KeyboardShortcutBehavior} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
+import {appContext} from '../services/app-context.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAppElement extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -156,6 +156,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -176,11 +181,13 @@
   ready() {
     super.ready();
     this._updateLoginUrl();
-    this.$.reporting.appStarted();
+    this.reporting.appStarted();
     this.$.router.start();
 
     this.$.restAPI.getAccount().then(account => {
       this._account = account;
+      const role = account ? 'user' : 'guest';
+      this.reporting.reportLifeCycle(`Started as ${role}`);
     });
     this.$.restAPI.getConfig().then(config => {
       this._serverConfig = config;
@@ -343,6 +350,8 @@
         this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
     this.bindShortcut(
         this.Shortcut.TOGGLE_BLAME, 'b');
+    this.bindShortcut(
+        this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
 
     this.bindShortcut(
         this.Shortcut.OPEN_FIRST_FILE, ']');
@@ -411,7 +420,7 @@
     if (e.ctrlKey) key = 'ctrl+' + key;
     if (e.metaKey) key = 'meta+' + key;
     if (e.altKey) key = 'alt+' + key;
-    this.$.reporting.reportInteraction('shortcut-triggered', {
+    this.reporting.reportInteraction('shortcut-triggered', {
       key,
       from: event.path && event.path[0]
         && event.path[0].nodeName || 'unknown',
@@ -461,7 +470,7 @@
     const baseUrl = this.getBaseUrl();
     if (baseUrl) {
       // Strip the canonical path from the path since needing canonical in
-      // the path is uneeded and breaks the url.
+      // the path is unneeded and breaks the url.
       this._loginUrl = baseUrl + '/login/' + encodeURIComponent(
           '/' + window.location.pathname.substring(baseUrl.length) +
           window.location.search +
@@ -562,7 +571,7 @@
    * that would create a cyclic dependency.
    */
   _handleRpcLog(e) {
-    this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
+    this.reporting.reportRpcTiming(e.detail.anonymizedUrl,
         e.detail.elapsed);
   }
 
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js
index 2ee48c1..47139ce 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.js
+++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -111,6 +111,7 @@
   <main>
     <gr-smart-search
       id="search"
+      label="Search for changes"
       search-query="{{params.query}}"
       hidden="[[!mobileSearch]]"
     >
@@ -219,7 +220,6 @@
     login-url="[[_loginUrl]]"
   ></gr-error-manager>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
   <gr-router id="router"></gr-router>
   <gr-plugin-host id="plugins" config="[[_serverConfig]]"> </gr-plugin-host>
   <gr-lib-loader id="libLoader"></gr-lib-loader>
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.js
index f0a9131..2c166f5 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -48,7 +48,6 @@
 import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {util} from '../scripts/util.js';
-import moment from 'moment/src/moment.js';
 import page from 'page/page.mjs';
 import {Auth} from './shared/gr-rest-api-interface/gr-auth.js';
 import {EventEmitter} from './shared/gr-event-interface/gr-event-interface.js';
@@ -103,7 +102,6 @@
   window.GrCountStringFormatter = GrCountStringFormatter;
   window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
   window.util = util;
-  window.moment = moment;
   window.page = page;
   window.Auth = Auth;
   window.EventEmitter = EventEmitter;
diff --git a/polygerrit-ui/app/elements/gr-app-init.js b/polygerrit-ui/app/elements/gr-app-init.js
index 7caec6d..780e64a 100644
--- a/polygerrit-ui/app/elements/gr-app-init.js
+++ b/polygerrit-ui/app/elements/gr-app-init.js
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 import {initAppContext} from '../services/app-context-init.js';
+import {initVisibilityReporter, initPerformanceReporter, initErrorReporter} from '../services/gr-reporting/gr-reporting.js';
+import {appContext} from '../services/app-context.js';
 
 if (!window.Polymer) {
   window.Polymer = {
@@ -24,4 +26,7 @@
 }
 window.Gerrit = window.Gerrit || {};
 
-initAppContext();
\ No newline at end of file
+initAppContext();
+initVisibilityReporter(appContext);
+initPerformanceReporter(appContext);
+initErrorReporter(appContext);
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index c9910d3..410539c 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -15,13 +15,9 @@
  * limitations under the License.
  */
 
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
 import './gr-app-init.js';
 import './font-roboto-local-loader.js';
+// Sets up global Polymer variable, because plugins requires it.
 import '../scripts/bundled-polymer.js';
 
 /**
@@ -36,7 +32,6 @@
 import 'polymer-resin/standalone/polymer-resin.js';
 import {initGlobalVariables} from './gr-app-global-var-init.js';
 import './gr-app-element.js';
-import './change-list/gr-embed-dashboard/gr-embed-dashboard.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -50,7 +45,7 @@
   safeTypesBridge: SafeTypes.safeTypesBridge,
 });
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrApp extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 6a13789..6322518 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -34,6 +34,7 @@
 <script type="module">
 import '../test/common-test-setup.js';
 import './gr-app.js';
+import {appContext} from '../services/app-context.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
 
 suite('gr-app tests', () => {
@@ -42,9 +43,7 @@
 
   setup(done => {
     sandbox = sinon.sandbox.create();
-    stub('gr-reporting', {
-      appStarted: sandbox.stub(),
-    });
+    sandbox.stub(appContext.reportingService, 'appStarted');
     stub('gr-account-dropdown', {
       _getTopContent: sinon.stub(),
     });
@@ -80,12 +79,12 @@
   const appElement = () => element.$['app-element'];
 
   test('reporting', () => {
-    assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
+    assert.isTrue(appElement().reporting.appStarted.calledOnce);
   });
 
   test('reporting called before router start', () => {
     const element = appElement();
-    const appStartedStub = element.$.reporting.appStarted;
+    const appStartedStub = element.reporting.appStarted;
     const routerStartStub = element.$.router.start;
     sinon.assert.callOrder(appStartedStub, routerStartStub);
   });
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index d5ebb65..3f8aa44 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,15 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrAttributeHelper(element) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
index 8be50b1..89d8ec2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,15 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrChangeMetadataApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 5ef9dbf..4822163 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-dom-hooks">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
 /** @constructor */
 export function GrDomHooksManager(plugin) {
@@ -63,13 +54,18 @@
 }
 
 GrDomHook.prototype._createPlaceholder = function(hookName) {
-  Polymer({
-    is: hookName,
-    properties: {
-      plugin: Object,
-      content: Object,
-    },
-  });
+  class HookPlaceholder extends PolymerElement {
+    static get is() { return hookName; }
+
+    static get properties() {
+      return {
+        plugin: Object,
+        content: Object,
+      };
+    }
+  }
+
+  customElements.define(HookPlaceholder.is, HookPlaceholder);
 };
 
 GrDomHook.prototype.handleInstanceDetached = function(instance) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 62057a1..4991770 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {importHref} from '../../../scripts/import-href.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -28,7 +26,7 @@
 
 const INIT_PROPERTIES_TIMEOUT_MS = 10000;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEndpointDecorator extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -76,12 +74,22 @@
     });
   }
 
-  _initDecoration(name, plugin) {
+  _initDecoration(name, plugin, slot) {
     const el = document.createElement(name);
     return this._initProperties(el, plugin,
         this.getContentChildren().find(
             el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
-        .then(el => this._appendChild(el));
+        .then(el => {
+          const slotEl = slot ?
+            dom(this).querySelector(`gr-endpoint-slot[name=${slot}]`) :
+            null;
+          if (slot && slotEl) {
+            slotEl.parentNode.insertBefore(el, slotEl.nextSibling);
+          } else {
+            this._appendChild(el);
+          }
+          return el;
+        });
   }
 
   _initReplacement(name, plugin) {
@@ -135,7 +143,7 @@
     return dom(this.root).appendChild(el);
   }
 
-  _initModule({moduleName, plugin, type, domHook}) {
+  _initModule({moduleName, plugin, type, domHook, slot}) {
     const name = plugin.getPluginName() + '.' + moduleName;
     if (this._initializedPlugins.get(name)) {
       return;
@@ -143,7 +151,7 @@
     let initPromise;
     switch (type) {
       case 'decorate':
-        initPromise = this._initDecoration(moduleName, plugin);
+        initPromise = this._initDecoration(moduleName, plugin, slot);
         break;
       case 'replace':
         initPromise = this._initReplacement(moduleName, plugin);
@@ -164,16 +172,18 @@
     super.ready();
     this._endpointCallBack = this._initModule.bind(this);
     pluginEndpoints.onNewEndpoint(this.name, this._endpointCallBack);
-    pluginLoader.awaitPluginsLoaded()
-        .then(() => Promise.all(
-            pluginEndpoints.getPlugins(this.name).map(
-                pluginUrl => this._import(pluginUrl)))
-        )
-        .then(() =>
-          pluginEndpoints
-              .getDetails(this.name)
-              .forEach(this._initModule, this)
-        );
+    if (this.name) {
+      pluginLoader.awaitPluginsLoaded()
+          .then(() => Promise.all(
+              pluginEndpoints.getPlugins(this.name).map(
+                  pluginUrl => this._import(pluginUrl)))
+          )
+          .then(() =>
+            pluginEndpoints
+                .getDetails(this.name)
+                .forEach(this._initModule, this)
+          );
+    }
   }
 }
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index d7eafa5..890a457 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -29,6 +29,10 @@
     <div>
       <gr-endpoint-decorator name="first">
         <gr-endpoint-param name="someparam" value="barbar"></gr-endpoint-param>
+        <p>
+          <span>test slot</span>
+          <gr-endpoint-slot name="test"></gr-endpoint-slot>
+        </p>
       </gr-endpoint-decorator>
       <gr-endpoint-decorator name="second">
         <gr-endpoint-param name="someparam" value="foofoo"></gr-endpoint-param>
@@ -44,6 +48,7 @@
 import '../../../test/common-test-setup.js';
 import './gr-endpoint-decorator.js';
 import '../gr-endpoint-param/gr-endpoint-param.js';
+import '../gr-endpoint-slot/gr-endpoint-slot.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {resetPlugins} from '../../../test/test-utils.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
@@ -56,6 +61,7 @@
   let sandbox;
   let plugin;
   let decorationHook;
+  let decorationHookWithSlot;
   let replacementHook;
 
   setup(done => {
@@ -69,6 +75,11 @@
         'http://some/plugin/url.html');
     // Decoration
     decorationHook = plugin.registerCustomComponent('first', 'some-module');
+    decorationHookWithSlot = plugin.registerCustomComponent(
+        'first',
+        'some-module-2',
+        {slot: 'test'}
+    );
     // Replacement
     replacementHook = plugin.registerCustomComponent(
         'second', 'other-module', {replace: true});
@@ -100,15 +111,34 @@
     const [module] = modules;
     assert.isOk(module);
     assert.equal(module['someparam'], 'barbar');
-    return decorationHook.getLastAttached().then(element => {
-      assert.strictEqual(element, module);
-    })
+    return decorationHook.getLastAttached()
+        .then(element => {
+          assert.strictEqual(element, module);
+        })
         .then(() => {
           element.remove();
           assert.equal(decorationHook.getAllAttached().length, 0);
         });
   });
 
+  test('decoration with slot', () => {
+    const element =
+        container.querySelector('gr-endpoint-decorator[name="first"]');
+    const modules = [...dom(element).querySelectorAll('p > some-module-2')];
+    assert.equal(modules.length, 1);
+    const [module] = modules;
+    assert.isOk(module);
+    assert.equal(module['someparam'], 'barbar');
+    return decorationHookWithSlot.getLastAttached()
+        .then(element => {
+          assert.strictEqual(element, module);
+        })
+        .then(() => {
+          element.remove();
+          assert.equal(decorationHookWithSlot.getAllAttached().length, 0);
+        });
+  });
+
   test('replacement', () => {
     const element =
         container.querySelector('gr-endpoint-decorator[name="second"]');
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index 9574391..82402c0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -14,13 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEndpointParam extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.js
similarity index 62%
copy from polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
copy to polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.js
index cf934b0..9ee9c3d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.js
@@ -14,21 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    :host {
-      display: block;
-      margin-bottom: var(--spacing-xxl);
-    }
-  </style>
-  <h3>[[title]]</h3>
-  <gr-button
-    title$="[[tooltip]]"
-    disabled$="[[disabled]]"
-    on-click="_onCommandTap"
-  >
-    [[title]]
-  </gr-button>
-`;
+class GrEndpointSlot extends PolymerElement {
+  static get is() { return 'gr-endpoint-slot'; }
+
+  static get properties() {
+    return {
+      name: String,
+    };
+  }
+}
+
+customElements.define(GrEndpointSlot.is, GrEndpointSlot);
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index 63d40fc..466f84a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,15 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-event-helper">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrEventHelper(element) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 68b1494..f27053d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {importHref} from '../../../scripts/import-href.js';
 import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin.js';
@@ -26,7 +24,7 @@
 import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrExternalStyle extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index d1b2106..1833efa 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrPluginHost extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index db44cea..eaecd29 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-overlay/gr-overlay.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -25,7 +23,7 @@
 (function(window) {
   'use strict';
 
-  /** @extends Polymer.Element */
+  /** @extends PolymerElement */
   class GrPluginPopup extends GestureEventListeners(
       LegacyElementMixin(
           PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index 3363d72..e2dd047 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -14,17 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import './gr-plugin-popup.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-popup-interface">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /**
  * Plugin popup API.
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
index f9a2bdf..1a2cd28 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -14,22 +14,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-plugin-repo-command_html.js';
 
-import '../../admin/gr-repo-command/gr-repo-command.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-Polymer({
-  _template: html`
-    <gr-repo-command title="[[title]]">
-    </gr-repo-command>
-`,
+class GrPluginRepoCommand extends PolymerElement {
+  static get is() {
+    return 'gr-plugin-repo-command';
+  }
 
-  is: 'gr-plugin-repo-command',
+  static get properties() {
+    return {
+      title: String,
+      repoName: String,
+      config: Object,
+    };
+  }
 
-  properties: {
-    title: String,
-    repoName: String,
-    config: Object,
-  },
-});
+  static get template() {
+    return htmlTemplate;
+  }
+
+  _handleClick() {
+    this.dispatchEvent(
+        new CustomEvent('command-tap', {composed: true, bubbles: true})
+    );
+  }
+}
+
+customElements.define(GrPluginRepoCommand.is, GrPluginRepoCommand);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
similarity index 86%
rename from polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
rename to polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
index cf934b0..1e5088b3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
@@ -24,11 +24,5 @@
     }
   </style>
   <h3>[[title]]</h3>
-  <gr-button
-    title$="[[tooltip]]"
-    disabled$="[[disabled]]"
-    on-click="_onCommandTap"
-  >
-    [[title]]
-  </gr-button>
+  <gr-button on-click="_handleClick">[[title]]</gr-button>
 `;
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
index 36d822c..04408f8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import './gr-plugin-repo-command.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-repo-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrRepoApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index 32ae959..1af91fd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -73,13 +73,12 @@
       const pluginCommand = element.shadowRoot
           .querySelector('gr-plugin-repo-command');
       assert.isOk(pluginCommand);
-      const command = pluginCommand.shadowRoot
-          .querySelector('gr-repo-command');
-      assert.isOk(command);
-      assert.equal(command.title, 'foo');
+      const btn = pluginCommand.shadowRoot
+          .querySelector('gr-button');
+      assert.isOk(btn);
+      assert.equal(btn.textContent, 'foo');
       assert.isFalse(tapStub.called);
-      MockInteractions.tap(command.shadowRoot
-          .querySelector('gr-button'));
+      MockInteractions.tap(btn);
       assert.isTrue(tapStub.called);
       done();
     });
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index 4fb971f..8050cd6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -14,17 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../settings/gr-settings-view/gr-settings-item.js';
 import '../../settings/gr-settings-view/gr-settings-menu-item.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-settings-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrSettingsApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
index 3da60db..8a1b601 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {useShadow} from '@polymer/polymer/lib/utils/settings.js';
 
 let styleObjectCount = 0;
 
@@ -34,7 +35,7 @@
  * @return {string} Appropriate class name for the element is returned
  */
 GrStyleObject.prototype.getClassName = function(element) {
-  let rootNode = Polymer.Settings.useShadow
+  let rootNode = useShadow
     ? element.getRootNode() : document.body;
   if (rootNode === document) {
     rootNode = document.head;
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
index 411a7c8..1e37603 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -14,12 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-Polymer({
-  _template: html`
+
+class CustomPluginHeader extends PolymerElement {
+  static get is() {
+    return 'gr-custom-plugin-header';
+  }
+
+  static get properties() {
+    return {
+      logoUrl: String,
+      title: String,
+    };
+  }
+
+  static get template() {
+    return html`
     <style>
       img {
         width: 1em;
@@ -34,12 +45,8 @@
       <img src="[[logoUrl]]" hidden\$="[[!logoUrl]]">
       <span class="title">[[title]]</span>
     </span>
-`,
+`;
+  }
+}
 
-  is: 'gr-custom-plugin-header',
-
-  properties: {
-    logoUrl: String,
-    title: String,
-  },
-});
+customElements.define(CustomPluginHeader.is, CustomPluginHeader);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index c987af3..48b14d3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -14,16 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import './gr-custom-plugin-header.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-theme-api">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 /** @constructor */
 export function GrThemeApi(plugin) {
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 7e312d3..14b6cfe 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-avatar/gr-avatar.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
@@ -27,7 +26,7 @@
 import {htmlTemplate} from './gr-account-info_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountInfo extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 390baf6..0a84f56c 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -27,7 +26,7 @@
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAgreementsList extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 61e8e93..2051947 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -29,7 +28,7 @@
 import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeTableEditor extends mixinBehaviors( [
   ChangeTableBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 79d1390..27532b4 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -125,7 +125,7 @@
         columns.filter(c => c !== 'Assignee'));
   });
 
-  test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
+  test('_handleCheckboxContainerClick relays taps to checkboxes', () => {
     sandbox.stub(element, '_handleNumberCheckboxClick');
     sandbox.stub(element, '_handleTargetClick');
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index 957eb48..8359f96 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -16,7 +16,6 @@
  */
 
 import '@polymer/iron-input/iron-input.js';
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -29,7 +28,7 @@
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrClaView extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -112,7 +111,7 @@
     return this.$.restAPI.saveAccountAgreement({name}).then(res => {
       let message = 'Agreement failed to be submitted, please try again';
       if (res.status === 200) {
-        message = 'Agreement has been successfully submited.';
+        message = 'Agreement has been successfully submitted.';
       }
       this._createToast(message);
       this.loadData();
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
index 2d371e2..b1a951b7 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
@@ -61,8 +61,8 @@
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <main>
-    <h1>New Contributor Agreement</h1>
-    <h3>Select an agreement type:</h3>
+    <h1 class="heading-1">New Contributor Agreement</h1>
+    <h3 class="heading-3">Select an agreement type:</h3>
     <template
       is="dom-repeat"
       items="[[_serverConfig.auth.contributor_agreements]]"
@@ -92,7 +92,7 @@
       id="claNewAgreement"
       class$="[[_computeShowAgreementsClass(_showAgreements)]]"
     >
-      <h3 class="smallHeading">Review the agreement:</h3>
+      <h3 class="heading-3">Review the agreement:</h3>
       <div id="agreementsUrl" class="agreementsUrl">
         <a href$="[[_agreementsUrl]]" target="blank" rel="noopener">
           Please review the agreement.</a
@@ -101,7 +101,7 @@
       <div
         class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]"
       >
-        <h3 class="smallHeading">Complete the agreement:</h3>
+        <h3 class="heading-3">Complete the agreement:</h3>
         <iron-input
           bind-value="{{_agreementsText}}"
           placeholder="Enter 'I agree' here"
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 2a7ac06..6973292 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -26,7 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-edit-preferences_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEditPreferences extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index fc97079..0cc5c2c 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -26,7 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-email-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEmailEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
index 90631c7..6c6ad01 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -29,7 +27,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-gpg-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrGpgEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index 1cc1369..429a7c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -25,7 +23,7 @@
 import {htmlTemplate} from './gr-group-list_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrGroupList extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index 02657f8..164bdee 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
@@ -27,7 +25,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-http-password_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrHttpPassword extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index 74c5eed..d0d30ea 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-form-styles.js';
 import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
@@ -35,7 +33,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrIdentities extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 42982fd..b68915a 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
@@ -28,7 +26,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-menu-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrMenuEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 6635de2..1da513b 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -27,7 +25,7 @@
 import {htmlTemplate} from './gr-registration-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRegistrationDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index 3884a15..2455cec 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -14,13 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-settings-item_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSettingsItem extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
index e26faab..26200d4 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
@@ -23,6 +23,6 @@
       margin-bottom: var(--spacing-xxl);
     }
   </style>
-  <h2 id="[[anchor]]">[[title]]</h2>
+  <h2 id="[[anchor]]" class="heading-2">[[title]]</h2>
   <slot></slot>
 `;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index 5b11516..4d839f8 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-page-nav-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-settings-menu-item_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSettingsMenuItem extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index c2f97ca..95f7a2c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -78,7 +76,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSettingsView extends mixinBehaviors( [
   DocsUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
index 7e04b29..e32a551 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
@@ -21,6 +21,12 @@
     :host {
       color: var(--primary-text-color);
     }
+    h2 {
+      font-family: var(--header-font-family);
+      font-size: var(--font-size-h2);
+      font-weight: var(--font-weight-h2);
+      line-height: var(--line-height-h2);
+    }
     .newEmailInput {
       width: 20em;
     }
@@ -94,14 +100,15 @@
       </ul>
     </gr-page-nav>
     <main class="gr-form-styles">
-      <h1>User Settings</h1>
+      <h1 class="heading-1">User Settings</h1>
       <section class="darkToggle">
         <div class="toggle">
           <paper-toggle-button
+            aria-labelledby="darkThemeToggleLabel"
             checked="[[_isDark]]"
             on-change="_handleToggleDark"
           ></paper-toggle-button>
-          <div>Dark theme (alpha)</div>
+          <div id="darkThemeToggleLabel">Dark theme (alpha)</div>
         </div>
         <p>
           Gerrit's dark theme is in early alpha, and almost definitely will not
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index 814eb7a..d869128 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -29,7 +27,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-ssh-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSshEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index b8960e8..2af1bc7 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-button/gr-button.js';
@@ -36,7 +34,7 @@
   {name: 'Abandons', key: 'notify_abandoned_changes'},
 ];
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrWatchedProjectsEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 6ceee26..c74a396 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-account-link/gr-account-link.js';
 import '../gr-button/gr-button.js';
 import '../gr-icons/gr-icons.js';
@@ -27,7 +25,7 @@
 import {htmlTemplate} from './gr-account-chip_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountChip extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
index 96e0160..ee19374 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
@@ -92,7 +92,6 @@
       link=""
       hidden$="[[!removable]]"
       hidden=""
-      tabindex="-1"
       aria-label="Remove"
       class$="remove [[_getBackgroundClass(transparentBackground)]]"
       on-click="_handleRemoveTap"
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
index c991a37..807aa81 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-autocomplete/gr-autocomplete.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -28,7 +26,7 @@
  * gr-account-entry is an element for entering account
  * and/or group with autocomplete support.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountEntry extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 110d884..8c2b7da 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../gr-avatar/gr-avatar.js';
@@ -29,7 +27,7 @@
 import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountLabel extends mixinBehaviors( [
   DisplayNameBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 27de4b3..d7dd88d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../gr-account-label/gr-account-label.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,7 +26,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountLink extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
index 17e7f49..ce2dc9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
@@ -40,7 +40,7 @@
     }
   </style>
   <span>
-    <a href$="[[_computeOwnerLink(account)]]" tabindex="-1">
+    <a href$="[[_computeOwnerLink(account)]]">
       <gr-account-label
         show-attention="[[showAttention]]"
         hide-avatar="[[hideAvatar]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 73ccf7d..6f5f9e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-account-chip/gr-account-chip.js';
 import '../gr-account-entry/gr-account-entry.js';
 import '../../../styles/shared-styles.js';
@@ -24,11 +22,12 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-account-list_html.js';
+import {appContext} from '../../../services/app-context.js';
 
 const VALID_EMAIL_ALERT = 'Please input a valid email.';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountList extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -119,6 +118,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -154,17 +158,19 @@
   }
 
   _handleAdd(e) {
-    this._addAccountItem(e.detail.value);
+    this.addAccountItem(e.detail.value);
   }
 
-  _addAccountItem(item) {
+  addAccountItem(item) {
     // Append new account or group to the accounts property. We add our own
     // internal properties to the account/group here, so we clone the object
     // to avoid cluttering up the shared change object.
+    let itemTypeAdded = 'unknown';
     if (item.account) {
       const account =
           Object.assign({}, item.account, {_pendingAdd: true});
       this.push('accounts', account);
+      itemTypeAdded = 'account';
     } else if (item.group) {
       if (item.confirm) {
         this.pendingConfirmation = item;
@@ -173,6 +179,7 @@
       const group = Object.assign({}, item.group,
           {_pendingAdd: true, _group: true});
       this.push('accounts', group);
+      itemTypeAdded = 'group';
     } else if (this.allowAnyInput) {
       if (!item.includes('@')) {
         // Repopulate the input with what the user tried to enter and have
@@ -187,8 +194,11 @@
       } else {
         const account = {email: item, _pendingAdd: true};
         this.push('accounts', account);
+        itemTypeAdded = 'email';
       }
     }
+
+    this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
     this.pendingConfirmation = null;
     return true;
   }
@@ -238,11 +248,11 @@
 
   _handleRemove(e) {
     const toRemove = e.detail.account;
-    this._removeAccount(toRemove);
+    this.removeAccount(toRemove);
     this.$.entry.focus();
   }
 
-  _removeAccount(toRemove) {
+  removeAccount(toRemove) {
     if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
       return;
     }
@@ -256,6 +266,7 @@
       }
       if (matches) {
         this.splice('accounts', i, 1);
+        this.reporting.reportInteraction(`Remove from ${this.id}`);
         return;
       }
     }
@@ -275,7 +286,7 @@
     }
     switch (e.detail.keyCode) {
       case 8: // Backspace
-        this._removeAccount(this.accounts[this.accounts.length - 1]);
+        this.removeAccount(this.accounts[this.accounts.length - 1]);
         break;
       case 37: // Left arrow
         if (this.accountChips[this.accountChips.length - 1]) {
@@ -294,7 +305,7 @@
       case 13: // Enter
       case 32: // Spacebar
       case 46: // Delete
-        this._removeAccount(chip.account);
+        this.removeAccount(chip.account);
         // Splice from this array to avoid inconsistent ordering of
         // event handling.
         chips.splice(index, 1);
@@ -334,7 +345,7 @@
   submitEntryText() {
     const text = this.$.entry.getText();
     if (!text.length) { return true; }
-    const wasSubmitted = this._addAccountItem(text);
+    const wasSubmitted = this.addAccountItem(text);
     if (wasSubmitted) { this.$.entry.clear(); }
     return wasSubmitted;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index b3b32606..41158a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -351,7 +351,7 @@
     element.readonly = true;
     const acct = makeAccount();
     element.accounts = [acct];
-    element._removeAccount(acct);
+    element.removeAccount(acct);
     assert.equal(element.accounts.length, 1);
   });
 
@@ -491,7 +491,7 @@
       sandbox.stub(input, '_updateSuggestions');
       sandbox.stub(element, '_computeRemovable').returns(true);
       flush(() => {
-        // Next line is a workaround for Firefix not moving cursor
+        // Next line is a workaround for Firefox not moving cursor
         // on input field update
         assert.equal(
             element._getNativeInput(input.$.input).selectionStart, 0);
@@ -537,7 +537,7 @@
       element.accounts = [makeAccount(), makeAccount()];
       flush(() => {
         const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
-        const removeSpy = sandbox.spy(element, '_removeAccount');
+        const removeSpy = sandbox.spy(element, 'removeAccount');
         MockInteractions.pressAndReleaseKeyOn(
             element.accountChips[0], 8); // Backspace
         assert.isTrue(focusSpy.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 1ec453e..d3a6fad 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-button/gr-button.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-alert_html.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrAlert extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 46e8829..3cf91fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
@@ -28,7 +27,7 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAutocompleteDropdown extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
index b31af73..fecaa73 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
@@ -95,7 +95,7 @@
     id="cursor"
     index="{{index}}"
     cursor-target-class="selected"
-    scroll-behavior="never"
+    scroll-mode="never"
     focus-on-move=""
     stops="[[_suggestionEls]]"
   ></gr-cursor-manager>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 7f9ed72..4cf56a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-input/paper-input.js';
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
@@ -33,7 +31,7 @@
 const DEBOUNCE_WAIT_MS = 200;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAutocomplete extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -186,6 +184,14 @@
         type: Boolean,
         value: false,
       },
+      /**
+       * Invisible label for input element. This label is exposed to
+       * screen readers by paper-input
+       */
+      label: {
+        type: String,
+        value: '',
+      },
 
       /** The DOM element of the selected suggestion. */
       _selected: Object,
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
index eae8741..c0e8abf 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
@@ -66,6 +66,12 @@
         height: 0;
         display: none;
       }
+      /* Hide label for input. The label is still visible for
+      screen readers. Workaround found at:
+      https://github.com/PolymerElements/paper-input/issues/478 */
+      --paper-input-container-label: {
+        display: none;
+      }
     }
     paper-input.warnUncommitted {
       --paper-input-container-input: {
@@ -85,6 +91,7 @@
     on-focus="_onInputFocus"
     on-blur="_onInputBlur"
     autocomplete="off"
+    label="[[label]]"
   >
     <!-- prefix as attribute is required to for polymer 1 -->
     <div slot="prefix" prefix="">
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
similarity index 93%
rename from polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
rename to polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
index 6dd5a97..eb05b90 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
@@ -1,40 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reviewer-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-autocomplete no-debounce></gr-autocomplete>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-autocomplete.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {dom, flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromTemplate(
+    html`<gr-autocomplete no-debounce></gr-autocomplete>`);
+
 suite('gr-autocomplete tests', () => {
   let element;
   let sandbox;
@@ -44,7 +32,7 @@
   };
 
   setup(() => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     sandbox = sinon.sandbox.create();
   });
 
@@ -609,4 +597,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 75181b1..1385f6d 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-js-api-interface/gr-js-api-interface.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -28,7 +26,7 @@
 import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAvatar extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 3169c56..346d84e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-button/paper-button.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -26,10 +23,11 @@
 import {htmlTemplate} from './gr-button_html.js';
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {getEventPath} from '../../../utils/dom-util.js';
+import {appContext} from '../../../services/app-context.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrButton extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -67,6 +65,11 @@
         value: false,
         reflectToAttribute: true,
       },
+      ariaDisabled: {
+        type: Boolean,
+        computed: '_computeDisabled(disabled, loading)',
+        reflectToAttribute: true,
+      },
 
       _disabled: {
         type: Boolean,
@@ -80,6 +83,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -104,8 +112,8 @@
       return;
     }
 
-    this.$.reporting.reportInteraction('button-click',
-        {path: util.getEventPath(e)});
+    this.reporting.reportInteraction('button-click',
+        {path: getEventPath(e)});
   }
 
   _disabledChanged(disabled) {
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
index db3b880..e992ffc 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
@@ -39,7 +39,7 @@
           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 replacecment copied lines can be removed.
+          After replacement copied lines can be removed.
         */
       @apply --layout-inline;
       @apply --layout-center-center;
@@ -141,7 +141,7 @@
     :host([link]) {
       --background-color: transparent;
       --margin: 0;
-      --padding: 5px 4px;
+      --padding: var(--spacing-s);
     }
     :host([disabled][link]),
     :host([loading][link]) {
@@ -173,5 +173,4 @@
     <slot></slot>
     <i class="downArrow"></i>
   </paper-button>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index ae627d1..dfc3d75 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -49,6 +49,8 @@
 import '../../../test/common-test-setup.js';
 import './gr-button.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+import {appContext} from '../../../services/app-context.js';
+
 suite('gr-button tests', () => {
   let element;
   let sandbox;
@@ -190,13 +192,10 @@
   });
 
   suite('reporting', () => {
-    const reportStub = sinon.stub();
+    let reportStub;
     setup(() => {
-      stub('gr-reporting', {
-        reportInteraction: (...args) => {
-          reportStub(...args);
-        },
-      });
+      reportStub = sandbox.stub(appContext.reportingService,
+          'reportInteraction');
       reportStub.reset();
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 10e06dd..dac1755 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-icons/gr-icons.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-star_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrChangeStar extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -55,6 +53,10 @@
     return `gr-icons:star${starred ? '' : '-border'}`;
   }
 
+  _computeAriaLabel(starred) {
+    return starred ? 'Unstar this change' : 'Star this change';
+  }
+
   toggleStar() {
     const newVal = !this.change.starred;
     this.set('change.starred', newVal);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
index f723717a..3b7b1af 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
@@ -37,7 +37,11 @@
       );
     }
   </style>
-  <button aria-label="Change star" on-click="toggleStar">
+  <button
+    role="checkbox"
+    aria-label="[[_computeAriaLabel(change.starred)]]]"
+    on-click="toggleStar"
+  >
     <iron-icon
       class$="[[_computeStarClass(change.starred)]]"
       icon$="[[_computeStarIcon(change.starred)]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index b99612e..915d171 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-tooltip-content/gr-tooltip-content.js';
 import '../../../styles/shared-styles.js';
@@ -43,7 +41,7 @@
 const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
     'current reviewers (or anyone with "View Private Changes" permission).';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrChangeStatus extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
index 904ef1d..38e620f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
@@ -25,43 +25,43 @@
       white-space: nowrap;
     }
     :host(.merged) .chip {
-      background-color: #5b9d52;
-      color: #5b9d52;
+      background-color: var(--status-merged);
+      color: var(--status-merged);
     }
     :host(.abandoned) .chip {
-      background-color: #afafaf;
-      color: #afafaf;
+      background-color: var(--status-abandoned);
+      color: var(--status-abandoned);
     }
     :host(.wip) .chip {
-      background-color: #8f756c;
-      color: #8f756c;
+      background-color: var(--status-wip);
+      color: var(--status-wip);
     }
     :host(.private) .chip {
-      background-color: #c17ccf;
-      color: #c17ccf;
+      background-color: var(--status-private);
+      color: var(--status-private);
     }
     :host(.merge-conflict) .chip {
-      background-color: #dc5c60;
-      color: #dc5c60;
+      background-color: var(--status-conflict);
+      color: var(--status-conflict);
     }
     :host(.active) .chip {
-      background-color: #29b6f6;
-      color: #29b6f6;
+      background-color: var(--status-active);
+      color: var(--status-active);
     }
     :host(.ready-to-submit) .chip {
-      background-color: #e10ca3;
-      color: #e10ca3;
+      background-color: var(--status-ready);
+      color: var(--status-ready);
     }
     :host(.custom) .chip {
-      background-color: #825cc2;
-      color: #825cc2;
+      background-color: var(--status-custom);
+      color: var(--status-custom);
     }
     :host([flat]) .chip {
       background-color: transparent;
       padding: 0;
     }
     :host(:not([flat])) .chip {
-      color: white;
+      color: var(--status-text-color);
     }
   </style>
   <gr-tooltip-content
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index ccfc44c..fa686da 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-storage/gr-storage.js';
 import '../gr-comment/gr-comment.js';
@@ -29,14 +26,16 @@
 import {htmlTemplate} from './gr-comment-thread_html.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 const UNRESOLVED_EXPAND_COUNT = 5;
 const NEWLINE_PATTERN = /\n/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCommentThread extends mixinBehaviors( [
   /**
@@ -156,6 +155,14 @@
         value: false,
         reflectToAttribute: true,
       },
+      showFileName: {
+        type: Boolean,
+        value: true,
+      },
+      showPatchset: {
+        type: Boolean,
+        value: true,
+      },
     };
   }
 
@@ -171,6 +178,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -217,15 +229,40 @@
         {detail: {rootId: this.rootId}, bubbles: false}));
   }
 
+  _getDiffUrlForPath(path) {
+    return GerritNav.getUrlForDiffById(this.changeNum, this.projectName, path,
+        this.patchNum);
+  }
+
   _getDiffUrlForComment(projectName, changeNum, path, patchNum) {
     return GerritNav.getUrlForDiffById(changeNum,
         projectName, path, patchNum,
         null, this.lineNum);
   }
 
+  _isPatchsetLevelComment(path) {
+    return path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+  }
+
   _computeDisplayPath(path) {
-    const lineString = this.lineNum ? `#${this.lineNum}` : '';
-    return this.computeDisplayPath(path) + lineString;
+    const displayPath = this.computeDisplayPath(path);
+    if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+      return `Patchset ${this.patchNum}`;
+    }
+    return displayPath;
+  }
+
+  _computeDisplayLine() {
+    if (this.lineNum) return `#${this.lineNum}`;
+    // If range is set, then lineNum equals the end line of the range.
+    if (!this.lineNum && !this.range) {
+      if (this.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+        return '';
+      }
+      return 'FILE';
+    }
+    if (this.range) return `#${this.range.end_line}`;
+    return '';
   }
 
   _getLoggedIn() {
@@ -304,8 +341,8 @@
 
   _sortedComments(comments) {
     return comments.slice().sort((c1, c2) => {
-      const c1Date = c1.__date || util.parseDate(c1.updated);
-      const c2Date = c2.__date || util.parseDate(c2.updated);
+      const c1Date = c1.__date || parseDate(c1.updated);
+      const c2Date = c2.__date || parseDate(c2.updated);
       const dateCompare = c1Date - c2Date;
       // Ensure drafts are at the end. There should only be one but in edge
       // cases could be more. In the unlikely event two drafts are being
@@ -318,15 +355,13 @@
     });
   }
 
-  _createReplyComment(parent, content, opt_isEditing,
+  _createReplyComment(content, opt_isEditing,
       opt_unresolved) {
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
     const reply = this._newReply(
         this._orderedComments[this._orderedComments.length - 1].id,
-        parent.line,
         content,
-        opt_unresolved,
-        parent.range);
+        opt_unresolved);
 
     // If there is currently a comment in an editing state, add an attribute
     // so that the gr-comment knows not to populate the draft text.
@@ -366,25 +401,23 @@
       const msg = comment.message;
       quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
     }
-    this._createReplyComment(comment, quoteStr, true, comment.unresolved);
+    this._createReplyComment(quoteStr, true, comment.unresolved);
   }
 
-  _handleCommentReply(e) {
+  _handleCommentReply() {
     this._processCommentReply();
   }
 
-  _handleCommentQuote(e) {
+  _handleCommentQuote() {
     this._processCommentReply(true);
   }
 
-  _handleCommentAck(e) {
-    const comment = this._lastComment;
-    this._createReplyComment(comment, 'Ack', false, false);
+  _handleCommentAck() {
+    this._createReplyComment('Ack', false, false);
   }
 
-  _handleCommentDone(e) {
-    const comment = this._lastComment;
-    this._createReplyComment(comment, 'Done', false, false);
+  _handleCommentDone() {
+    this._createReplyComment('Done', false, false);
   }
 
   _handleCommentFix(e) {
@@ -392,7 +425,7 @@
     const msg = comment.message;
     const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
     const response = quoteStr + 'Please fix.';
-    this._createReplyComment(comment, response, false, true);
+    this._createReplyComment(response, false, true);
   }
 
   _commentElWithDraftID(id) {
@@ -405,11 +438,9 @@
     return null;
   }
 
-  _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
-      opt_range) {
-    const d = this._newDraft(opt_lineNum);
+  _newReply(inReplyTo, opt_message, opt_unresolved) {
+    const d = this._newDraft();
     d.in_reply_to = inReplyTo;
-    d.range = opt_range;
     if (opt_message != null) {
       d.message = opt_message;
     }
@@ -428,19 +459,40 @@
       __draft: true,
       __draftID: Math.random().toString(36),
       __date: new Date(),
-      path: this.path,
-      patchNum: this.patchNum,
-      side: this._getSide(this.isOnParent),
-      __commentSide: this.commentSide,
     };
-    if (opt_lineNum) {
-      d.line = opt_lineNum;
-    }
-    if (opt_range) {
-      d.range = opt_range;
-    }
-    if (this.parentIndex) {
-      d.parent = this.parentIndex;
+
+    // For replies, always use same meta info as root.
+    if (this.comments && this.comments.length >= 1) {
+      const rootComment = this.comments[0];
+      [
+        'path',
+        'patchNum',
+        'side',
+        '__commentSide',
+        'line',
+        'range',
+        'parent',
+      ].forEach(key => {
+        if (rootComment.hasOwnProperty(key)) {
+          d[key] = rootComment[key];
+        }
+      });
+    } else {
+      // Set meta info for root comment.
+      d.path = this.path;
+      d.patchNum = this.patchNum;
+      d.side = this._getSide(this.isOnParent);
+      d.__commentSide = this.commentSide;
+
+      if (opt_lineNum) {
+        d.line = opt_lineNum;
+      }
+      if (opt_range) {
+        d.range = opt_range;
+      }
+      if (this.parentIndex) {
+        d.parent = this.parentIndex;
+      }
     }
     return d;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
index fbc18b4..837a58f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -27,17 +27,17 @@
     gr-button {
       margin-left: var(--spacing-m);
     }
-    gr-comment:not(:last-of-type) {
+    gr-comment {
       border-bottom: 1px solid var(--comment-separator-color);
     }
     #actions {
       margin-left: auto;
-      padding: var(--spacing-m);
+      padding: var(--spacing-s) var(--spacing-m);
     }
     #container {
       background-color: var(--comment-background-color);
       color: var(--comment-text-color);
-      display: block;
+      display: var(--gr-comment-thread-display, block);
       margin: 0 var(--spacing-s) var(--spacing-s);
       white-space: normal;
       box-shadow: var(--elevation-level-2);
@@ -55,7 +55,6 @@
       background-color: var(--robot-comment-background-color);
     }
     #commentInfoContainer {
-      border-top: 1px dotted var(--border-color);
       display: flex;
     }
     #unresolvedLabel {
@@ -73,14 +72,30 @@
       margin-left: var(--spacing-m);
       font-style: italic;
     }
+    .fileName {
+      padding: var(--spacing-m) var(--spacing-s) var(--spacing-m);
+    }
   </style>
   <template is="dom-if" if="[[showFilePath]]">
+    <template is="dom-if" if="[[showFileName]]">
+      <div class="fileName">
+        <template is="dom-if" if="[[_isPatchsetLevelComment(path)]]">
+          <span> [[_computeDisplayPath(path)]] </span>
+        </template>
+        <template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
+          <a href$="[[_getDiffUrlForPath(path)]]">
+            [[_computeDisplayPath(path)]]
+          </a>
+        </template>
+      </div>
+    </template>
     <div class="pathInfo">
-      <a
-        href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
-        >[[_computeDisplayPath(path)]]</a
-      >
-      <span class="descriptionText">Patchset [[patchNum]]</span>
+      <template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
+        <a
+          href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
+          >[[_computeDisplayLine()]]</a
+        >
+      </template>
     </div>
   </template>
   <div
@@ -101,6 +116,7 @@
         patch-num="[[patchNum]]"
         draft="[[_isDraft(comment)]]"
         show-actions="[[_showActions]]"
+        show-patchset="[[showPatchset]]"
         comment-side="[[comment.__commentSide]]"
         side="[[comment.side]]"
         project-config="[[_projectConfig]]"
@@ -148,7 +164,6 @@
       </div>
     </div>
   </div>
-  <gr-reporting id="reporting"></gr-reporting>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index 244a9ec..e219b22 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -42,6 +42,7 @@
 import './gr-comment-thread.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 suite('gr-comment-thread tests', () => {
   suite('basic test', () => {
@@ -215,17 +216,37 @@
       assert.notEqual(getComputedStyle(element.shadowRoot
           .querySelector('.pathInfo')).display,
       'none');
-      assert.isTrue(GerritNav.getUrlForDiffById.lastCall.calledWithExactly(
+      assert.isTrue(GerritNav.getUrlForDiffById.getCall(0).calledWithExactly(
           element.changeNum, element.projectName, element.path,
           element.patchNum, null, element.lineNum));
+      assert.isTrue(GerritNav.getUrlForDiffById.getCall(1).calledWithExactly(
+          element.changeNum, element.projectName, element.path,
+          element.patchNum));
     });
 
     test('_computeDisplayPath', () => {
-      const path = 'path/to/file';
+      let path = 'path/to/file';
       assert.equal(element._computeDisplayPath(path), 'path/to/file');
 
       element.lineNum = 5;
-      assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
+      assert.equal(element._computeDisplayPath(path), 'path/to/file');
+
+      element.patchNum = '3';
+      path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+      assert.equal(element._computeDisplayPath(path), 'Patchset 3');
+    });
+
+    test('_computeDisplayLine', () => {
+      element.lineNum = 5;
+      assert.equal(element._computeDisplayLine(), '#5');
+
+      element.path = SpecialFilePath.COMMIT_MESSAGE;
+      element.lineNum = 5;
+      assert.equal(element._computeDisplayLine(), '#5');
+
+      element.lineNum = undefined;
+      element.path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+      assert.equal(element._computeDisplayLine(), '');
     });
   });
 });
@@ -268,6 +289,8 @@
       updated: '2015-12-08 19:48:33.843000000',
       path: '/path/to/file.txt',
       unresolved: true,
+      patchNum: 3,
+      __commentSide: 'left',
     }];
     flushAsynchronousOperations();
   });
@@ -279,7 +302,7 @@
   test('reply', () => {
     const commentEl = element.shadowRoot
         .querySelector('gr-comment');
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     assert.ok(commentEl);
 
@@ -297,7 +320,7 @@
   test('quote reply', () => {
     const commentEl = element.shadowRoot
         .querySelector('gr-comment');
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     assert.ok(commentEl);
 
@@ -313,7 +336,7 @@
   });
 
   test('quote reply multiline', () => {
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     element.comments = [{
       author: {
@@ -344,7 +367,7 @@
   });
 
   test('ack', done => {
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     element.changeNum = '42';
     element.patchNum = '1';
@@ -367,7 +390,7 @@
   });
 
   test('done', done => {
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     element.changeNum = '42';
     element.patchNum = '1';
@@ -440,7 +463,6 @@
     element.path = '/path/to/file.txt';
     element.push('comments', element._newReply(
         element.comments[0].id,
-        element.comments[0].line,
         element.comments[0].path,
         'it’s pronouced jiff, not giff'));
     flushAsynchronousOperations();
@@ -634,7 +656,7 @@
     });
 
     test('comment in_reply_to is either null or most recent comment', () => {
-      element._createReplyComment(element.comments[3], 'dummy', true);
+      element._createReplyComment('dummy', true);
       flushAsynchronousOperations();
       assert.equal(element._orderedComments.length, 5);
       assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
@@ -642,7 +664,7 @@
 
     test('resolvable comments', () => {
       assert.isFalse(element.unresolved);
-      element._createReplyComment(element.comments[3], 'dummy', true, true);
+      element._createReplyComment('dummy', true, true);
       flushAsynchronousOperations();
       assert.isTrue(element.unresolved);
     });
@@ -705,14 +727,21 @@
     assert.equal(element.comments[0].unresolved, true);
   });
 
-  test('_newDraft', () => {
-    element.commentSide = 'left';
-    element.patchNum = 3;
+  test('_newDraft with root', () => {
     const draft = element._newDraft();
     assert.equal(draft.__commentSide, 'left');
     assert.equal(draft.patchNum, 3);
   });
 
+  test('_newDraft with no root', () => {
+    element.comments = [];
+    element.commentSide = 'right';
+    element.patchNum = 2;
+    const draft = element._newDraft();
+    assert.equal(draft.__commentSide, 'right');
+    assert.equal(draft.patchNum, 2);
+  });
+
   test('new comment gets created', () => {
     element.comments = [];
     element.addOrEditDraft(1);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index ee9df11..02fec5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
 import '../gr-button/gr-button.js';
@@ -41,6 +38,7 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
 import {GrDisplayNameUtils} from '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import {appContext} from '../../../services/app-context.js';
 
 const STORAGE_DEBOUNCE_INTERVAL = 400;
 const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -69,7 +67,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrComment extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -166,6 +164,7 @@
       collapsed: {
         type: Boolean,
         value: true,
+        reflectToAttribute: true,
         observer: '_toggleCollapseClass',
       },
       /** @type {?} */
@@ -214,6 +213,10 @@
         type: Boolean,
         value: false,
       },
+      showPatchset: {
+        type: Boolean,
+        value: true,
+      },
       _respectfulReviewTip: String,
       _respectfulTipDismissed: {
         type: Boolean,
@@ -241,6 +244,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   attached() {
     super.attached();
@@ -283,7 +291,7 @@
       this._showRespectfulTip = true;
       const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
       this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
-      this.$.reporting.reportInteraction(
+      this.reporting.reportInteraction(
           'respectful-tip-appeared',
           {tip: this._respectfulReviewTip}
       );
@@ -303,7 +311,7 @@
 
   _dismissRespectfulTip() {
     this._respectfulTipDismissed = true;
-    this.$.reporting.reportInteraction(
+    this.reporting.reportInteraction(
         'respectful-tip-dismissed',
         {tip: this._respectfulReviewTip}
     );
@@ -312,7 +320,7 @@
   }
 
   _onRespectfulReadMoreClick() {
-    this.$.reporting.reportInteraction('respectful-read-more-clicked');
+    this.reporting.reportInteraction('respectful-read-more-clicked');
   }
 
   get textarea() {
@@ -343,6 +351,10 @@
     return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
   }
 
+  _computeShowHideAriaLabel(collapsed) {
+    return collapsed ? 'Expand' : 'Collapse';
+  }
+
   _calculateActionstoShow(showActions, isRobotComment) {
     // Polymer 2: check for undefined
     if ([showActions, isRobotComment].some(arg => arg === undefined)) {
@@ -479,7 +491,10 @@
 
     this.$.container.classList.toggle('editing', editing);
     if (this.comment && this.comment.id) {
-      this.shadowRoot.querySelector('.cancel').hidden = !editing;
+      const cancelButton = this.shadowRoot.querySelector('.cancel');
+      if (cancelButton) {
+        cancelButton.hidden = !editing;
+      }
     }
     if (this.comment) {
       this.comment.__editing = this.editing;
@@ -583,7 +598,7 @@
     e.preventDefault();
     this._messageText = this.comment.message;
     this.editing = true;
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
   }
 
   _handleSave(e) {
@@ -595,7 +610,7 @@
     }
     const timingLabel = this.comment.id ?
       REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
-    const timer = this.$.reporting.getTimer(timingLabel);
+    const timer = this.reporting.getTimer(timingLabel);
     this.set('comment.__editing', false);
     return this.save().then(() => { timer.end(); });
   }
@@ -643,7 +658,7 @@
 
   _handleDiscard(e) {
     e.preventDefault();
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
 
     if (!this._messageText) {
       this._discardDraft();
@@ -658,7 +673,7 @@
 
   _handleConfirmDiscard(e) {
     e.preventDefault();
-    const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
+    const timer = this.reporting.getTimer(REPORT_DISCARD_DRAFT);
     this._closeConfirmDiscardOverlay();
     return this._discardDraft().then(() => { timer.end(); });
   }
@@ -799,7 +814,7 @@
   }
 
   _handleToggleResolved() {
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
     this.resolved = !this.resolved;
     // Modify payload instead of this.comment, as this.comment is passed from
     // the parent by ref.
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index fc79cba..1d04969 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -23,6 +23,9 @@
       font-family: var(--font-family);
       padding: var(--spacing-m);
     }
+    :host([collapsed]) {
+      padding: var(--spacing-s) var(--spacing-m);
+    }
     :host([disabled]) {
       pointer-events: none;
     }
@@ -34,20 +37,17 @@
     :host([discarding]) {
       display: none;
     }
+    .body {
+      padding-top: var(--spacing-m);
+    }
     .header {
       align-items: center;
       cursor: pointer;
       display: flex;
-      margin: calc(0px - var(--spacing-m)) calc(0px - var(--spacing-m)) 0
-        calc(0px - var(--spacing-m));
-      padding: var(--spacing-m);
     }
     .headerLeft > span {
       font-weight: var(--font-weight-bold);
     }
-    .container.collapsed .header {
-      margin-bottom: calc(0 - var(--spacing-m));
-    }
     .headerMiddle {
       color: var(--deemphasized-text-color);
       flex: 1;
@@ -60,8 +60,6 @@
     }
     .date {
       justify-content: flex-end;
-      margin-left: 5px;
-      min-width: 4.5em;
       text-align: right;
       white-space: nowrap;
     }
@@ -77,6 +75,12 @@
       justify-content: flex-end;
       padding-top: 0;
     }
+    .robotActions {
+      /* Better than the negative margin would be to remove the gr-button
+       * padding, but then we would also need to fix the buttons that are
+       * inserted by plugins. :-/ */
+      margin: 4px 0 -4px;
+    }
     .action {
       margin-left: var(--spacing-l);
     }
@@ -131,16 +135,6 @@
     .robotId {
       color: var(--deemphasized-text-color);
       margin-bottom: var(--spacing-m);
-      margin-top: -0.4em;
-    }
-    .robotIcon {
-      margin-right: var(--spacing-xs);
-      /* because of the antenna of the robot, it looks off center even when it
-         is centered. artificially adjust margin to account for this. */
-      margin-top: -4px;
-    }
-    .runIdInformation {
-      margin: var(--spacing-m) 0;
     }
     .robotRun {
       margin-left: var(--spacing-m);
@@ -161,16 +155,18 @@
     #container .collapsedContent {
       display: none;
     }
-    #container.collapsed {
-      padding-bottom: 3px;
+    #container.collapsed .body {
+      padding-top: 0;
     }
     #container.collapsed .collapsedContent {
       display: block;
       overflow: hidden;
-      padding-left: 5px;
+      padding-left: var(--spacing-m);
       text-overflow: ellipsis;
       white-space: nowrap;
     }
+    #container.collapsed #deleteBtn,
+    #container.collapsed .date,
     #container.collapsed .actions,
     #container.collapsed gr-formatted-text,
     #container.collapsed gr-textarea,
@@ -192,12 +188,6 @@
       flex-direction: column;
       width: 100%;
     }
-    .comment-extra-note {
-      color: var(--deemphasized-text-color);
-      border: 1px solid var(--deemphasized-text-color);
-      border-radius: var(--border-radius);
-      padding: 0px var(--spacing-s);
-    }
     #deleteBtn {
       display: none;
       --gr-button: {
@@ -241,6 +231,10 @@
     .pointer {
       cursor: pointer;
     }
+    .patchset-text {
+      color: var(--deemphasized-text-color);
+      margin-left: var(--spacing-s);
+    }
   </style>
   <div id="container" class="container">
     <div class="header" id="header" on-click="_handleToggleCollapsed">
@@ -270,9 +264,6 @@
           </a>
         </div>
       </div>
-      <template is="dom-if" if="[[comment.extraNote]]">
-        <span class="comment-extra-note">[[comment.extraNote]]</span>
-      </template>
       <gr-button
         id="deleteBtn"
         link=""
@@ -282,14 +273,21 @@
       >
         <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
       </gr-button>
-      <span class="date" on-click="_handleAnchorClick">
+      <template is="dom-if" if="[[showPatchset]]">
+        <span class="patchset-text"> Patchset [[patchNum]]</span>
+      </template>
+      <span class="separator"></span>
+      <span class="date" tabindex="0" on-click="_handleAnchorClick">
         <gr-date-formatter
           has-tooltip=""
           date-str="[[comment.updated]]"
         ></gr-date-formatter>
       </span>
-      <div class="show-hide">
-        <label class="show-hide">
+      <div class="show-hide" tabindex="0">
+        <label
+          class="show-hide"
+          aria-label="[[_computeShowHideAriaLabel(collapsed)]]"
+        >
           <input
             type="checkbox"
             class="show-hide"
@@ -373,33 +371,35 @@
             Resolved
           </label>
         </div>
-        <div class="rightActions">
-          <gr-button
-            link=""
-            class="action cancel hideOnPublished"
-            on-click="_handleCancel"
-            >Cancel</gr-button
-          >
-          <gr-button
-            link=""
-            class="action discard hideOnPublished"
-            on-click="_handleDiscard"
-            >Discard</gr-button
-          >
-          <gr-button
-            link=""
-            class="action edit hideOnPublished"
-            on-click="_handleEdit"
-            >Edit</gr-button
-          >
-          <gr-button
-            link=""
-            disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
-            class="action save hideOnPublished"
-            on-click="_handleSave"
-            >Save</gr-button
-          >
-        </div>
+        <template is="dom-if" if="[[draft]]">
+          <div class="rightActions">
+            <gr-button
+              link=""
+              class="action cancel hideOnPublished"
+              on-click="_handleCancel"
+              >Cancel</gr-button
+            >
+            <gr-button
+              link=""
+              class="action discard hideOnPublished"
+              on-click="_handleDiscard"
+              >Discard</gr-button
+            >
+            <gr-button
+              link=""
+              class="action edit hideOnPublished"
+              on-click="_handleEdit"
+              >Edit</gr-button
+            >
+            <gr-button
+              link=""
+              disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
+              class="action save hideOnPublished"
+              on-click="_handleSave"
+              >Save</gr-button
+            >
+          </div>
+        </template>
       </div>
       <div class="robotActions" hidden$="[[!_showRobotActions]]">
         <template is="dom-if" if="[[isRobotComment]]">
@@ -458,5 +458,4 @@
   </template>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-storage id="storage"></gr-storage>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 56581d4..17c01e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -259,14 +259,6 @@
       });
     });
 
-    test('extra note shown if exists', () => {
-      element.comment = {id: 'abc_123', extraNote: 'asd'};
-      flushAsynchronousOperations();
-      assert.equal(element.shadowRoot
-          .querySelector('.comment-extra-note')
-          .textContent, 'asd');
-    });
-
     test('delete comment button for non-admins is hidden', () => {
       element._isAdmin = false;
       assert.isFalse(element.shadowRoot
@@ -320,7 +312,7 @@
         sandbox.stub(element, '_discardDraft')
             .returns(Promise.resolve({}));
         endStub = sinon.stub();
-        getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
+        getTimerStub = sandbox.stub(element.reporting, 'getTimer')
             .returns({end: endStub});
       });
 
@@ -354,17 +346,20 @@
     });
 
     test('edit reports interaction', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
+      const reportStub = sandbox.stub(element.reporting,
           'recordDraftInteraction');
+      element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       assert.isTrue(reportStub.calledOnce);
     });
 
     test('discard reports interaction', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
+      const reportStub = sandbox.stub(element.reporting,
           'recordDraftInteraction');
       element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.discard'));
       assert.isTrue(reportStub.calledOnce);
@@ -435,6 +430,7 @@
           .querySelector('.robotActions').hasAttribute('hidden'));
 
       element.draft = true;
+      flushAsynchronousOperations();
       assert.isTrue(isVisible(element.shadowRoot
           .querySelector('.edit')), 'edit is visible');
       assert.isTrue(isVisible(element.shadowRoot
@@ -560,6 +556,8 @@
 
       // When the edit button is pressed, should still see the actions
       // and also textarea
+      element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       flushAsynchronousOperations();
@@ -663,6 +661,8 @@
 
     test('draft creation/cancellation', done => {
       assert.isFalse(element.editing);
+      element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       assert.isTrue(element.editing);
@@ -795,12 +795,13 @@
       const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
 
       element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       element._messageText = 'good news, everyone!';
       element.flushDebouncer('fire-update');
       element.flushDebouncer('store');
-      assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update'),
+      assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update');
       assert.isTrue(dispatchEventStub.calledTwice);
 
       element._messageText = 'good news, everyone!';
@@ -860,6 +861,7 @@
       const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
       element.showActions = true;
       element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.$.header);
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index b0f387b..f9ce12e 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -25,7 +22,7 @@
 import {htmlTemplate} from './gr-confirm-delete-comment-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmDeleteCommentDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index 0f6168e..39c149f 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../gr-button/gr-button.js';
@@ -28,7 +26,7 @@
 
 const COPY_TIMEOUT_MS = 1000;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCopyClipboard extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
index 8378de5..9fc83c8 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
@@ -79,6 +79,7 @@
       class="copyToClipboard"
       title="[[buttonTitle]]"
       on-click="_copyToClipboard"
+      aria-label="Click to copy to clipboard"
     >
       <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon>
     </gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 222109e..647f41b 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -14,18 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-cursor-manager_html.js';
+import {ScrollMode} from '../../../constants/constants.js';
 
-const ScrollBehavior = {
-  NEVER: 'never',
-  KEEP_VISIBLE: 'keep-visible',
-};
-
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCursorManager extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -82,9 +77,9 @@
        *
        * @type {string|undefined}
        */
-      scrollBehavior: {
+      scrollMode: {
         type: String,
-        value: ScrollBehavior.NEVER,
+        value: ScrollMode.NEVER,
       },
 
       /**
@@ -214,8 +209,8 @@
   setCursor(element, opt_noScroll) {
     let behavior;
     if (opt_noScroll) {
-      behavior = this.scrollBehavior;
-      this.scrollBehavior = ScrollBehavior.NEVER;
+      behavior = this.scrollMode;
+      this.scrollMode = ScrollMode.NEVER;
     }
 
     this.unsetCursor();
@@ -223,7 +218,7 @@
     this._updateIndex();
     this._decorateTarget();
 
-    if (opt_noScroll) { this.scrollBehavior = behavior; }
+    if (opt_noScroll) { this.scrollMode = behavior; }
   }
 
   unsetCursor() {
@@ -391,7 +386,7 @@
    */
   _targetIsVisible(top) {
     const dims = this._getWindowDims();
-    return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
+    return this.scrollMode === ScrollMode.KEEP_VISIBLE &&
         top > (dims.pageYOffset + this.scrollTopMargin) &&
         top < dims.pageYOffset + dims.innerHeight;
   }
@@ -403,7 +398,7 @@
   }
 
   _scrollToTarget() {
-    if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
+    if (!this.target || this.scrollMode === ScrollMode.NEVER) {
       return;
     }
 
@@ -415,8 +410,8 @@
 
     if (this._targetIsVisible(top)) {
       // Don't scroll if either the bottom is visible or if the position that
-      // would get scrolled to is higher up than the current position. this
-      // woulld cause less of the target content to be displayed than is
+      // would get scrolled to is higher up than the current position. This
+      // would cause less of the target content to be displayed than is
       // already.
       if (bottomIsVisible || scrollToValue < dims.scrollY) {
         return;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
rename to polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index 98a7d24..561746a 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -1,32 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
 
-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
+import '../../../test/common-test-setup-karma.js';
+import './gr-cursor-manager.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-cursor-manager</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
+const basicTestFixutre = fixtureFromTemplate(html`
     <gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
     <ul>
       <li>A</li>
@@ -34,12 +27,8 @@
       <li>C</li>
       <li>D</li>
     </ul>
-  </template>
-</test-fixture>
+`);
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-cursor-manager.js';
 suite('gr-cursor-manager tests', () => {
   let sandbox;
   let element;
@@ -47,7 +36,7 @@
 
   setup(() => {
     sandbox = sinon.sandbox.create();
-    const fixtureElements = fixture('basic');
+    const fixtureElements = basicTestFixutre.instantiate();
     element = fixtureElements[0];
     list = fixtureElements[1];
   });
@@ -178,7 +167,7 @@
     sandbox.stub(element, '_targetIsVisible', () => false);
     const scrollStub = sandbox.stub(window, 'scrollTo');
     element.stops = list.querySelectorAll('li');
-    element.scrollBehavior = 'keep-visible';
+    element.scrollMode = 'keep-visible';
 
     element.setCursorAtIndex(1, true);
     assert.isFalse(scrollStub.called);
@@ -236,7 +225,7 @@
     let scrollStub;
     setup(() => {
       element.stops = list.querySelectorAll('li');
-      element.scrollBehavior = 'keep-visible';
+      element.scrollMode = 'keep-visible';
 
       // There is a target which has a targetNext
       element.setCursor(list.children[0]);
@@ -300,4 +289,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 6f19587..5335ede 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -24,13 +22,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-date-formatter_html.js';
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
-import {util} from '../../../scripts/util.js';
-import moment from 'moment/src/moment.js';
-
-const Duration = {
-  HOUR: 1000 * 60 * 60,
-  DAY: 1000 * 60 * 60 * 24,
-};
+import {parseDate, fromNow, isValidDate, isWithinDay, isWithinHalfYear, formatDate, utcOffsetString} from '../../../utils/date-util.js';
 
 const TimeFormats = {
   TIME_12: 'h:mm A', // 2:14 PM
@@ -63,7 +55,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDateFormatter extends mixinBehaviors( [
   TooltipBehavior,
@@ -108,6 +100,10 @@
     };
   }
 
+  constructor() {
+    super();
+  }
+
   /** @override */
   attached() {
     super.attached();
@@ -115,7 +111,7 @@
   }
 
   _getUtcOffsetString() {
-    return ' UTC' + moment().format('Z');
+    return utcOffsetString();
   }
 
   _loadPreferences() {
@@ -192,50 +188,28 @@
     return this.$.restAPI.getPreferences();
   }
 
-  /**
-   * Return true if date is within 24 hours and on the same day.
-   */
-  _isWithinDay(now, date) {
-    const diff = -date.diff(now);
-    return diff < Duration.DAY && date.day() === now.getDay();
-  }
-
-  /**
-   * Returns true if date is from one to six months.
-   */
-  _isWithinHalfYear(now, date) {
-    const diff = -date.diff(now);
-    return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
-      diff < 180 * Duration.DAY;
-  }
-
   _computeDateStr(
       dateStr, timeFormat, dateFormat, relative, showDateAndTime
   ) {
     if (!dateStr || !timeFormat || !dateFormat) { return ''; }
-    const date = moment(util.parseDate(dateStr));
-    if (!date.isValid()) { return ''; }
+    const date = parseDate(dateStr);
+    if (!isValidDate(date)) { return ''; }
     if (relative) {
-      const dateFromNow = date.fromNow();
-      if (dateFromNow === 'a few seconds ago') {
-        return 'just now';
-      } else {
-        return dateFromNow;
-      }
+      return fromNow(date);
     }
     const now = new Date();
     let format = dateFormat.full;
-    if (this._isWithinDay(now, date)) {
+    if (isWithinDay(now, date)) {
       format = timeFormat;
     } else {
-      if (this._isWithinHalfYear(now, date)) {
+      if (isWithinHalfYear(now, date)) {
         format = dateFormat.short;
       }
       if (this.showDateAndTime) {
         format = `${format} ${timeFormat}`;
       }
     }
-    return date.format(format);
+    return formatDate(date, format);
   }
 
   _timeToSecondsFormat(timeFormat) {
@@ -255,11 +229,11 @@
     }
 
     if (!dateStr) { return ''; }
-    const date = moment(util.parseDate(dateStr));
-    if (!date.isValid()) { return ''; }
+    const date = parseDate(dateStr);
+    if (!isValidDate(date)) { return ''; }
     let format = dateFormat.full + ', ';
     format += this._timeToSecondsFormat(timeFormat);
-    return date.format(format) + this._getUtcOffsetString();
+    return formatDate(date, format) + this._getUtcOffsetString();
   }
 }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 7169ef27..589a157 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -34,7 +34,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-date-formatter.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 suite('gr-date-formatter tests', () => {
   let element;
   let sandbox;
@@ -51,7 +51,7 @@
    * Parse server-formatter date and normalize into current timezone.
    */
   function normalizedDate(dateStr) {
-    const d = util.parseDate(dateStr);
+    const d = parseDate(dateStr);
     d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
     return d;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index db64661..2292ae7 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-button/gr-button.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
index b32f871..54b404d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
@@ -60,7 +60,7 @@
     }
   </style>
   <div class="container" on-keydown="_handleKeydown">
-    <header class="font-h3"><slot name="header"></slot></header>
+    <header class="heading-3"><slot name="header"></slot></header>
     <main>
       <div class="overflow-container">
         <slot name="main"></slot>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
index 00f9078..1d00941 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../gr-button/gr-button.js';
@@ -26,7 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-preferences_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDiffPreferences extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index fcc09c4..7b92e3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-tabs/paper-tabs.js';
 import '../gr-shell-command/gr-shell-command.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -28,7 +26,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDownloadCommands extends mixinBehaviors( [
   RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 6b250de..f84ef4a 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '@polymer/paper-item/paper-item.js';
 import '@polymer/paper-listbox/paper-listbox.js';
@@ -56,7 +54,7 @@
  */
 Defs.item;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDropdownList extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 12a2025..8e7ea87 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '../gr-button/gr-button.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
@@ -35,7 +33,7 @@
 const REL_EXTERNAL = 'external';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDropdown extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -184,7 +182,7 @@
   }
 
   /**
-   * Hanlde a click on the button to open the dropdown.
+   * Handle a click on the button to open the dropdown.
    *
    * @param {!Event} e
    */
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
index d0b0d09..6617a0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
@@ -161,7 +161,7 @@
   <gr-cursor-manager
     id="cursor"
     cursor-target-class="selected"
-    scroll-behavior="never"
+    scroll-mode="never"
     focus-on-move=""
     stops="[[_listElements]]"
   ></gr-cursor-manager>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 804eb16..b2bcda9 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.js';
 import '../gr-storage/gr-storage.js';
@@ -29,7 +27,7 @@
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditableContent extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index 8669f03..6c3c72f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '@polymer/paper-input/paper-input.js';
@@ -33,7 +31,7 @@
 const AWAIT_STEP = 5;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditableLabel extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index 5673194..1d1bce8 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -188,7 +188,7 @@
 
     element.async(() => {
       assert.isFalse(editedStub.called);
-      // Text changes sould be discarded.
+      // Text changes should be discarded.
       assert.equal(input.value, 'value text');
       assert.isFalse(element.editing);
       done();
@@ -214,7 +214,7 @@
 
     element.async(() => {
       assert.isFalse(editedStub.called);
-      // Text changes sould be discarded.
+      // Text changes should be discarded.
       assert.equal(input.value, 'value text');
       assert.isFalse(element.editing);
       done();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index 0d19f00..bc79737 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-fixed-panel_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrFixedPanel extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
index 139e09c..039b95d 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-linked-text/gr-linked-text.js';
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -28,7 +26,7 @@
 const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
 const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrFormattedText extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -91,7 +89,7 @@
 
   /**
    * Given a source string, parse into an array of block objects. Each block
-   * has a `type` property which takes any of the follwoing values.
+   * has a `type` property which takes any of the following values.
    * * 'paragraph'
    * * 'quote' (Block quote.)
    * * 'pre' (Pre-formatted text.)
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
index 0bc9cb7..49aff7e 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
@@ -26,7 +25,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-hovercard-account_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrHovercardAccount extends GestureEventListeners(
     hovercardBehaviorMixin(LegacyElementMixin(
         PolymerElement))) {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
index 8d14ff4..bbf6a96 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
@@ -64,33 +64,35 @@
     }
   </style>
   <div id="container" role="tooltip" tabindex="-1">
-    <div class="top">
-      <div class="avatar">
-        <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+    <template is="dom-if" if="[[_isShowing]]">
+      <div class="top">
+        <div class="avatar">
+          <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+        </div>
+        <div class="account">
+          <h3 class="name heading-3">[[account.name]]</h3>
+          <div class="email">[[account.email]]</div>
+        </div>
       </div>
-      <div class="account">
-        <h3 class="name">[[account.name]]</h3>
-        <div class="email">[[account.email]]</div>
-      </div>
-    </div>
-    <template is="dom-if" if="[[account.status]]">
-      <div class="status">
-        <span class="title">
-          <iron-icon icon="gr-icons:calendar"></iron-icon>
-          Status:
-        </span>
-        <span class="value">[[account.status]]</span>
+      <template is="dom-if" if="[[account.status]]">
+        <div class="status">
+          <span class="title">
+            <iron-icon icon="gr-icons:calendar"></iron-icon>
+            Status:
+          </span>
+          <span class="value">[[account.status]]</span>
+        </div>
+      </template>
+      <template is="dom-if" if="[[voteableText]]">
+        <div class="voteable">
+          <span class="title">Voteable:</span>
+          <span class="value">[[voteableText]]</span>
+        </div>
+      </template>
+      <div class="attention">
+        <iron-icon icon="gr-icons:attention"></iron-icon>
+        <span>It is this user's turn to take action.</span>
       </div>
     </template>
-    <template is="dom-if" if="[[voteableText]]">
-      <div class="voteable">
-        <span class="title">Voteable:</span>
-        <span class="value">[[voteableText]]</span>
-      </div>
-    </template>
-    <div class="attention">
-      <iron-icon icon="gr-icons:attention"></iron-icon>
-      <span>It is this user's turn to take action.</span>
-    </div>
   </div>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
index be0f2b2..884e1b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
@@ -49,6 +49,8 @@
     setup(() => {
       element = fixture('basic');
       element.account = Object.assign({}, ACCOUNT);
+      element.show({});
+      flushAsynchronousOperations();
     });
 
     test('account name is shown', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
index 956c68c..1190908 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index e77a4c5..e334064 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +23,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import './gr-hovercard-shared-style.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrHovercard extends GestureEventListeners(
     hovercardBehaviorMixin(LegacyElementMixin(PolymerElement))
 ) {
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 84d0d2a..070ac02 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -98,8 +98,12 @@
       <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
       <!-- This SVG is an adaptation of material.io https://material.io/icons/#label_important-->
       <g id="attention"><path d="M5.5 19 l9 0 c.67 0 1.27 -.33 1.63 -.84 L20.5 12 l-4.37 -6.16 c-.36 -.51 -.96 -.84 -1.63 -.84 l-9 0 L9 12 z"></path></g>
+      <!-- This SVG is a copy from material.io https://material.io/icons/#pets-->
+      <g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
       <!-- This SVG is a copy from material.io https://material.io/icons/#visibility-->
       <g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
+      <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons -->
+      <g id="schedule"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
     </defs>
   </svg>
 </iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
index 997e08c..a98936f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -46,7 +44,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrJsApiInterface extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index 1cdb20f..6f0ade9 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import './gr-js-api-interface-element.js';
 import './gr-public-js-api.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js
index e3256a1..63555da 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js
@@ -14,6 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
 export function GrPluginActionContext(plugin, action, change, revision) {
   this.action = action;
   this.plugin = plugin;
@@ -47,14 +49,14 @@
 
 GrPluginActionContext.prototype.msg = function(text) {
   const label = document.createElement('gr-label');
-  Polymer.dom(label).appendChild(document.createTextNode(text));
+  dom(label).appendChild(document.createTextNode(text));
   return label;
 };
 
 GrPluginActionContext.prototype.div = function(...els) {
   const div = document.createElement('div');
   for (const el of els) {
-    Polymer.dom(div).appendChild(el);
+    dom(div).appendChild(el);
   }
   return div;
 };
@@ -62,7 +64,7 @@
 GrPluginActionContext.prototype.button = function(label, callbacks) {
   const onClick = callbacks && callbacks.onclick;
   const button = document.createElement('gr-button');
-  Polymer.dom(button).appendChild(document.createTextNode(label));
+  dom(button).appendChild(document.createTextNode(label));
   if (onClick) {
     this.plugin.eventHelper(button).onTap(onClick);
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
index 388243c..0727397 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -39,12 +39,13 @@
   }
 };
 
-GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin,
-    endpoint, type, moduleName, domHook) {
+GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin, opts) {
+  const {endpoint, slot, type, moduleName, domHook} = opts;
   const existingModule = this._endpoints[endpoint].find(info =>
     info.plugin === plugin &&
       info.moduleName === moduleName &&
-      info.domHook === domHook
+      info.domHook === domHook &&
+      info.slot === slot
   );
   if (existingModule) {
     return existingModule;
@@ -55,6 +56,7 @@
       pluginUrl: plugin._url,
       type,
       domHook,
+      slot,
     };
     this._endpoints[endpoint].push(newModule);
     return newModule;
@@ -67,9 +69,12 @@
  * Dynamic plugins are registered to a specific prefix, such as
  * 'change-list-header'. These plugins are then fetched by prefix to determine
  * which endpoints to dynamically add to the page.
+ *
+ * @param {Object} plugin
+ * @param {Object} opts
  */
-GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
-    moduleName, domHook, dynamicEndpoint) {
+GrPluginEndpoints.prototype.registerModule = function(plugin, opts) {
+  const {endpoint, dynamicEndpoint} = opts;
   if (dynamicEndpoint) {
     if (!this._dynamicPlugins[dynamicEndpoint]) {
       this._dynamicPlugins[dynamicEndpoint] = new Set();
@@ -79,8 +84,7 @@
   if (!this._endpoints[endpoint]) {
     this._endpoints[endpoint] = [];
   }
-  const moduleInfo = this._getOrCreateModuleInfo(plugin, endpoint, type,
-      moduleName, domHook);
+  const moduleInfo = this._getOrCreateModuleInfo(plugin, opts);
   if (pluginLoader.arePluginsLoaded() && this._callbacks[endpoint]) {
     this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index ec8710b..3494e99 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -48,11 +48,25 @@
     pluginApi.install(p => { pluginFoo = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/foo.html');
     instance.registerModule(
-        pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
+        pluginFoo,
+        {
+          endpoint: 'a-place',
+          type: 'decorate',
+          moduleName: 'foo-module',
+          domHook,
+        }
+    );
     pluginApi.install(p => { pluginBar = p; }, '0.1',
         'http://test.com/plugins/testplugin/static/bar.html');
     instance.registerModule(
-        pluginBar, 'a-place', 'style', 'bar-module', domHook);
+        pluginBar,
+        {
+          endpoint: 'a-place',
+          type: 'style',
+          moduleName: 'bar-module',
+          domHook,
+        }
+    );
     sandbox.stub(pluginLoader, 'arePluginsLoaded').returns(true);
   });
 
@@ -68,6 +82,7 @@
         pluginUrl: pluginFoo._url,
         type: 'decorate',
         domHook,
+        slot: undefined,
       },
       {
         moduleName: 'bar-module',
@@ -75,6 +90,7 @@
         pluginUrl: pluginBar._url,
         type: 'style',
         domHook,
+        slot: undefined,
       },
     ]);
   });
@@ -87,6 +103,7 @@
         pluginUrl: pluginBar._url,
         type: 'style',
         domHook,
+        slot: undefined,
       },
     ]);
   });
@@ -101,6 +118,7 @@
             pluginUrl: pluginFoo._url,
             type: 'decorate',
             domHook,
+            slot: undefined,
           },
         ]);
   });
@@ -119,13 +137,20 @@
     const newModuleStub = sandbox.stub();
     instance.onNewEndpoint('a-place', newModuleStub);
     instance.registerModule(
-        pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
+        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,
     });
   });
 
@@ -139,6 +164,7 @@
         pluginUrl: pluginFoo._url,
         type: 'decorate',
         domHook,
+        slot: undefined,
       },
       {
         moduleName: 'bar-module',
@@ -146,6 +172,7 @@
         pluginUrl: pluginBar._url,
         type: 'style',
         domHook,
+        slot: undefined,
       },
     ]);
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
index 2f27304..6c5546e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -1,3 +1,5 @@
+import {appContext} from '../../../services/app-context.js';
+
 /**
  * @license
  * Copyright (C) 2019 The Android Open Source Project
@@ -15,6 +17,7 @@
  * limitations under the License.
  */
 import './gr-api-utils.js';
+import {importHref} from '../../../scripts/import-href.js';
 
 import {
   PLUGIN_LOADING_TIMEOUT_MS,
@@ -85,7 +88,7 @@
 
   _getReporting() {
     if (!this._reporting) {
-      this._reporting = document.createElement('gr-reporting');
+      this._reporting = appContext.reportingService;
     }
     return this._reporting;
   }
@@ -333,7 +336,7 @@
       };
     }
 
-    (Polymer.importHref || Polymer.Base.importHref)(
+    importHref(
         url, () => {},
         onerror,
         !sync);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index c972f53..ea1fcf2 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -99,12 +99,14 @@
   });
 
   test('report pluginsLoaded', done => {
-    stub('gr-reporting', {
-      pluginsLoaded() {
-        done();
-      },
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
+    pluginsLoadedStub.reset();
+    window.Gerrit._loadPlugins([]);
+    flush(() => {
+      assert.isTrue(pluginsLoadedStub.calledOnce);
+      done();
     });
-    pluginLoader.loadPlugins([]);
   });
 
   test('arePluginsLoaded', done => {
@@ -129,10 +131,8 @@
     sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     const plugins = [
       'http://test.com/plugins/foo/static/test.js',
@@ -151,10 +151,6 @@
     sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
 
     const plugins = [
       'http://test.com/plugins/foo/static/test.js',
@@ -193,10 +189,8 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
 
@@ -225,10 +219,8 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
     assert.isTrue(
@@ -260,10 +252,8 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
 
@@ -289,10 +279,8 @@
       }, url === plugins[0] ? '' : 'alpha', url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
 
@@ -308,10 +296,8 @@
     sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     const plugins = [
       'http://test.com/plugins/foo/static/test.js',
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index d111d5c..9d79462 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -94,9 +94,9 @@
     return this._name;
   };
 
-  Plugin.prototype.registerStyleModule = function(endpointName, moduleName) {
+  Plugin.prototype.registerStyleModule = function(endpoint, moduleName) {
     pluginEndpoints.registerModule(
-        this, endpointName, EndpointType.STYLE, moduleName);
+        this, {endpoint, type: EndpointType.STYLE, moduleName});
   };
 
   /**
@@ -122,14 +122,15 @@
   };
 
   Plugin.prototype._registerCustomComponent = function(
-      endpointName, opt_moduleName, opt_options, dynamicEndpoint) {
+      endpoint, opt_moduleName, opt_options, dynamicEndpoint) {
     const type = opt_options && opt_options.replace ?
       EndpointType.REPLACE : EndpointType.DECORATE;
-    const hook = this._domHooks.getDomHook(endpointName, opt_moduleName);
-    const moduleName = opt_moduleName || hook.getModuleName();
+    const slot = opt_options && opt_options.slot || '';
+    const domHook = this._domHooks.getDomHook(endpoint, opt_moduleName);
+    const moduleName = opt_moduleName || domHook.getModuleName();
     pluginEndpoints.registerModule(
-        this, endpointName, type, moduleName, hook, dynamicEndpoint);
-    return hook.getPublicAPI();
+        this, {slot, endpoint, type, moduleName, domHook, dynamicEndpoint});
+    return domHook.getPublicAPI();
   };
 
   /**
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 22bdce1..eb23708 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-voting-styles.js';
 import '../../../styles/shared-styles.js';
 import '../gr-account-label/gr-account-label.js';
@@ -31,7 +29,7 @@
 import {htmlTemplate} from './gr-label-info_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabelInfo extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -131,7 +129,7 @@
 
   /**
    * Closure annotation for Polymer.prototype.splice is off.
-   * For now, supressing annotations.
+   * For now, suppressing annotations.
    *
    * @suppress {checkTypes} */
   _onDeleteVote(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
index 2a86669..1722d02 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
@@ -109,7 +109,7 @@
         <td>
           <gr-button
             link=""
-            aria-label="Remove"
+            aria-label="Remove vote"
             on-click="_onDeleteVote"
             tooltip="Remove vote"
             data-account-id$="[[mappedLabel.account._account_id]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index d7ccc45..b97b5b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -212,7 +212,7 @@
     let score = '0';
     assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
 
-    // Non-exsistent score.
+    // Non-existent score.
     score = '2';
     assert.equal(element._computeValueTooltip(labelInfo, score), '');
 
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
index 014e85e..fa1f758 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -24,7 +22,7 @@
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrLabel extends mixinBehaviors( [
   TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index f585347..d17f7d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-autocomplete/gr-autocomplete.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-labeled-autocomplete_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabeledAutocomplete extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 9bd8a11..e9cd441 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-js-api-interface/gr-js-api-interface.js';
 import {importHref} from '../../../scripts/import-href.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -27,7 +25,7 @@
 const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
 const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLibLoader extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index b7bfbf3..1b5224f 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -29,7 +27,7 @@
  * configured limit, then an ellipsis indicates that the text was truncated
  * and a tooltip containing the full text is enabled.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrLimitedText extends mixinBehaviors( [
   TooltipBehavior,
@@ -67,7 +65,7 @@
       },
 
       /**
-       * The maximum number of characters to display in the tooltop.
+       * The maximum number of characters to display in the tooltip.
        */
       tooltipLimit: {
         type: Number,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index 077ca74..46588ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../gr-button/gr-button.js';
 import '../gr-icons/gr-icons.js';
@@ -26,7 +25,7 @@
 import {htmlTemplate} from './gr-linked-chip_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrLinkedChip extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 6ea4b78..b969d1d 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -26,7 +24,7 @@
 import {GrLinkTextParser} from './link-text-parser.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLinkedText extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index 7a44c67..595694c 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
@@ -32,7 +30,7 @@
 const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrListView extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index a5a3fb4..5437ca5 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {IronOverlayBehaviorImpl, IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -29,7 +27,7 @@
 const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrOverlay extends mixinBehaviors( [
   IronOverlayBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index 8366463..f191981 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -14,14 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-page-nav_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrPageNav extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
index c499f56..9439c08 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../gr-icons/gr-icons.js';
@@ -32,7 +30,7 @@
 const REF_PREFIX = 'refs/heads/';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoBranchPicker extends mixinBehaviors( [
   URLEncodingBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index 5fa476f..f919cbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -87,7 +87,7 @@
     });
   });
 
-  suite('cache and events behaivor', () => {
+  suite('cache and events behavior', () => {
     let fakeFetch;
     let clock;
     setup(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 23b8de7..a4adbac 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,15 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator">
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
 
 // Limit cache size because /change/detail responses may be large.
 const MAX_CACHE_SIZE = 30;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index b675df7..de4f12d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,28 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */
 /* NB: Order is important, because of namespaced classes. */
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-import '../../../scripts/bundled-polymer.js';
 
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import 'es6-promise/lib/es6-promise.js';
-import 'whatwg-fetch/fetch.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GrEtagDecorator} from './gr-etag-decorator.js';
 import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './gr-rest-apis/gr-rest-api-helper.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 import {authService} from './gr-auth.js';
 
 const DiffViewMode = {
@@ -61,7 +52,7 @@
     '/revisions/*';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRestApiInterface extends mixinBehaviors( [
   PathListBehavior,
@@ -2081,7 +2072,7 @@
   _setRanges(comments) {
     comments = comments || [];
     comments.sort(
-        (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
+        (a, b) => parseDate(a.updated) - parseDate(b.updated)
     );
     for (const comment of comments) {
       this._setRange(comments, comment);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
index 3d1ce05..ed474d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
@@ -15,7 +15,8 @@
  * limitations under the License.
  */
 
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
+import {MessageTag} from '../../../constants/constants.js';
 
 /** @constructor */
 export function GrReviewerUpdatesParser(change) {
@@ -52,9 +53,7 @@
  */
 GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() {
   this.result.messages = this.result.messages
-      .filter(
-          message => message.tag !== 'autogenerated:gerrit:deleteReviewer'
-      );
+      .filter(message => message.tag !== MessageTag.TAG_DELETE_REVIEWER);
 };
 
 /**
@@ -68,6 +67,7 @@
     author: update.updated_by,
     date: update.updated,
     type: 'REVIEWER_UPDATE',
+    tag: MessageTag.TAG_REVIEWER_UPDATE,
   };
 };
 
@@ -106,8 +106,8 @@
     if (!this._batch) {
       this._batch = this._startBatch(update);
     }
-    const updateDate = util.parseDate(update.updated).getTime();
-    const batchUpdateDate = util.parseDate(this._batch.date).getTime();
+    const updateDate = parseDate(update.updated).getTime();
+    const batchUpdateDate = parseDate(this._batch.date).getTime();
     const reviewerId = update.reviewer._account_id.toString();
     if (updateDate - batchUpdateDate >
         GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS ||
@@ -206,14 +206,14 @@
   const updates = this.result.reviewer_updates;
   const messages = this.result.messages;
   messages.forEach((message, index) => {
-    const messageDate = util.parseDate(message.date).getTime();
+    const messageDate = parseDate(message.date).getTime();
     const nextMessageDate = index === messages.length - 1 ? null :
-      util.parseDate(messages[index + 1].date).getTime();
+      parseDate(messages[index + 1].date).getTime();
     for (const update of updates) {
-      const date = util.parseDate(update.date).getTime();
+      const date = parseDate(update.date).getTime();
       if (date >= messageDate &&
           (!nextMessageDate || date < nextMessageDate)) {
-        const timestamp = util.parseDate(update.date).getTime() -
+        const timestamp = parseDate(update.date).getTime() -
             GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
         update.date = new Date(timestamp)
             .toISOString()
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index f2ccfb7..fa54d6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -27,7 +27,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 
 suite('gr-reviewer-updates-parser tests', () => {
   let sandbox;
@@ -253,7 +253,7 @@
   });
 
   test('_advanceUpdates', () => {
-    const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime();
+    const T0 = parseDate('2017-02-17 19:04:18.000000000').getTime();
     const tplus = delta => new Date(T0 + delta)
         .toISOString()
         .replace('T', ' ')
@@ -297,8 +297,8 @@
     instance = new GrReviewerUpdatesParser(change);
     instance._advanceUpdates();
     const updates = instance.result.reviewer_updates;
-    assert.isBelow(util.parseDate(updates[0].date).getTime(), T0);
-    assert.isBelow(util.parseDate(updates[1].date).getTime(), T0);
+    assert.isBelow(parseDate(updates[0].date).getTime(), T0);
+    assert.isBelow(parseDate(updates[1].date).getTime(), T0);
     assert.equal(updates[2].date, tplus(100));
     assert.equal(updates[3].date, tplus(500));
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index e061e93..9b4bbaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -14,27 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-select">
-  <slot></slot>
-  
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSelect extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
   static get is() { return 'gr-select'; }
 
+  static get template() {
+    return html`
+      <slot></slot>
+    `;
+  }
+
   static get properties() {
     return {
       bindValue: {
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 151498c..a5212fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-copy-clipboard/gr-copy-clipboard.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-shell-command_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrShellCommand extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 8f5c486..90fdcd3 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -32,7 +30,7 @@
   'editablecontent:': DURATION_DAY,
 };
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrStorage extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index 15ab8e4..e98e11d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -14,15 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
 import '../gr-overlay/gr-overlay.js';
 import '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -30,6 +27,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-textarea_html.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {appContext} from '../../../services/app-context.js';
 
 const MAX_ITEMS_DROPDOWN = 10;
 
@@ -67,7 +65,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrTextarea extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -139,6 +137,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   ready() {
     super.ready();
@@ -214,7 +217,7 @@
     this.text = this._getText(text);
     this.$.textarea.selectionStart = colonIndex + 1;
     this.$.textarea.selectionEnd = colonIndex + 1;
-    this.$.reporting.reportInteraction('select-emoji', {type: text});
+    this.reporting.reportInteraction('select-emoji', {type: text});
     this._resetEmojiDropdown();
   }
 
@@ -302,7 +305,7 @@
 
   _openEmojiDropdown() {
     this.$.emojiSuggestions.open();
-    this.$.reporting.reportInteraction('open-emoji-dropdown');
+    this.reporting.reportInteraction('open-emoji-dropdown');
   }
 
   _formatSuggestions(matchedSuggestions) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
index 61e530a..8454f62 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
@@ -90,5 +90,4 @@
     value="{{text}}"
     on-bind-value-changed="_onValueChanged"
   ></iron-autogrow-textarea>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index c33b2ae..bcf1207 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -53,7 +53,7 @@
   setup(() => {
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
-    sandbox.stub(element.$.reporting, 'reportInteraction');
+    sandbox.stub(element.reporting, 'reportInteraction');
   });
 
   teardown(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
index 160f50a..502c82e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-icons/gr-icons.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -25,7 +23,7 @@
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrTooltipContent extends mixinBehaviors( [
   TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index 0cd2d7c..2244cca 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-tooltip_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrTooltip extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/embed/README.md b/polygerrit-ui/app/embed/README.md
index bef098b..8860878 100644
--- a/polygerrit-ui/app/embed/README.md
+++ b/polygerrit-ui/app/embed/README.md
@@ -1,4 +1,4 @@
-This folder contains shared components that can be used independently from Gerrit.
+This folder contains shared components that can be used independently of Gerrit.
 
 ### gr-diff
 
@@ -10,4 +10,4 @@
 
 All supported attributes defined in `polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js`, you can pass them by just assigning them to the `gr-app` element.
 
-To customize the style of the diff, you can use `css variables`, all supported varibled defined in `polygerrit-ui/app/styles/themes/app-theme.html` and `polygerrit-ui/app/styles/themes/dark-theme.html`.
+To customize the style of the diff, you can use `css variables`, all supported variables defined in `polygerrit-ui/app/styles/themes/app-theme.html` and `polygerrit-ui/app/styles/themes/dark-theme.html`.
diff --git a/polygerrit-ui/app/embed/app-context-init.js b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
similarity index 71%
rename from polygerrit-ui/app/embed/app-context-init.js
rename to polygerrit-ui/app/embed/gr-diff-app-context-init.js
index 55a5866..90110f0 100644
--- a/polygerrit-ui/app/embed/app-context-init.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
@@ -16,6 +16,7 @@
  */
 
 import {appContext} from '../services/app-context.js';
+import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
 
 class MockFlagsService {
   isEnabled(experimentId) {
@@ -23,7 +24,7 @@
   }
 
   /**
-   * @returns {string[]} array of all enabled experiments.
+   * @returns {!Array<string>} array of all enabled experiments.
    */
   get enabledExperiments() {
     return [];
@@ -34,5 +35,13 @@
 // This is a temporary solution
 // TODO(dmfilippov): find a better solution for gr-diff
 export function initDiffAppContext() {
-  appContext.flagsService = new MockFlagsService();
+  function setMock(serviceName, setupMock) {
+    Object.defineProperty(appContext, serviceName, {
+      get() {
+        return setupMock;
+      },
+    });
+  }
+  setMock('flagsService', new MockFlagsService);
+  setMock('reportingService', grReportingMock);
 }
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html
new file mode 100644
index 0000000..3aed13f
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+  import '../test/common-test-setup.js';
+  import {appContext} from '../services/app-context.js';
+  import {initDiffAppContext} from './gr-diff-app-context-init.js';
+  suite('gr diff app context initializer tests', () => {
+    setup(() => {
+      initDiffAppContext();
+    });
+
+    test('all services initialized and are singletons', () => {
+      Object.keys(appContext).forEach(serviceName => {
+        const service = appContext[serviceName];
+        assert.isNotNull(service);
+        const service2 = appContext[serviceName];
+        assert.strictEqual(service, service2);
+      });
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/embed/gr-diff.js b/polygerrit-ui/app/embed/gr-diff.js
index a8b7e03..5907842 100644
--- a/polygerrit-ui/app/embed/gr-diff.js
+++ b/polygerrit-ui/app/embed/gr-diff.js
@@ -16,9 +16,16 @@
  */
 
 window.Gerrit = window.Gerrit || {};
+// TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
+// https://github.com/Polymer/polymer-resin/issues/9 is resolved.
+// Because gr-diff.js is a shared component, it shouldn' pollute global
+// variables. If an application wants to use Polymer global variable -
+// the app must assign/import it and do not rely on the Polymer variable
+// exposed by shared gr-diff component.
+import '../scripts/bundled-polymer.js';
 import '../elements/diff/gr-diff/gr-diff.js';
 import '../elements/diff/gr-diff-cursor/gr-diff-cursor.js';
-import {initDiffAppContext} from './app-context-init.js';
+import {initDiffAppContext} from './gr-diff-app-context-init.js';
 import {GrDiffLine} from '../elements/diff/gr-diff/gr-diff-line.js';
 import {GrAnnotation} from '../elements/diff/gr-diff-highlight/gr-annotation.js';
 
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index fe07569..1141d6b 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -261,26 +261,10 @@
     }
   },
   {
-    name: "es6-promise",
-    license: {
-      name: "es6-promise",
-      type: LicenseTypes.Mit,
-      packageLicenseFile: "LICENSE"
-    }
-  },
-  {
     name: "isarray",
     license: SharedLicenses.IsArray
   },
   {
-    name: "moment",
-    license: {
-      name: "moment",
-      type: LicenseTypes.Mit,
-      packageLicenseFile: "LICENSE"
-    }
-  },
-  {
     name: "page",
     license: SharedLicenses.Page
   },
@@ -299,14 +283,6 @@
   {
     name: "polymer-bridges",
     license: SharedLicenses.Polymer2018
-  },
-  {
-    name: "whatwg-fetch",
-    license: {
-      name: "whatwg-fetch",
-      type: LicenseTypes.Mit,
-      packageLicenseFile: "LICENSE"
-    }
   }
 ];
 
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 5989493..32560ff 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -25,13 +25,10 @@
     "@polymer/polymer": "^3.3.0",
     "@webcomponents/shadycss": "^1.9.2",
     "@webcomponents/webcomponentsjs": "^1.3.3",
-    "es6-promise": "^3.3.1",
-    "moment": "^2.24.0",
     "page": "^1.11.5",
     "polymer-bridges": "file:../../polymer-bridges/",
     "ba-linkify": "file:../../lib/ba-linkify/src/",
-    "polymer-resin": "^2.0.1",
-    "whatwg-fetch": "^3.0.0"
+    "polymer-resin": "^2.0.1"
   },
   "license": "Apache-2.0",
   "private": true
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 9303f2b..bc2830d 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -124,13 +124,14 @@
         tags = [
             "local",
             "manual",
+            "wct",
         ],
     )
 
 def wct_suite(name, srcs, split_count):
     """Define test suites for WCT tests.
 
-    All tests files are splited to split_count WCT suites
+    All tests files are split to split_count WCT suites
 
     Args:
         name: rule name. The macro create a test suite rule with the name name+"_test"
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index 5f61de7..a5231a1 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -14,4 +14,5 @@
       --test_env="DISPLAY=${DISPLAY}" \
       --test_env="WCT_HEADLESS_MODE=${WCT_HEADLESS_MODE}" \
       "$@" \
-      //polygerrit-ui/app:wct_test
+      //polygerrit-ui/app:wct_test \
+      //polygerrit-ui:karma_test
diff --git a/polygerrit-ui/app/samples/add-from-favorite-reviewers.js b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
new file mode 100644
index 0000000..76b2787
--- /dev/null
+++ b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
@@ -0,0 +1,145 @@
+/**
+ * @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.
+ */
+/**
+ * This plugin will a button to quickly add favorite reviewers to
+ * reviewers in reply dialog.
+ */
+
+const onToggleButtonClicks = [];
+function toggleButtonClicked(expanded) {
+  onToggleButtonClicks.forEach(cb => {
+    cb(expanded);
+  });
+}
+
+class ReviewerShortcut extends Polymer.Element {
+  static get is() { return 'reviewer-shortcut'; }
+
+  static get properties() {
+    return {
+      change: Object,
+      expanded: {
+        type: Boolean,
+        value: false,
+      },
+    };
+  }
+
+  static get template() {
+    return Polymer.html`
+      <button on-click="toggleControlContent">
+        [[computeButtonText(expanded)]]
+      </button>
+    `;
+  }
+
+  toggleControlContent() {
+    this.expanded = !this.expanded;
+    toggleButtonClicked(this.expanded);
+  }
+
+  computeButtonText(expanded) {
+    return expanded ? 'Collapse' : 'Add favorite reviewers';
+  }
+}
+
+customElements.define(ReviewerShortcut.is, ReviewerShortcut);
+
+class ReviewerShortcutContent extends Polymer.Element {
+  static get is() { return 'reviewer-shortcut-content'; }
+
+  static get properties() {
+    return {
+      change: Object,
+      hidden: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+    };
+  }
+
+  static get template() {
+    return Polymer.html`
+      <style>
+      :host([hidden]) {
+        display: none;
+      }
+      :host {
+        display: block;
+      }
+      </style>
+      <ul>
+        <li><button on-click="addApple">Apple</button></li>
+        <li><button on-click="addBanana">Banana</button></li>
+        <li><button on-click="addCherry">Cherry</button></li>
+      </ul>
+    `;
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    onToggleButtonClicks.push(expanded => {
+      this.hidden = !expanded;
+    });
+  }
+
+  addApple() {
+    this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+      reviewer: {
+        display_name: 'Apple',
+        email: 'apple@gmail.com',
+        name: 'Apple',
+        _account_id: 0,
+      },
+    },
+    composed: true, bubbles: true}));
+  }
+
+  addBanana() {
+    this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+      reviewer: {
+        display_name: 'Banana',
+        email: 'banana@gmail.com',
+        name: 'B',
+        _account_id: 1,
+      },
+    },
+    composed: true, bubbles: true}));
+  }
+
+  addCherry() {
+    this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+      reviewer: {
+        display_name: 'Cherry',
+        email: 'cherry@gmail.com',
+        name: 'C',
+        _account_id: 2,
+      },
+    },
+    composed: true, bubbles: true}));
+  }
+}
+
+customElements.define(ReviewerShortcutContent.is, ReviewerShortcutContent);
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'reply-reviewers', ReviewerShortcut.is, {slot: 'right'});
+  plugin.registerCustomComponent(
+      'reply-reviewers', ReviewerShortcutContent.is, {slot: 'below'});
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/bind-parameters.js b/polygerrit-ui/app/samples/bind-parameters.js
index 8f08e27..2c89064 100644
--- a/polygerrit-ui/app/samples/bind-parameters.js
+++ b/polygerrit-ui/app/samples/bind-parameters.js
@@ -14,9 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const {Element, html} = Polymer;
 
-class MyBindSample extends Element {
+// Element class exists in all browsers:
+// https://developer.mozilla.org/en-US/docs/Web/API/Element
+// Rename it to PolymerElement to avoid conflicts. Also,
+// typescript reports the following error:
+// error TS2451: Cannot redeclare block-scoped variable 'Element'.
+const {html, Element: PolymerElement} = Polymer;
+
+class MyBindSample extends PolymerElement {
   static get is() { return 'my-bind-sample'; }
 
   static get properties() {
@@ -62,4 +68,4 @@
   // between the file list and the change log
   plugin.registerCustomComponent(
       'change-view-integration', 'my-bind-sample');
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/extra-column-on-file-list.js b/polygerrit-ui/app/samples/extra-column-on-file-list.js
new file mode 100644
index 0000000..2e37c01
--- /dev/null
+++ b/polygerrit-ui/app/samples/extra-column-on-file-list.js
@@ -0,0 +1,77 @@
+/**
+ * @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.
+ */
+/**
+ * This plugin will an extra column to file list on change page to show
+ * the first character of the path.
+ */
+
+// Header of this extra column
+class ColumnHeader extends Polymer.Element {
+  static get is() { return 'column-header'; }
+
+  static get template() {
+    return Polymer.html`
+      <style>
+      :host {
+        display: block;
+        padding-right: var(--spacing-m);
+        min-width: 5em;
+      }
+      </style>
+      <div>First Char</div>
+    `;
+  }
+}
+
+customElements.define(ColumnHeader.is, ColumnHeader);
+
+// Content of this extra column
+class ColumnContent extends Polymer.Element {
+  static get is() { return 'column-content'; }
+
+  static get properties() {
+    return {
+      path: String,
+    };
+  }
+
+  static get template() {
+    return Polymer.html`
+      <style>
+      :host {
+        display:block;
+        padding-right: var(--spacing-m);
+        min-width: 5em;
+      }
+      </style>
+      <div>[[getStatus(path)]]</div>
+    `;
+  }
+
+  getStatus(path) {
+    return path.charAt(0);
+  }
+}
+
+customElements.define(ColumnContent.is, ColumnContent);
+
+Gerrit.install(plugin => {
+  plugin.registerDynamicCustomComponent(
+      'change-view-file-list-header-prepend', ColumnHeader.is);
+  plugin.registerDynamicCustomComponent(
+      'change-view-file-list-content-prepend', ColumnContent.is);
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/repo-command.js b/polygerrit-ui/app/samples/repo-command.js
index 00f95f5..5aaea30 100644
--- a/polygerrit-ui/app/samples/repo-command.js
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -14,9 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const {Element, html} = Polymer;
 
-class RepoCommandLow extends Element {
+// Element class exists in all browsers:
+// https://developer.mozilla.org/en-US/docs/Web/API/Element
+// Rename it to PolymerElement to avoid conflicts. Also,
+// typescript reports the following error:
+// error TS2451: Cannot redeclare block-scoped variable 'Element'.
+const {html, Element: PolymerElement} = Polymer;
+
+class RepoCommandLow extends PolymerElement {
   static get is() { return 'repo-command-low'; }
 
   static get properties() {
@@ -27,11 +33,19 @@
 
   static get template() {
     return html`
-    <gr-repo-command
-      title="Low-level bork"
-      on-command-tap="_handleCommandTap">
-    </gr-repo-command>
-    `;
+    <style include="shared-styles">
+    :host {
+      display: block;
+      margin-bottom: var(--spacing-xxl);
+    }
+    </style>
+    <h3>Low-level bork</h3>
+    <gr-button
+      on-click="_handleCommandTap"
+    >
+      Low-level bork
+    </gr-button>
+   `;
   }
 
   connectedCallback() {
@@ -70,4 +84,4 @@
   // Low-level API
   plugin.registerCustomComponent(
       'repo-command', 'repo-command-low');
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/some-screen.js b/polygerrit-ui/app/samples/some-screen.js
index 09acc81..c600fe4 100644
--- a/polygerrit-ui/app/samples/some-screen.js
+++ b/polygerrit-ui/app/samples/some-screen.js
@@ -14,9 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-const {Element, html} = Polymer;
 
-class SomeScreenMain extends Element {
+// Element class exists in all browsers:
+// https://developer.mozilla.org/en-US/docs/Web/API/Element
+// Rename it to PolymerElement to avoid conflicts. Also,
+// typescript reports the following error:
+// error TS2451: Cannot redeclare block-scoped variable 'Element'.
+const {html, Element: PolymerElement} = Polymer;
+
+class SomeScreenMain extends PolymerElement {
   static get is() { return 'some-screen-main'; }
 
   static get properties() {
@@ -64,4 +70,4 @@
   plugin.hook('change-metadata-item').onAttached(el => {
     el.innerHTML = `<a href="${mainUrl}">Plugin screen</a>`;
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/theme-plugin.js b/polygerrit-ui/app/samples/theme-plugin.js
index f3a8931..b3d4033 100644
--- a/polygerrit-ui/app/samples/theme-plugin.js
+++ b/polygerrit-ui/app/samples/theme-plugin.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 const customTheme = document.createElement('dom-module');
-customTheme.id = 'theme-plugin';
 customTheme.innerHTML = `
   <template>
     <style>
@@ -25,9 +24,9 @@
     </style>
   </template>
 `;
+customTheme.register('theme-plugin');
 
 const darkCustomTheme = document.createElement('dom-module');
-darkCustomTheme.id = 'dark-theme-plugin';
 darkCustomTheme.innerHTML = `
   <template>
     <style>
@@ -37,6 +36,7 @@
     </style>
   </template>
 `;
+darkCustomTheme.register('dark-theme-plugin');
 
 /**
  * This plugin will change the primary text color to red.
diff --git a/polygerrit-ui/app/scripts/bundled-polymer.js b/polygerrit-ui/app/scripts/bundled-polymer.js
index 711d587..780d82a 100644
--- a/polygerrit-ui/app/scripts/bundled-polymer.js
+++ b/polygerrit-ui/app/scripts/bundled-polymer.js
@@ -17,9 +17,9 @@
 
 // This file is a replacement for the
 // polymer-bridges/polymer/polymer.html file. The polymer.html file loads
-// other scripts to setup different global variables. Because polygerrit
-// code still uses global variables (like Polymer.importHref and other),
-// we must setup this global variables after conversion to es6 modules.
+// other scripts to setup different global variables. Because plugins
+// expects that Polymer is available we must setup all Polymer global
+// variables
 //
 // The bundled-polymer.js imports all scripts in the same order as the
 // polymer.html does and must be imported in all es6-modules instead
@@ -68,4 +68,4 @@
 import 'polymer-bridges/polymer/polymer-legacy_bridge.js';
 import {importHref} from './import-href.js';
 
-Polymer.importHref = importHref;
+window.Polymer.importHref = importHref;
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 46fa1ad..e4be858 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -15,28 +15,9 @@
  * limitations under the License.
  */
 
-function getPathFromNode(el) {
-  if (!el.tagName || el.tagName === 'GR-APP'
-      || el instanceof DocumentFragment
-      || el instanceof HTMLSlotElement) {
-    return '';
-  }
-  let path = el.tagName.toLowerCase();
-  if (el.id) path += `#${el.id}`;
-  if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
-  return path;
-}
 // TODO (dmfilippov): Each function must be exported separately. According to
 // the code style guide, a namespacing is not allowed.
 export const util = {
-  parseDate(dateStr) {
-    // Timestamps are given in UTC and have the format
-    // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
-    // nanoseconds.
-    // Munge the date into an ISO 8061 format and parse that.
-    return new Date(dateStr.replace(' ', 'T') + 'Z');
-  },
-
   getCookie(name) {
     const key = name + '=';
     const cookies = document.cookie.split(';');
@@ -84,133 +65,4 @@
     };
     return wrappedPromise;
   },
-
-  /**
-   * Get computed style value.
-   *
-   * If ShadyCSS is provided, use ShadyCSS api.
-   * If `getComputedStyleValue` is provided on the elment, use it.
-   * Otherwise fallback to native method (in polymer 2).
-   *
-   */
-  getComputedStyleValue: (name, el) => {
-    let style;
-    if (window.ShadyCSS) {
-      style = ShadyCSS.getComputedStyleValue(el, name);
-    } else if (el.getComputedStyleValue) {
-      style = el.getComputedStyleValue(name);
-    } else {
-      style = getComputedStyle(el).getPropertyValue(name);
-    }
-    return style;
-  },
-
-  /**
-   * Query selector on a dom element.
-   *
-   * This is shadow DOM compatible, but only works when selector is within
-   * one shadow host, won't work if your selector is crossing
-   * multiple shadow hosts.
-   *
-   */
-  querySelector: (el, selector) => {
-    let nodes = [el];
-    let result = null;
-    while (nodes.length) {
-      const node = nodes.pop();
-
-      // Skip if it's an invalid node.
-      if (!node || !node.querySelector) continue;
-
-      // Try find it with native querySelector directly
-      result = node.querySelector(selector);
-
-      if (result) {
-        break;
-      }
-
-      // Add all nodes with shadowRoot and loop through
-      const allShadowNodes = [...node.querySelectorAll('*')]
-          .filter(child => !!child.shadowRoot)
-          .map(child => child.shadowRoot);
-      nodes = nodes.concat(allShadowNodes);
-
-      // Add shadowRoot of current node if has one
-      // as its not included in node.querySelectorAll('*')
-      if (node.shadowRoot) {
-        nodes.push(node.shadowRoot);
-      }
-    }
-    return result;
-  },
-
-  /**
-   * Query selector all dom elements matching with certain selector.
-   *
-   * This is shadow DOM compatible, but only works when selector is within
-   * one shadow host, won't work if your selector is crossing
-   * multiple shadow hosts.
-   *
-   * Note: this can be very expensive, only use when have to.
-   */
-  querySelectorAll: (el, selector) => {
-    let nodes = [el];
-    const results = new Set();
-    while (nodes.length) {
-      const node = nodes.pop();
-
-      if (!node || !node.querySelectorAll) continue;
-
-      // Try find all from regular children
-      [...node.querySelectorAll(selector)]
-          .forEach(el => results.add(el));
-
-      // Add all nodes with shadowRoot and loop through
-      const allShadowNodes = [...node.querySelectorAll('*')]
-          .filter(child => !!child.shadowRoot)
-          .map(child => child.shadowRoot);
-      nodes = nodes.concat(allShadowNodes);
-
-      // Add shadowRoot of current node if has one
-      // as its not included in node.querySelectorAll('*')
-      if (node.shadowRoot) {
-        nodes.push(node.shadowRoot);
-      }
-    }
-    return [...results];
-  },
-
-  /**
-   * Retrieves the dom path of the current event.
-   *
-   * If the event object contains a `path` property, then use it,
-   * otherwise, construct the dom path based on the event target.
-   *
-   * @param {!Event} e
-   * @return {string}
-   * @example
-   *
-   * domNode.onclick = e => {
-   *  getEventPath(e); // eg: div.class1>p#pid.class2
-   * }
-   */
-  getEventPath: e => {
-    if (!e) return '';
-
-    let path = e.path;
-    if (!path || !path.length) {
-      path = [];
-      let el = e.target;
-      while (el) {
-        path.push(el);
-        el = el.parentNode || el.host;
-      }
-    }
-
-    return path.reduce((domPath, curEl) => {
-      const pathForEl = getPathFromNode(curEl);
-      if (!pathForEl) return domPath;
-      return domPath ? `${pathForEl}>${domPath}` : pathForEl;
-    }, '');
-  },
 };
diff --git a/polygerrit-ui/app/scripts/util_test.html b/polygerrit-ui/app/scripts/util_test.html
deleted file mode 100644
index a3893d2..0000000
--- a/polygerrit-ui/app/scripts/util_test.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <div id="test" class="a b c">
-      <a class="testBtn"></a>
-    </div>
-  </template>
-</test-fixture>
-
-<script type="module">
-  import '../test/common-test-setup.js';
-  import {util} from './util.js';
-  suite('util tests', () => {
-    suite('getEventPath', () => {
-      test('empty event', () => {
-        assert.equal(util.getEventPath(), '');
-        assert.equal(util.getEventPath(null), '');
-        assert.equal(util.getEventPath(undefined), '');
-        assert.equal(util.getEventPath({}), '');
-      });
-
-      test('event with fake path', () => {
-        assert.equal(util.getEventPath({path: []}), '');
-        assert.equal(util.getEventPath({path: [
-          {tagName: 'dd'},
-        ]}), 'dd');
-      });
-
-      test('event with fake complicated path', () => {
-        assert.equal(util.getEventPath({path: [
-          {tagName: 'dd', id: 'test', className: 'a b'},
-          {tagName: 'DIV', id: 'test2', className: 'a b c'},
-        ]}), 'div#test2.a.b.c>dd#test.a.b');
-      });
-
-      test('event with fake target', () => {
-        const fakeTargetParent2 = {
-          tagName: 'DIV', id: 'test2', className: 'a b c',
-        };
-        const fakeTargetParent1 = {
-          parentNode: fakeTargetParent2,
-          tagName: 'dd',
-          id: 'test',
-          className: 'a b',
-        };
-        const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
-        assert.equal(
-            util.getEventPath({target: fakeTarget}),
-            'div#test2.a.b.c>dd#test.a.b>span'
-        );
-      });
-
-      test('event with real click', () => {
-        const element = fixture('basic');
-        const aLink = element.querySelector('a');
-        let path;
-        aLink.onclick = e => path = util.getEventPath(e);
-        MockInteractions.click(aLink);
-        assert.equal(
-            path,
-            'html>body>test-fixture#basic>div#test.a.b.c>a.testBtn'
-        );
-      });
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
index 1c32eee..983314e 100644
--- a/polygerrit-ui/app/services/app-context-init.js
+++ b/polygerrit-ui/app/services/app-context-init.js
@@ -16,14 +16,15 @@
  */
 import {appContext} from './app-context.js';
 import {FlagsService} from './flags.js';
+import {GrReporting} from './gr-reporting/gr-reporting.js';
 
 const initializedServices = new Map();
 
 function getService(serviceName, serviceInit) {
-  if (!initializedServices[serviceName]) {
-    initializedServices[serviceName] = serviceInit();
+  if (!initializedServices.has(serviceName)) {
+    initializedServices.set(serviceName, serviceInit());
   }
-  return initializedServices[serviceName];
+  return initializedServices.get(serviceName);
 }
 
 /**
@@ -43,6 +44,8 @@
   }
 
   addService('flagsService', () => new FlagsService());
+  addService('reportingService',
+      () => new GrReporting(appContext.flagsService));
 
   Object.defineProperties(appContext, registeredServices);
 }
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.js
index e10ced5..62b396d 100644
--- a/polygerrit-ui/app/services/app-context.js
+++ b/polygerrit-ui/app/services/app-context.js
@@ -23,4 +23,5 @@
  */
 export const appContext = {
   flagsService: null,
+  reportingService: null,
 };
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/flags.js b/polygerrit-ui/app/services/flags.js
index 8f04f4a..64f0115 100644
--- a/polygerrit-ui/app/services/flags.js
+++ b/polygerrit-ui/app/services/flags.js
@@ -16,6 +16,15 @@
  */
 
 /**
+ * @enum
+ * @desc Experiment ids used in Gerrit.
+ */
+export const ExperimentIds = {
+  CLEANER_CHANGELOG: 'UiFeature__cleaner_changelog',
+  PATCHSET_COMMENTS: 'UiFeature__patchset_comments',
+};
+
+/**
  * Flags service.
  *
  * Provides all related methods / properties regarding on feature flags.
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
similarity index 65%
rename from polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
rename to polygerrit-ui/app/services/gr-reporting/gr-reporting.js
index c8b4ff5..42d112e 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
@@ -14,49 +14,53 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {appContext} from '../../../services/app-context.js';
 
 // Latency reporting constants.
+
 const TIMING = {
   TYPE: 'timing-report',
-  CATEGORY_UI_LATENCY: 'UI Latency',
-  CATEGORY_RPC: 'RPC Timing',
-  // Reported events - alphabetize below.
-  APP_STARTED: 'App Started',
+  CATEGORY: {
+    UI_LATENCY: 'UI Latency',
+    RPC: 'RPC Timing',
+  },
+  EVENT: {
+    APP_STARTED: 'App Started',
+  },
 };
 
-// Plugin-related reporting constants.
-const PLUGINS = {
+const LIFECYCLE = {
   TYPE: 'lifecycle',
-  // Reported events - alphabetize below.
-  INSTALLED: 'Plugins installed',
+  CATEGORY: {
+    DEFAULT: 'Default',
+    EXTENSION_DETECTED: 'Extension detected',
+    PLUGINS_INSTALLED: 'Plugins installed',
+  },
 };
 
-// Chrome extension-related reporting constants.
-const EXTENSION = {
-  TYPE: 'lifecycle',
-  // Reported events - alphabetize below.
-  DETECTED: 'Extension detected',
+const INTERACTION = {
+  TYPE: 'interaction',
+  CATEGORY: {
+    DEFAULT: 'Default',
+    VISIBILITY: 'Visibility',
+  },
 };
 
-// Navigation reporting constants.
 const NAVIGATION = {
   TYPE: 'nav-report',
-  CATEGORY: 'Location Changed',
-  PAGE: 'Page',
+  CATEGORY: {
+    LOCATION_CHANGED: 'Location Changed',
+  },
+  EVENT: {
+    PAGE: 'Page',
+  },
 };
 
 const ERROR = {
   TYPE: 'error',
-  CATEGORY: 'exception',
-};
-
-const ERROR_DIALOG = {
-  TYPE: 'error',
-  CATEGORY: 'Error Dialog',
+  CATEGORY: {
+    EXCEPTION: 'exception',
+    ERROR_DIALOG: 'Error Dialog',
+  },
 };
 
 const TIMER = {
@@ -89,125 +93,158 @@
 STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
 STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
 STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
+STARTUP_TIMERS[TIMING.EVENT.APP_STARTED] = 0;
 // WebComponentsReady timer is triggered from gr-router.
 STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
 
-const INTERACTION_TYPE = 'interaction';
-
 const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
 const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
-
-let pending = [];
-let slowRpcList = [];
 const SLOW_RPC_THRESHOLD = 500;
 
-// Variables that hold context info in global scope
-let reportRepoName = undefined;
-
-const onError = function(oldOnError, msg, url, line, column, error) {
-  if (oldOnError) {
-    oldOnError(msg, url, line, column, error);
-  }
-  if (error) {
-    line = line || error.lineNumber;
-    column = column || error.columnNumber;
-    let shortenedErrorStack = msg;
-    if (error.stack) {
-      const errorStackLines = error.stack.split('\n');
-      shortenedErrorStack = errorStackLines.slice(0,
-          Math.min(3, errorStackLines.length)).join('\n');
+export function initErrorReporter(appContext) {
+  const reportingService = appContext.reportingService;
+  const onError = function(oldOnError, msg, url, line, column, error) {
+    if (oldOnError) {
+      oldOnError(msg, url, line, column, error);
     }
-    msg = shortenedErrorStack || error.toString();
-  }
-  const payload = {
-    url,
-    line,
-    column,
-    error,
-  };
-  GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
-  return true;
-};
-
-const catchErrors = function(opt_context) {
-  const context = opt_context || window;
-  context.onerror = onError.bind(null, context.onerror);
-  context.addEventListener('unhandledrejection', e => {
-    const msg = e.reason.message;
-    const payload = {
-      error: e.reason,
-    };
-    GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
-  });
-};
-catchErrors();
-
-// PerformanceObserver interface is a browser API.
-if (window.PerformanceObserver) {
-  const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
-  // Safari doesn't support longtask yet
-  if (supportedEntryTypes.includes('longtask')) {
-    const catchLongJsTasks = new PerformanceObserver(list => {
-      for (const task of list.getEntries()) {
-        // We are interested in longtask longer than 200 ms (default is 50 ms)
-        if (task.duration > 200) {
-          GrReporting.prototype.reporter(TIMING.TYPE,
-              TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`,
-              Math.round(task.duration), {}, false);
-        }
+    if (error) {
+      line = line || error.lineNumber;
+      column = column || error.columnNumber;
+      let shortenedErrorStack = msg;
+      if (error.stack) {
+        const errorStackLines = error.stack.split('\n');
+        shortenedErrorStack = errorStackLines.slice(0,
+            Math.min(3, errorStackLines.length)).join('\n');
       }
+      msg = shortenedErrorStack || error.toString();
+    }
+    const payload = {
+      url,
+      line,
+      column,
+      error,
+    };
+    reportingService.reporter(ERROR.TYPE, ERROR.CATEGORY.EXCEPTION,
+        msg, payload);
+    return true;
+  };
+
+  const catchErrors = function(opt_context) {
+    const context = opt_context || window;
+    context.onerror = onError.bind(null, context.onerror);
+    context.addEventListener('unhandledrejection', e => {
+      const msg = e.reason.message;
+      const payload = {
+        error: e.reason,
+      };
+      reportingService.reporter(ERROR.TYPE,
+          ERROR.CATEGORY.EXCEPTION, msg, payload);
     });
-    catchLongJsTasks.observe({entryTypes: ['longtask']});
+  };
+
+  catchErrors();
+
+  // for testing
+  return {catchErrors};
+}
+
+export function initPerformanceReporter(appContext) {
+  const reportingService = appContext.reportingService;
+  // PerformanceObserver interface is a browser API.
+  if (window.PerformanceObserver) {
+    const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
+    // Safari doesn't support longtask yet
+    if (supportedEntryTypes.includes('longtask')) {
+      const catchLongJsTasks = new PerformanceObserver(list => {
+        for (const task of list.getEntries()) {
+          // We are interested in longtask longer than 200 ms (default is 50 ms)
+          if (task.duration > 200) {
+            reportingService.reporter(TIMING.TYPE,
+                TIMING.CATEGORY.UI_LATENCY, `Task ${task.name}`,
+                Math.round(task.duration), {}, false);
+          }
+        }
+      });
+      catchLongJsTasks.observe({entryTypes: ['longtask']});
+    }
   }
 }
 
-document.addEventListener('visibilitychange', () => {
-  const eventName = `Visibility changed to ${document.visibilityState}`;
-  GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName,
-      undefined, {}, true);
-});
+export function initVisibilityReporter(appContext) {
+  const reportingService = appContext.reportingService;
+  document.addEventListener('visibilitychange', () => {
+    reportingService.onVisibilityChange();
+  });
+}
 
-// The Polymer pass of JSCompiler requires this to be reassignable
-// eslint-disable-next-line prefer-const
-let GrReporting = Polymer({
-  is: 'gr-reporting',
+// Calculates the time of Gerrit being in a background tab. When Gerrit reports
+// a pageLoad metric it’s attached to its details for latency analysis.
+// It resets on locationChange.
+class HiddenDurationTimer {
+  constructor() {
+    this.reset();
+  }
 
-  properties: {
-    category: String,
+  reset() {
+    this.accHiddenDurationMs = 0;
+    this.lastVisibleTimestampMs = 0;
+  }
 
-    _baselines: {
-      type: Object,
-      value: STARTUP_TIMERS, // Shared across all instances.
-    },
+  onVisibilityChange() {
+    if (document.visibilityState === 'hidden') {
+      this.lastVisibleTimestampMs = now();
+    } else if (document.visibilityState === 'visible') {
+      if (this.lastVisibleTimestampMs !== null) {
+        this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
+        // Set to null for guarding against two 'visible' events in a row.
+        this.lastVisibleTimestampMs = null;
+      }
+    }
+  }
 
-    _timers: {
-      type: Object,
-      value: {timeBetweenDraftActions: null}, // Shared across all instances.
-    },
-  },
+  get hiddenDurationMs() {
+    if (document.visibilityState === 'hidden'
+      && this.lastVisibleTimestampMs !== null) {
+      return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
+    }
+    return this.accHiddenDurationMs;
+  }
+}
+
+export function now() {
+  return Math.round(window.performance.now());
+}
+
+export class GrReporting {
+  constructor(flagsService) {
+    this._flagsService = flagsService;
+    this._baselines = STARTUP_TIMERS;
+    this._timers = {
+      timeBetweenDraftActions: null,
+    };
+    this._reportRepoName = undefined;
+    this._pending = [];
+    this._slowRpcList = [];
+    this.hiddenDurationTimer = new HiddenDurationTimer();
+  }
 
   get performanceTiming() {
     return window.performance.timing;
-  },
+  }
 
   get slowRpcSnapshot() {
-    return slowRpcList.slice();
-  },
-
-  now() {
-    return Math.round(window.performance.now());
-  },
+    return (this._slowRpcList || []).slice();
+  }
 
   _arePluginsLoaded() {
     return this._baselines &&
       !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
-  },
+  }
 
   _isMetricsPluginLoaded() {
     return this._arePluginsLoaded() || this._baselines &&
       !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
-  },
+  }
 
   /**
    * Reporter reports events. Events will be queued if metrics plugin is not
@@ -224,24 +261,24 @@
   reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
     const eventInfo = this._createEventInfo(type, category,
         eventName, eventValue, eventDetails);
-    if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
+    if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
       console.error(eventValue && eventValue.error || eventName);
     }
 
     // We report events immediately when metrics plugin is loaded
-    if (this._isMetricsPluginLoaded() && !pending.length) {
+    if (this._isMetricsPluginLoaded() && !this._pending.length) {
       this._reportEvent(eventInfo, opt_noLog);
     } else {
       // We cache until metrics plugin is loaded
-      pending.push([eventInfo, opt_noLog]);
+      this._pending.push([eventInfo, opt_noLog]);
       if (this._isMetricsPluginLoaded()) {
-        pending.forEach(([eventInfo, opt_noLog]) => {
+        this._pending.forEach(([eventInfo, opt_noLog]) => {
           this._reportEvent(eventInfo, opt_noLog);
         });
-        pending = [];
+        this._pending = [];
       }
     }
-  },
+  }
 
   _reportEvent(eventInfo, opt_noLog) {
     const {type, value, name} = eventInfo;
@@ -254,7 +291,7 @@
         console.log(`Reporting: ${name}`);
       }
     }
-  },
+  }
 
   _createEventInfo(type, category, name, value, eventDetails) {
     const eventInfo = {
@@ -262,7 +299,7 @@
       category,
       name,
       value,
-      eventStart: this.now(),
+      eventStart: now(),
     };
 
     if (typeof(eventDetails) === 'object' &&
@@ -270,8 +307,8 @@
       eventInfo.eventDetails = JSON.stringify(eventDetails);
     }
 
-    if (reportRepoName) {
-      eventInfo.repoName = reportRepoName;
+    if (this._reportRepoName) {
+      eventInfo.repoName = this._reportRepoName;
     }
 
     const isInBackgroundTab = document.visibilityState === 'hidden';
@@ -279,21 +316,30 @@
       eventInfo.inBackgroundTab = isInBackgroundTab;
     }
 
-    const enabledExperiments = appContext.flagsService.enabledExperiments;
-    if (enabledExperiments.length) {
-      eventInfo.enabledExperiments = JSON.stringify(enabledExperiments);
+    if (this._flagsService.enabledExperiments.length) {
+      eventInfo.enabledExperiments =
+        JSON.stringify(this._flagsService.enabledExperiments);
     }
 
     return eventInfo;
-  },
+  }
 
   /**
    * User-perceived app start time, should be reported when the app is ready.
    */
   appStarted() {
-    this.timeEnd(TIMING.APP_STARTED);
+    this.timeEnd(TIMING.EVENT.APP_STARTED);
     this._reportNavResTimes();
-  },
+  }
+
+  onVisibilityChange() {
+    this.hiddenDurationTimer.onVisibilityChange();
+    const eventName = `Visibility changed to ${document.visibilityState}`;
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.VISIBILITY,
+        eventName, undefined, {
+          hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
+        }, true);
+  }
 
   /**
    * Browser's navigation and resource timings
@@ -303,7 +349,7 @@
     perfEvents.forEach(
         eventName => this._reportPerformanceTiming(eventName)
     );
-  },
+  }
 
   _reportPerformanceTiming(eventName, eventDetails) {
     const eventTiming = this.performanceTiming[eventName];
@@ -311,10 +357,10 @@
       const elapsedTime = eventTiming -
           this.performanceTiming.navigationStart;
       // NavResTime - Navigation and resource timings.
-      this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
+      this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY,
           `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
     }
-  },
+  }
 
   beforeLocationChanged() {
     for (const prop of Object.keys(this._baselines)) {
@@ -327,15 +373,16 @@
     this.time(TIMER.DIFF_VIEW_DISPLAYED);
     this.time(TIMER.DIFF_VIEW_LOAD_FULL);
     this.time(TIMER.FILE_LIST_DISPLAYED);
-    reportRepoName = undefined;
+    this._reportRepoName = undefined;
     // reset slow rpc list since here start page loads which report these rpcs
-    slowRpcList = [];
-  },
+    this._slowRpcList = [];
+    this.hiddenDurationTimer.reset();
+  }
 
   locationChanged(page) {
-    this.reporter(
-        NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
-  },
+    this.reporter(NAVIGATION.TYPE, NAVIGATION.CATEGORY.LOCATION_CHANGED,
+        NAVIGATION.EVENT.PAGE, page);
+  }
 
   dashboardDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
@@ -343,7 +390,7 @@
     } else {
       this.timeEnd(TIMER.DASHBOARD_DISPLAYED, this._pageLoadDetails());
     }
-  },
+  }
 
   changeDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
@@ -351,7 +398,7 @@
     } else {
       this.timeEnd(TIMER.CHANGE_DISPLAYED, this._pageLoadDetails());
     }
-  },
+  }
 
   changeFullyLoaded() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
@@ -359,7 +406,7 @@
     } else {
       this.timeEnd(TIMER.CHANGE_LOAD_FULL);
     }
-  },
+  }
 
   diffViewDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
@@ -367,7 +414,7 @@
     } else {
       this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
     }
-  },
+  }
 
   diffViewFullyLoaded() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
@@ -375,7 +422,7 @@
     } else {
       this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
     }
-  },
+  }
 
   diffViewContentDisplayed() {
     if (this._baselines.hasOwnProperty(
@@ -384,7 +431,7 @@
     } else {
       this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
     }
-  },
+  }
 
   fileListDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
@@ -392,7 +439,7 @@
     } else {
       this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
     }
-  },
+  }
 
   _pageLoadDetails() {
     const details = {
@@ -419,33 +466,35 @@
         toMb(window.performance.memory.usedJSHeapSize);
     }
 
+    details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
     return details;
-  },
+  }
 
   reportExtension(name) {
-    this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
-  },
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.EXTENSION_DETECTED, name);
+  }
 
   pluginLoaded(name) {
     if (name.startsWith('metrics-')) {
       this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
     }
-  },
+  }
 
   pluginsLoaded(pluginsList) {
     this.timeEnd(TIMER.PLUGINS_LOADED);
     this.reporter(
-        PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined,
+        LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+        LIFECYCLE.CATEGORY.PLUGINS_INSTALLED, undefined,
         {pluginsList: pluginsList || []}, true);
-  },
+  }
 
   /**
    * Reset named timer.
    */
   time(name) {
-    this._baselines[name] = this.now();
+    this._baselines[name] = now();
     window.performance.mark(`${name}-start`);
-  },
+  }
 
   /**
    * Finish named timer and report it to server.
@@ -454,7 +503,7 @@
     if (!this._baselines.hasOwnProperty(name)) { return; }
     const baseTime = this._baselines[name];
     delete this._baselines[name];
-    this._reportTiming(name, this.now() - baseTime, eventDetails);
+    this._reportTiming(name, now() - baseTime, eventDetails);
 
     // Finalize the interval. Either from a registered start mark or
     // the navigation start time (if baseTime is 0).
@@ -465,7 +514,7 @@
       // (if undefined).
       window.performance.measure(name);
     }
-  },
+  }
 
   /**
    * Reports just line timeEnd, but additionally reports an average given a
@@ -483,9 +532,9 @@
 
     // Guard against division by zero.
     if (!denominator) { return; }
-    const time = this.now() - baseTime;
+    const time = now() - baseTime;
     this._reportTiming(averageName, time / denominator);
-  },
+  }
 
   /**
    * Send a timing report with an arbitrary time value.
@@ -495,9 +544,9 @@
    * @param {Object} eventDetails non sensitive details
    */
   _reportTiming(name, time, eventDetails) {
-    this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time,
+    this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY, name, time,
         eventDetails);
-  },
+  }
 
   /**
    * Get a timer object to for reporing a user timing. The start time will be
@@ -517,7 +566,7 @@
       // Clear the timer and reset the start time.
       reset: () => {
         called = false;
-        start = this.now();
+        start = now();
         return timer;
       },
 
@@ -527,7 +576,7 @@
           throw new Error(`Timer for "${name}" already ended.`);
         }
         called = true;
-        const time = this.now() - start;
+        const time = now() - start;
 
         // If a maximum is specified and the time exceeds it, do not report.
         if (max && time > max) { return timer; }
@@ -546,7 +595,7 @@
 
     // The timer is initialized to its creation time.
     return timer.reset();
-  },
+  }
 
   /**
    * Log timing information for an RPC.
@@ -555,17 +604,22 @@
    * @param {number} elapsed The time elapsed of the RPC.
    */
   reportRpcTiming(anonymizedUrl, elapsed) {
-    this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl,
+    this.reporter(TIMING.TYPE, TIMING.CATEGORY.RPC, 'RPC-' + anonymizedUrl,
         elapsed, {}, true);
     if (elapsed >= SLOW_RPC_THRESHOLD) {
-      slowRpcList.push({anonymizedUrl, elapsed});
+      this._slowRpcList.push({anonymizedUrl, elapsed});
     }
-  },
+  }
+
+  reportLifeCycle(eventName, details) {
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.DEFAULT, eventName,
+        undefined, details, true);
+  }
 
   reportInteraction(eventName, details) {
-    this.reporter(INTERACTION_TYPE, this.category, eventName, undefined,
-        details, true);
-  },
+    this.reporter(INTERACTION.TYPE, INTERACTION.CATEGORY.DEFAULT, eventName,
+        undefined, details, true);
+  }
 
   /**
    * A draft interaction was started. Update the time-betweeen-draft-actions
@@ -585,19 +639,16 @@
 
     // Mark the time and reinitialize the timer.
     timer.end().reset();
-  },
+  }
 
   reportErrorDialog(message) {
-    this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
+    this.reporter(ERROR.TYPE, ERROR.CATEGORY.ERROR_DIALOG,
         'ErrorDialog: ' + message, {error: new Error(message)});
-  },
+  }
 
   setRepoName(repoName) {
-    reportRepoName = repoName;
-  },
-});
+    this._reportRepoName = repoName;
+  }
+}
 
-window.GrReporting = GrReporting;
-// Expose onerror installation so it would be accessible from tests.
-window.GrReporting._catchErrors = catchErrors;
-window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
+export const DEFAULT_STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js
new file mode 100644
index 0000000..1ef2483
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js
@@ -0,0 +1,45 @@
+/**
+ * @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.
+ */
+export const grReportingMock = {
+  appStarted: () => {},
+  beforeLocationChanged: () => {},
+  changeDisplayed: () => {},
+  changeFullyLoaded: () => {},
+  dashboardDisplayed: () => {},
+  diffViewContentDisplayed: () => {},
+  diffViewDisplayed: () => {},
+  diffViewFullyLoaded: () => {},
+  fileListDisplayed: () => {},
+  getTimer: () => {
+    return {end: () => {}};
+  },
+  locationChanged: () => {},
+  onVisibilityChange: () => {},
+  pluginLoaded: () => {},
+  pluginsLoaded: () => {},
+  recordDraftInteraction: () => {},
+  reporter: () => {},
+  reportErrorDialog: () => {},
+  reportExtension: () => {},
+  reportInteraction: () => {},
+  reportLifeCycle: () => {},
+  reportRpcTiming: () => {},
+  setRepoName: () => {},
+  time: () => {},
+  timeEnd: () => {},
+  timeEndWithAverage: () => {},
+};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html
new file mode 100644
index 0000000..e33a214
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+  import '../../test/common-test-setup.js';
+  import {GrReporting} from './gr-reporting.js';
+  import {grReportingMock} from './gr-reporting_mock.js';
+  suite('gr-reporting_mock tests', () => {
+    test('mocks all public methods', () => {
+      const methods = Object.getOwnPropertyNames(GrReporting.prototype)
+          .filter(name => typeof GrReporting.prototype[name] === 'function')
+          .filter(name => !name.startsWith('_') && name !== 'constructor')
+          .sort();
+      const mockMethods = Object.getOwnPropertyNames(grReportingMock)
+          .sort();
+      assert.deepEqual(methods, mockMethods);
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html
new file mode 100644
index 0000000..e309c8c
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,513 @@
+<!DOCTYPE html>
+<!--
+@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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<meta charset="utf-8">
+<title>gr-reporting</title>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-services-es5-adapter.js"></script>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+import '../../test/common-test-setup.js';
+import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting.js';
+import {appContext} from '../app-context.js';
+suite('gr-reporting tests', () => {
+  let service;
+  let sandbox;
+  let clock;
+  let fakePerformance;
+
+  const NOW_TIME = 100;
+
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    clock = sinon.useFakeTimers(NOW_TIME);
+    service = new GrReporting(appContext.flagsService);
+    service._baselines = Object.assign({}, DEFAULT_STARTUP_TIMERS);
+    sandbox.stub(service, 'reporter');
+  });
+
+  teardown(() => {
+    sandbox.restore();
+    clock.restore();
+  });
+
+  test('appStarted', () => {
+    fakePerformance = {
+      navigationStart: 1,
+      loadEventEnd: 2,
+    };
+    fakePerformance.toJSON = () => fakePerformance;
+    sinon.stub(service, 'performanceTiming',
+        {get() { return fakePerformance; }});
+    sandbox.stub(window.performance, 'now').returns(42);
+    service.appStarted();
+    assert.isTrue(
+        service.reporter.calledWithMatch(
+            'timing-report', 'UI Latency', 'App Started', 42
+        ));
+    assert.isTrue(
+        service.reporter.calledWithExactly(
+            'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
+            fakePerformance.loadEventEnd - fakePerformance.navigationStart,
+            undefined, true)
+    );
+  });
+
+  test('WebComponentsReady', () => {
+    sandbox.stub(window.performance, 'now').returns(42);
+    service.timeEnd('WebComponentsReady');
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'WebComponentsReady', 42
+    ));
+  });
+
+  test('beforeLocationChanged', () => {
+    service._baselines['garbage'] = 'monster';
+    sandbox.stub(service, 'time');
+    service.beforeLocationChanged();
+    assert.isTrue(service.time.calledWithExactly('DashboardDisplayed'));
+    assert.isTrue(service.time.calledWithExactly('ChangeDisplayed'));
+    assert.isTrue(service.time.calledWithExactly('ChangeFullyLoaded'));
+    assert.isTrue(service.time.calledWithExactly('DiffViewDisplayed'));
+    assert.isTrue(service.time.calledWithExactly('FileListDisplayed'));
+    assert.isFalse(service._baselines.hasOwnProperty('garbage'));
+  });
+
+  test('changeDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.changeDisplayed();
+    assert.isFalse(service.timeEnd.calledWith('ChangeDisplayed'));
+    assert.isTrue(service.timeEnd.calledWith('StartupChangeDisplayed'));
+    service.changeDisplayed();
+    assert.isTrue(service.timeEnd.calledWith('ChangeDisplayed'));
+  });
+
+  test('changeFullyLoaded', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.changeFullyLoaded();
+    assert.isFalse(
+        service.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+    assert.isTrue(
+        service.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
+    service.changeFullyLoaded();
+    assert.isTrue(service.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+  });
+
+  test('diffViewDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.diffViewDisplayed();
+    assert.isFalse(service.timeEnd.calledWith('DiffViewDisplayed'));
+    assert.isTrue(service.timeEnd.calledWith('StartupDiffViewDisplayed'));
+    service.diffViewDisplayed();
+    assert.isTrue(service.timeEnd.calledWith('DiffViewDisplayed'));
+  });
+
+  test('fileListDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.fileListDisplayed();
+    assert.isFalse(
+        service.timeEnd.calledWithExactly('FileListDisplayed'));
+    assert.isTrue(
+        service.timeEnd.calledWithExactly('StartupFileListDisplayed'));
+    service.fileListDisplayed();
+    assert.isTrue(service.timeEnd.calledWithExactly('FileListDisplayed'));
+  });
+
+  test('dashboardDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.dashboardDisplayed();
+    assert.isFalse(service.timeEnd.calledWith('DashboardDisplayed'));
+    assert.isTrue(service.timeEnd.calledWith('StartupDashboardDisplayed'));
+    service.dashboardDisplayed();
+    assert.isTrue(service.timeEnd.calledWith('DashboardDisplayed'));
+  });
+
+  test('dashboardDisplayed details', () => {
+    sandbox.spy(service, 'timeEnd');
+    sandbox.stub(window, 'performance', {
+      memory: {
+        usedJSHeapSize: 1024 * 1024,
+      },
+      measure: () => {},
+      now: () => { 42; },
+    });
+    service.reportRpcTiming('/changes/*~*/comments', 500);
+    service.dashboardDisplayed();
+    assert.isTrue(
+        service.timeEnd.calledWithExactly('StartupDashboardDisplayed',
+            {rpcList: [
+              {
+                anonymizedUrl: '/changes/*~*/comments',
+                elapsed: 500,
+              },
+            ],
+            screenSize: {
+              width: window.screen.width,
+              height: window.screen.height,
+            },
+            viewport: {
+              width: document.documentElement.clientWidth,
+              height: document.documentElement.clientHeight,
+            },
+            usedJSHeapSizeMb: 1,
+            hiddenDurationMs: 0,
+            }
+        ));
+  });
+
+  suite('hidden duration', () => {
+    let nowStub;
+    let visibilityStateStub;
+    const assertHiddenDurationsMs = hiddenDurationMs => {
+      service.dashboardDisplayed();
+      assert.isTrue(
+          service.timeEnd.calledWithMatch('StartupDashboardDisplayed',
+              {hiddenDurationMs}
+          ));
+    };
+
+    setup(() => {
+      sandbox.spy(service, 'timeEnd');
+      nowStub = sandbox.stub(window.performance, 'now');
+      visibilityStateStub = {
+        value: value => {
+          Object.defineProperty(document, 'visibilityState',
+              {value, configurable: true});
+        },
+      };
+    });
+
+    test('starts in hidden', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      service.onVisibilityChange();
+      nowStub.returns(15);
+      visibilityStateStub.value('visible');
+      service.onVisibilityChange();
+      assertHiddenDurationsMs(5);
+    });
+
+    test('full in hidden', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      assertHiddenDurationsMs(10);
+    });
+
+    test('full in visible', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('visible');
+      assertHiddenDurationsMs(0);
+    });
+
+    test('accumulated', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      service.onVisibilityChange();
+      nowStub.returns(15);
+      visibilityStateStub.value('visible');
+      service.onVisibilityChange();
+      nowStub.returns(20);
+      visibilityStateStub.value('hidden');
+      service.onVisibilityChange();
+      nowStub.returns(25);
+      assertHiddenDurationsMs(10);
+    });
+
+    test('reset after location change', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      assertHiddenDurationsMs(10);
+      visibilityStateStub.value('visible');
+      nowStub.returns(15);
+      service.beforeLocationChanged();
+      service.timeEnd.reset();
+      service.dashboardDisplayed();
+      assert.isTrue(
+          service.timeEnd.calledWithMatch('DashboardDisplayed',
+              {hiddenDurationMs: 0}
+          ));
+    });
+  });
+
+  test('time and timeEnd', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(0);
+    service.time('foo');
+    nowStub.returns(1);
+    service.time('bar');
+    nowStub.returns(2);
+    service.timeEnd('bar');
+    nowStub.returns(3);
+    service.timeEnd('foo');
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo', 3
+    ));
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'bar', 1
+    ));
+  });
+
+  test('timer object', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const timer = service.getTimer('foo-bar');
+    nowStub.returns(150);
+    timer.end();
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo-bar', 50));
+  });
+
+  test('timer object double call', () => {
+    const timer = service.getTimer('foo-bar');
+    timer.end();
+    assert.isTrue(service.reporter.calledOnce);
+    assert.throws(() => {
+      timer.end();
+    }, 'Timer for "foo-bar" already ended.');
+  });
+
+  test('timer object maximum', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const timer = service.getTimer('foo-bar').withMaximum(100);
+    nowStub.returns(150);
+    timer.end();
+    assert.isTrue(service.reporter.calledOnce);
+
+    timer.reset();
+    nowStub.returns(260);
+    timer.end();
+    assert.isTrue(service.reporter.calledOnce);
+  });
+
+  test('recordDraftInteraction', () => {
+    const key = 'TimeBetweenDraftActions';
+    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const timingStub = sandbox.stub(service, '_reportTiming');
+    service.recordDraftInteraction();
+    assert.isFalse(timingStub.called);
+
+    nowStub.returns(200);
+    service.recordDraftInteraction();
+    assert.isTrue(timingStub.calledOnce);
+    assert.equal(timingStub.lastCall.args[0], key);
+    assert.equal(timingStub.lastCall.args[1], 100);
+
+    nowStub.returns(350);
+    service.recordDraftInteraction();
+    assert.isTrue(timingStub.calledTwice);
+    assert.equal(timingStub.lastCall.args[0], key);
+    assert.equal(timingStub.lastCall.args[1], 150);
+
+    nowStub.returns(370 + 2 * 60 * 1000);
+    service.recordDraftInteraction();
+    assert.isFalse(timingStub.calledThrice);
+  });
+
+  test('timeEndWithAverage', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(0);
+    nowStub.returns(1000);
+    service.time('foo');
+    nowStub.returns(1100);
+    service.timeEndWithAverage('foo', 'bar', 10);
+    assert.isTrue(service.reporter.calledTwice);
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo', 100));
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'bar', 10));
+  });
+
+  test('reportExtension', () => {
+    service.reportExtension('foo');
+    assert.isTrue(service.reporter.calledWithExactly(
+        'lifecycle', 'Extension detected', 'foo'
+    ));
+  });
+
+  test('reportInteraction', () => {
+    service.reporter.restore();
+    sandbox.spy(service, '_reportEvent');
+    service.pluginsLoaded(); // so we don't cache
+    service.reportInteraction('button-click', {name: 'sendReply'});
+    assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
+        {
+          type: 'interaction',
+          name: 'button-click',
+          eventDetails: JSON.stringify({name: 'sendReply'}),
+        }
+    ));
+  });
+
+  test('report start time', () => {
+    service.reporter.restore();
+    sandbox.stub(window.performance, 'now').returns(42);
+    sandbox.spy(service, '_reportEvent');
+    const dispatchStub = sandbox.spy(document, 'dispatchEvent');
+    service.pluginsLoaded();
+    service.time('timeAction');
+    service.timeEnd('timeAction');
+    assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
+        {
+          type: 'timing-report',
+          category: 'UI Latency',
+          name: 'timeAction',
+          value: 0,
+          eventStart: 42,
+        }
+    ));
+    assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
+  });
+
+  suite('plugins', () => {
+    setup(() => {
+      service.reporter.restore();
+      sandbox.stub(service, '_reportEvent');
+    });
+
+    test('pluginsLoaded reports time', () => {
+      sandbox.stub(window.performance, 'now').returns(42);
+      service.pluginsLoaded();
+      assert.isTrue(service._reportEvent.calledWithMatch(
+          {
+            type: 'timing-report',
+            category: 'UI Latency',
+            name: 'PluginsLoaded',
+            value: 42,
+          }
+      ));
+    });
+
+    test('pluginsLoaded reports plugins', () => {
+      service.pluginsLoaded(['foo', 'bar']);
+      assert.isTrue(service._reportEvent.calledWithMatch(
+          {
+            type: 'lifecycle',
+            category: 'Plugins installed',
+            eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
+          }
+      ));
+    });
+
+    test('caches reports if plugins are not loaded', () => {
+      service.timeEnd('foo');
+      assert.isFalse(service._reportEvent.called);
+    });
+
+    test('reports if plugins are loaded', () => {
+      service.pluginsLoaded();
+      assert.isTrue(service._reportEvent.called);
+    });
+
+    test('reports if metrics plugin xyz is loaded', () => {
+      service.pluginLoaded('metrics-xyz');
+      assert.isTrue(service._reportEvent.called);
+    });
+
+    test('reports cached events preserving order', () => {
+      service.time('foo');
+      service.time('bar');
+      service.timeEnd('foo');
+      service.pluginsLoaded();
+      service.timeEnd('bar');
+      assert.isTrue(service._reportEvent.getCall(0).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency', name: 'foo'}
+      ));
+      assert.isTrue(service._reportEvent.getCall(1).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency',
+            name: 'PluginsLoaded'}
+      ));
+      assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
+          {type: 'lifecycle', category: 'Plugins installed'}
+      ));
+      assert.isTrue(service._reportEvent.getCall(3).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency', name: 'bar'}
+      ));
+    });
+  });
+
+  test('search', () => {
+    service.locationChanged('_handleSomeRoute');
+    assert.isTrue(service.reporter.calledWithExactly(
+        'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
+  });
+
+  suite('exception logging', () => {
+    let fakeWindow;
+    let reporter;
+
+    const emulateThrow = function(msg, url, line, column, error) {
+      return fakeWindow.onerror(msg, url, line, column, error);
+    };
+
+    setup(() => {
+      reporter = service.reporter;
+      fakeWindow = {
+        handlers: {},
+        addEventListener(type, handler) {
+          this.handlers[type] = handler;
+        },
+      };
+      sandbox.stub(console, 'error');
+      Object.defineProperty(appContext, 'reportingService', {
+        get() {
+          return service;
+        },
+      });
+      const errorReporter = initErrorReporter(appContext);
+      errorReporter.catchErrors(fakeWindow);
+    });
+
+    test('is reported', () => {
+      const error = new Error('bar');
+      error.stack = undefined;
+      emulateThrow('bar', 'http://url', 4, 2, error);
+      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+      const payload = reporter.lastCall.args[3];
+      assert.deepEqual(payload, {
+        url: 'http://url',
+        line: 4,
+        column: 2,
+        error,
+      });
+    });
+
+    test('is reported with 3 lines of stack', () => {
+      const error = new Error('bar');
+      emulateThrow('bar', 'http://url', 4, 2, error);
+      const expectedStack = error.stack.split('\n').slice(0, 3)
+          .join('\n');
+      assert.isTrue(reporter.calledWith('error', 'exception',
+          expectedStack));
+    });
+
+    test('prevent default event handler', () => {
+      assert.isTrue(emulateThrow());
+    });
+
+    test('unhandled rejection', () => {
+      fakeWindow.handlers['unhandledrejection']({
+        reason: {
+          message: 'bar',
+        },
+      });
+      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+    });
+  });
+});
+</script>
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.js b/polygerrit-ui/app/styles/gr-voting-styles.js
index 4860428..60bf623 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.js
+++ b/polygerrit-ui/app/styles/gr-voting-styles.js
@@ -26,6 +26,7 @@
           box-shadow: none;
           box-sizing: border-box;
           min-width: 3em;
+          color: var(--vote-text-color);
         }
       }
     </style>
diff --git a/polygerrit-ui/app/styles/shared-styles.js b/polygerrit-ui/app/styles/shared-styles.js
index f5e048f..3e81761 100644
--- a/polygerrit-ui/app/styles/shared-styles.js
+++ b/polygerrit-ui/app/styles/shared-styles.js
@@ -103,19 +103,19 @@
         font-weight: var(--font-weight-normal);
         line-height: var(--line-height-small);
       }
-      h1, .font-h1 {
+      .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);
       }
-      h2, .font-h2 {
+      .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);
       }
-      h3, .font-h3 {
+      .heading-3 {
         font-family: var(--header-font-family);
         font-size: var(--font-size-h3);
         font-weight: var(--font-weight-h3);
diff --git a/polygerrit-ui/app/styles/themes/app-theme.js b/polygerrit-ui/app/styles/themes/app-theme.js
index 5d5d9e3..718d6a5 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.js
+++ b/polygerrit-ui/app/styles/themes/app-theme.js
@@ -38,10 +38,12 @@
   --primary-button-text-color: white;
     /* Used on text color for change list that doesn't need user's attention. */
   --reviewed-text-color: black;
+  --vote-text-color: black;
+  --status-text-color: white;
   --tooltip-text-color: white;
-  --vote-text-color-recommended: #388e3c;
-  --vote-text-color-disliked: #d32f2f;
-
+  --negative-red-text-color: #d93025;
+  --positive-green-text-color: #188038;
+  
   /* background colors */
   /* primary background colors */
   --background-color-primary: #ffffff;
@@ -83,6 +85,16 @@
   --border-color: #e8e8e8;
   --comment-separator-color: #dadce0;
 
+  /* status colors */
+  --status-merged: #188038;
+  --status-abandoned: #5f6368;
+  --status-wip: #795548;
+  --status-private: #a142f4;
+  --status-conflict: #d93025;
+  --status-active: #1976d2;
+  --status-ready: #b80672;
+  --status-custom: #681da8;
+
   /* fonts */
   --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
   --header-font-family: 'Open Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 4248878..18b2fd6 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -33,12 +33,14 @@
       --deemphasized-text-color: #9aa0a6;
       --default-button-text-color: #8ab4f8;
       --error-text-color: red;
-      --primary-button-text-color: var(--primary-text-color);
+      --primary-button-text-color: black;
         /* Used on text color for change list doesn't need user's attention. */
       --reviewed-text-color: #dadce0;
+      --vote-text-color: black;
+      --status-text-color: black;
       --tooltip-text-color: white;
-      --vote-text-color-recommended: #388e3c;
-      --vote-text-color-disliked: #d32f2f;
+      --negative-red-text-color: #f28b82;
+      --positive-green-text-color: #81c995;
 
       /* background colors */
       /* primary background colors */
@@ -71,6 +73,16 @@
       --border-color: #5f6368;
       --comment-separator-color: var(--border-color);
 
+      /* status colors */
+      --status-merged: #5bb974;
+      --status-abandoned: #dadce0;
+      --status-wip: #bcaaa4;
+      --status-private: #d7aefb;
+      --status-conflict: #f28b82;
+      --status-active: #669df6;
+      --status-ready: #f439a0;
+      --status-custom: #af5cf7;
+
       /* fonts */
       --font-weight-bold: 700; /* 700 is the same as 'bold' */
 
diff --git a/polygerrit-ui/app/test/a11y-test-utils.js b/polygerrit-ui/app/test/a11y-test-utils.js
new file mode 100644
index 0000000..a687e07
--- /dev/null
+++ b/polygerrit-ui/app/test/a11y-test-utils.js
@@ -0,0 +1,44 @@
+/**
+ * @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 './common-test-setup-karma.js';
+
+// Run a11y audit on test fixture
+// The code is inspired by the
+// https://github.com/Polymer/web-component-tester/blob/master/data/a11ySuite.js
+export async function runA11yAudit(fixture, ignoredRules) {
+  fixture.instantiate();
+  await flush();
+  const axsConfig = new axs.AuditConfiguration();
+  axsConfig.scope = document.body;
+  axsConfig.showUnsupportedRulesWarning = false;
+  axsConfig.auditRulesToIgnore = ignoredRules;
+
+  const auditResults = axs.Audit.run(axsConfig);
+  const errors = [];
+  auditResults.forEach((result, index) => {
+    // only show applicable tests
+    if (result.result === 'FAIL') {
+      const title = result.rule.heading;
+      // fail test if audit result is FAIL
+      const error = axs.Audit.accessibilityErrorMessage(result);
+      errors.push(`${title}: ${error}`);
+    }
+  });
+  if (errors.length > 0) {
+    assert.fail(errors.join('\n') + '\n');
+  }
+}
diff --git a/polygerrit-ui/app/test/common-test-setup-karma.js b/polygerrit-ui/app/test/common-test-setup-karma.js
new file mode 100644
index 0000000..4fd3b03
--- /dev/null
+++ b/polygerrit-ui/app/test/common-test-setup-karma.js
@@ -0,0 +1,152 @@
+/**
+ * @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 './common-test-setup.js';
+import '@polymer/test-fixture/test-fixture.js';
+import 'chai/chai.js';
+self.assert = window.chai.assert;
+
+/**
+ * Triggers a flush of any pending events, observations, etc and calls you back
+ * after they have been processed if callback is passed; otherwise returns
+ * promise.
+ *
+ * @param {function()} callback
+ */
+function flush(callback) {
+  // Ideally, this function would be a call to Polymer.dom.flush, but that
+  // doesn't support a callback yet
+  // (https://github.com/Polymer/polymer-dev/issues/851)
+  window.Polymer.dom.flush();
+  if (callback) {
+    window.setTimeout(callback, 0);
+  } else {
+    return new Promise(resolve => {
+      window.setTimeout(resolve, 0);
+    });
+  }
+}
+
+self.flush = flush;
+
+class TestFixtureIdProvider {
+  static get instance() {
+    if (!TestFixtureIdProvider._instance) {
+      TestFixtureIdProvider._instance = new TestFixtureIdProvider();
+    }
+    return TestFixtureIdProvider._instance;
+  }
+
+  constructor() {
+    this.fixturesCount = 1;
+  }
+
+  generateNewFixtureId() {
+    this.fixturesCount++;
+    return `fixture-${this.fixturesCount}`;
+  }
+}
+
+class TestFixture {
+  constructor(fixtureId) {
+    this.fixtureId = fixtureId;
+  }
+
+  /**
+   * Create an instance of a fixture's template.
+   *
+   * @param {Object} model - see Data-bound sections at
+   *   https://www.webcomponents.org/element/@polymer/test-fixture
+   * @return {HTMLElement | HTMLElement[]} - if the fixture's template contains
+   *   a single element, returns the appropriated instantiated element.
+   *   Otherwise, it return an array of all instantiated elements from the
+   *   template.
+   */
+  instantiate(model) {
+    // The window.fixture method is defined in common-test-setup.js
+    return window.fixture(this.fixtureId, model);
+  }
+}
+
+/**
+ * Wraps provided template to a test-fixture tag and adds test-fixture to
+ * the document. You can use the html function to create a template.
+ *
+ * Example:
+ * import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+ *
+ * // Create fixture at the root level of a test file
+ * const basicTestFixture = fixtureFromTemplate(html`
+ *   <gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
+ *   <ul>
+ *    <li>A</li>
+ *    <li>B</li>
+ *    <li>C</li>
+ *    <li>D</li>
+ *   </ul>
+ * `);
+ * ...
+ * // Instantiate fixture when needed:
+ *
+ * suite('example') {
+ *   let elements;
+ *   setup(() => {
+ *     elements = basicTestFixture.instantiate();
+ *   });
+ * }
+ *
+ * @param {HTMLTemplateElement} template - a template for a fixture
+ * @return {TestFixture} - the instance of TestFixture class
+ */
+function fixtureFromTemplate(template) {
+  const fixtureId = TestFixtureIdProvider.instance.generateNewFixtureId();
+  const testFixture = document.createElement('test-fixture');
+  testFixture.setAttribute('id', fixtureId);
+  testFixture.appendChild(template);
+  document.body.appendChild(testFixture);
+  return new TestFixture(fixtureId);
+}
+
+/**
+ * Wraps provided tag to a test-fixture/template tags and adds test-fixture
+ * to the document.
+ *
+ * Example:
+ *
+ * // Create fixture at the root level of a test file
+ * const basicTestFixture = fixtureFromElement('gr-diff-view');
+ * ...
+ * // Instantiate fixture when needed:
+ *
+ * suite('example') {
+ *   let element;
+ *   setup(() => {
+ *     element = basicTestFixture.instantiate();
+ *   });
+ * }
+ *
+ * @param {HTMLTemplateElement} template - a template for a fixture
+ * @return {TestFixture} - the instance of TestFixture class
+ */
+function fixtureFromElement(tagName) {
+  const template = document.createElement('template');
+  template.innerHTML = `<${tagName}></${tagName}>`;
+  return fixtureFromTemplate(template);
+}
+
+window.fixtureFromTemplate = fixtureFromTemplate;
+window.fixtureFromElement = fixtureFromElement;
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index aea24da..db0d279 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -14,14 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
+// https://github.com/Polymer/polymer-resin/issues/9 is resolved.
 import '../scripts/bundled-polymer.js';
 
 import 'polymer-resin/standalone/polymer-resin.js';
 import '@polymer/iron-test-helpers/iron-test-helpers.js';
 import './test-router.js';
 import {SafeTypes} from '../behaviors/safe-types-behavior/safe-types-behavior.js';
+import {appContext} from '../services/app-context.js';
 import {initAppContext} from '../services/app-context-init.js';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
+import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
+
+// Returns true if tests run under the Karma
+function isKarmaTest() {
+  return window.__karma__ !== undefined;
+}
 
 security.polymer_resin.install({
   allowedIdentifierPrefixes: [''],
@@ -57,28 +67,45 @@
 // Note, that fixture(...) and stub(..) methods are registered different by
 // WCT. This is why these methods implemented slightly different here.
 const cleanups = [];
-if (!window.fixture) {
+if (isKarmaTest() || !window.fixture) {
+  // For karma always set our implementation
+  // (karma doesn't provide the fixture method)
   window.fixture = function(fixtureId, model) {
     // This method is inspired by WCT method
     cleanups.push(() => document.getElementById(fixtureId).restore());
     return document.getElementById(fixtureId).create(model);
   };
 } else {
+  // The following error is important for WCT tests.
+  // If window.fixture already installed by WCT at this point, WCT tests
+  // performance decreases rapidly.
+  // It allows to catch performance problems earlier.
   throw new Error('window.fixture must be set before wct sets it');
 }
 
 // On the first call to the setup, WCT installs window.fixture
-// and widnow.stub methods
+// and window.stub methods
 setup(() => {
   // If the following asserts fails - then window.stub is
   // overwritten by some other code.
   assert.equal(cleanups.length, 0);
 
   _testOnly_resetPluginLoader();
+
   initAppContext();
+  function setMock(serviceName, setupMock) {
+    Object.defineProperty(appContext, serviceName, {
+      get() {
+        return setupMock;
+      },
+    });
+  }
+  setMock('reportingService', grReportingMock);
 });
 
-if (window.stub) {
+if (isKarmaTest() || window.stub) {
+  // For karma always set our implementation
+  // (karma doesn't provide the stub method)
   window.stub = function(tagName, implementation) {
     // This method is inspired by WCT method
     const proto = document.createElement(tagName).constructor.prototype;
@@ -91,6 +118,10 @@
     });
   };
 } else {
+  // The following error is important for WCT tests.
+  // If window.fixture already installed by WCT at this point, WCT tests
+  // performance decreases rapidly.
+  // It allows to catch performance problems earlier.
   throw new Error('window.stub must be set after wct sets it');
 }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js b/polygerrit-ui/app/test/mocks/comment-api.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
rename to polygerrit-ui/app/test/mocks/comment-api.js
diff --git a/polygerrit-ui/app/test/mock-diff-response.js b/polygerrit-ui/app/test/mocks/diff-response.js
similarity index 100%
rename from polygerrit-ui/app/test/mock-diff-response.js
rename to polygerrit-ui/app/test/mocks/diff-response.js
diff --git a/polygerrit-ui/app/test/tests.js b/polygerrit-ui/app/test/tests.js
index 934d9e8..f095302 100644
--- a/polygerrit-ui/app/test/tests.js
+++ b/polygerrit-ui/app/test/tests.js
@@ -24,10 +24,9 @@
 // Elements tests.
 /* eslint-disable max-len */
 const elements = [
-  // This seemed to be flakey when it was farther down the list. Keep at the
+  // This seemed to be flaky when it was farther down the list. Keep at the
   // beginning.
   'gr-app_test.html',
-  'admin/gr-access-section/gr-access-section_test.html',
   'admin/gr-admin-group-list/gr-admin-group-list_test.html',
   'admin/gr-admin-view/gr-admin-view_test.html',
   'admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html',
@@ -42,7 +41,6 @@
   'admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html',
   'admin/gr-plugin-list/gr-plugin-list_test.html',
   'admin/gr-repo-access/gr-repo-access_test.html',
-  'admin/gr-repo-command/gr-repo-command_test.html',
   'admin/gr-repo-commands/gr-repo-commands_test.html',
   'admin/gr-repo-dashboards/gr-repo-dashboards_test.html',
   'admin/gr-repo-detail-list/gr-repo-detail-list_test.html',
@@ -62,7 +60,6 @@
   'change/gr-change-metadata/gr-change-metadata-it_test.html',
   'change/gr-change-metadata/gr-change-metadata_test.html',
   'change/gr-change-requirements/gr-change-requirements_test.html',
-  'change/gr-change-view/gr-change-view_test.html',
   'change/gr-comment-list/gr-comment-list_test.html',
   'change/gr-commit-info/gr-commit-info_test.html',
   'change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html',
@@ -94,7 +91,6 @@
   'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html',
   'core/gr-main-header/gr-main-header_test.html',
   'core/gr-navigation/gr-navigation_test.html',
-  'core/gr-reporting/gr-reporting_test.html',
   'core/gr-router/gr-router_test.html',
   'core/gr-search-bar/gr-search-bar_test.html',
   'core/gr-smart-search/gr-smart-search_test.html',
@@ -112,7 +108,6 @@
   'diff/gr-diff-selection/gr-diff-selection_test.html',
   'diff/gr-diff-view/gr-diff-view_test.html',
   'diff/gr-diff/gr-diff-group_test.html',
-  'diff/gr-diff/gr-diff_test.html',
   'diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html',
   'diff/gr-patch-range-select/gr-patch-range-select_test.html',
   'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
@@ -158,7 +153,6 @@
   'shared/gr-account-link/gr-account-link_test.html',
   'shared/gr-alert/gr-alert_test.html',
   'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
-  'shared/gr-autocomplete/gr-autocomplete_test.html',
   'shared/gr-avatar/gr-avatar_test.html',
   'shared/gr-button/gr-button_test.html',
   'shared/gr-change-star/gr-change-star_test.html',
@@ -167,7 +161,6 @@
   'shared/gr-comment/gr-comment_test.html',
   'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
   'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
-  'shared/gr-cursor-manager/gr-cursor-manager_test.html',
   'shared/gr-date-formatter/gr-date-formatter_test.html',
   'shared/gr-dialog/gr-dialog_test.html',
   'shared/gr-diff-preferences/gr-diff-preferences_test.html',
@@ -223,7 +216,6 @@
 // Behaviors tests.
 /* eslint-disable max-len */
 const behaviors = [
-  'async-foreach-behavior/async-foreach-behavior_test.html',
   'base-url-behavior/base-url-behavior_test.html',
   'docs-url-behavior/docs-url-behavior_test.html',
   'dom-util-behavior/dom-util-behavior_test.html',
@@ -252,7 +244,6 @@
   'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
   'gr-display-name-utils/gr-display-name-utils_test.html',
   'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
-  'util_test.html',
 ];
 /* eslint-enable max-len */
 for (let file of scripts) {
@@ -261,13 +252,19 @@
 }
 
 const services = [
+  'app-context-init_test.html',
   'flags_test.html',
+  'gr-reporting/gr-reporting_test.html',
+  'gr-reporting/gr-reporting_mock_test.html',
 ];
 for (let file of services) {
   file = servicesPath + file;
   testFiles.push(file);
 }
 
+// embed test
+testFiles.push('../embed/gr-diff-app-context-init_test.html');
+
 /**
  * Converts multiline string to a map<file_name, test_count>.
  *
diff --git a/polygerrit-ui/app/types/custom-externs.js b/polygerrit-ui/app/types/custom-externs.js
index afa094c..bc95b3f 100644
--- a/polygerrit-ui/app/types/custom-externs.js
+++ b/polygerrit-ui/app/types/custom-externs.js
@@ -58,6 +58,5 @@
 var GrRestApiHelper;
 var GrDisplayNameUtils;
 var GrReviewerSuggestionsProvider;
-var moment;
 var page;
 var util;
\ No newline at end of file
diff --git a/polygerrit-ui/app/types/types.js b/polygerrit-ui/app/types/types.js
index 5408eea..91909a6 100644
--- a/polygerrit-ui/app/types/types.js
+++ b/polygerrit-ui/app/types/types.js
@@ -309,3 +309,16 @@
  *  }}
  */
 Gerrit.Comment;
+
+/**
+ * This contains path info used in diff, basePath
+ * is used on the left while path is used on the right.
+ *
+ * TODO(taoalpha): unify all *Range into one.
+ *
+ * @typedef {{
+ *  basePath: ?string,
+ *  path: string,
+ *  }}
+ */
+Gerrit.FileRange;
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/date-util.js b/polygerrit-ui/app/utils/date-util.js
new file mode 100644
index 0000000..475fdc6
--- /dev/null
+++ b/polygerrit-ui/app/utils/date-util.js
@@ -0,0 +1,178 @@
+/**
+ * @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.
+ */
+
+const Duration = {
+  HOUR: 1000 * 60 * 60,
+  DAY: 1000 * 60 * 60 * 24,
+};
+
+export function parseDate(dateStr) {
+  // Timestamps are given in UTC and have the format
+  // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
+  // nanoseconds.
+  // Munge the date into an ISO 8061 format and parse that.
+  return new Date(dateStr.replace(' ', 'T') + 'Z');
+}
+
+export function isValidDate(date) {
+  return date instanceof Date && !isNaN(date);
+}
+
+// similar to fromNow from moment.js
+export function fromNow(date) {
+  const now = new Date();
+  const secondsAgo = Math.round((now - date) / 1000);
+  if (secondsAgo <= 44) return 'just now';
+  if (secondsAgo <= 89) return 'a minute ago';
+  const minutesAgo = Math.round(secondsAgo / 60);
+  if (minutesAgo <= 44) return `${minutesAgo} minutes ago`;
+  if (minutesAgo <= 89) return 'an hour ago';
+  const hoursAgo = Math.round(minutesAgo / 60);
+  if (hoursAgo <= 21) return `${hoursAgo} hours ago`;
+  if (hoursAgo <= 35) return 'a day ago';
+  const daysAgo = Math.round(hoursAgo / 24);
+  if (daysAgo <= 25) return `${daysAgo} days ago`;
+  if (daysAgo <= 45) return `a month ago`;
+  const monthsAgo = Math.round(daysAgo / 30);
+  if (daysAgo <= 319) return `${monthsAgo} months ago`;
+  if (daysAgo <= 547) return `a year ago`;
+  const yearsAgo = Math.round(daysAgo / 365);
+  return `${yearsAgo} years ago`;
+}
+
+/**
+ * Return true if date is within 24 hours and on the same day.
+ */
+export function isWithinDay(now, date) {
+  const diff = now - date;
+  return diff < Duration.DAY && date.getDay() == now.getDay();
+}
+
+/**
+ * Returns true if date is from one to six months.
+ */
+export function isWithinHalfYear(now, date) {
+  const diff = now - date;
+  return diff < 180 * Duration.DAY;
+}
+
+export function formatDate(date, format) {
+  const options = {};
+  if (format.includes('MM')) {
+    if (format.includes('MMM')) {
+      options.month = 'short';
+    } else {
+      options.month = '2-digit';
+    }
+  }
+  if (format.includes('YY')) {
+    if (format.includes('YYYY')) {
+      options.year = 'numeric';
+    } else {
+      options.year = '2-digit';
+    }
+  }
+
+  if (format.includes('DD')) {
+    options.day = '2-digit';
+  }
+
+  if (format.includes('HH')) {
+    options.hour = '2-digit';
+    options.hour12 = false;
+  }
+
+  if (format.includes('h')) {
+    options.hour = 'numeric';
+    options.hour12 = true;
+  }
+
+  if (format.includes('mm')) {
+    options.minute = '2-digit';
+  }
+
+  if (format.includes('ss')) {
+    options.second = '2-digit';
+  }
+  let locale = 'en-US';
+  // Workaround for Chrome 80, en-US is using h24 (midnight is 24:00),
+  // en-GB is using h23 (midnight is 00:00)
+  if (format.includes('HH')) {
+    locale = 'en-GB';
+  }
+
+  const dtf = new Intl.DateTimeFormat(locale, options);
+  const parts = dtf.formatToParts(date).filter(o => o.type != 'literal')
+      .reduce((acc, o) => {
+        acc[o.type] = o.value;
+        return acc;
+      }, {});
+  if (format.includes('YY')) {
+    if (format.includes('YYYY')) {
+      format = format.replace('YYYY', parts.year);
+    } else {
+      format = format.replace('YY', parts.year);
+    }
+  }
+
+  if (format.includes('DD')) {
+    format = format.replace('DD', parts.day);
+  }
+
+  if (format.includes('HH')) {
+    format = format.replace('HH', parts.hour);
+  }
+
+  if (format.includes('h')) {
+    format = format.replace('h', parts.hour);
+  }
+
+  if (format.includes('mm')) {
+    format = format.replace('mm', parts.minute);
+  }
+
+  if (format.includes('ss')) {
+    format = format.replace('ss', parts.second);
+  }
+
+  if (format.includes('A')) {
+    if (parts.dayperiod) {
+      // Workaround for chrome 70 and below
+      format = format.replace('A', parts.dayperiod.toUpperCase());
+    } else {
+      format = format.replace('A', parts.dayPeriod.toUpperCase());
+    }
+  }
+  if (format.includes('MM')) {
+    if (format.includes('MMM')) {
+      format = format.replace('MMM', parts.month);
+    } else {
+      format = format.replace('MM', parts.month);
+    }
+  }
+  return format;
+}
+
+export function utcOffsetString() {
+  const now = new Date();
+  const tzo = -now.getTimezoneOffset();
+  const pad = num => {
+    const norm = Math.floor(Math.abs(num));
+    return (norm < 10 ? '0' : '') + norm;
+  };
+  return ` UTC${tzo >= 0 ? '+' : '-'}${pad(tzo / 60)}:${pad(tzo%60)}`;
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/date-util_test.js b/polygerrit-ui/app/utils/date-util_test.js
new file mode 100644
index 0000000..7b22cc6
--- /dev/null
+++ b/polygerrit-ui/app/utils/date-util_test.js
@@ -0,0 +1,121 @@
+/**
+ * @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 {isValidDate, parseDate, fromNow, isWithinDay, isWithinHalfYear, formatDate} from './date-util.js';
+
+suite('date-util tests', () => {
+  suite('parseDate', () => {
+    test('parseDate server date', () => {
+      const parsed = parseDate('2015-09-15 20:34:00.000000000');
+      assert.equal('2015-09-15T20:34:00.000Z', parsed.toISOString());
+    });
+  });
+
+  suite('isValidDate', () => {
+    test('date is valid', () => {
+      assert.isTrue(isValidDate(new Date()));
+    });
+    test('broken date is invalid', () => {
+      assert.isFalse(isValidDate(new Date('xxx')));
+    });
+  });
+
+  suite('fromNow', () => {
+    test('test all variants', () => {
+      const fakeNow = new Date('May 08 2020 12:00:00');
+      sinon.useFakeTimers(fakeNow.getTime());
+      assert.equal('just now', fromNow(new Date('May 08 2020 11:59:30')));
+      assert.equal('a minute ago', fromNow(new Date('May 08 2020 11:59:00')));
+      assert.equal('5 minutes ago', fromNow(new Date('May 08 2020 11:55:00')));
+      assert.equal('an hour ago', fromNow(new Date('May 08 2020 11:00:00')));
+      assert.equal('3 hours ago', fromNow(new Date('May 08 2020 9:00:00')));
+      assert.equal('a day ago', fromNow(new Date('May 07 2020 12:00:00')));
+      assert.equal('3 days ago', fromNow(new Date('May 05 2020 12:00:00')));
+      assert.equal('a month ago', fromNow(new Date('Apr 05 2020 12:00:00')));
+      assert.equal('2 months ago', fromNow(new Date('Mar 05 2020 12:00:00')));
+      assert.equal('a year ago', fromNow(new Date('May 05 2019 12:00:00')));
+      assert.equal('10 years ago', fromNow(new Date('May 05 2010 12:00:00')));
+    });
+  });
+
+  suite('isWithinDay', () => {
+    test('basics works', () => {
+      assert.isTrue(isWithinDay(new Date('May 08 2020 12:00:00'),
+          new Date('May 08 2020 02:00:00')));
+      assert.isFalse(isWithinDay(new Date('May 08 2020 12:00:00'),
+          new Date('May 07 2020 12:00:00')));
+    });
+  });
+
+  suite('isWithinHalfYear', () => {
+    test('basics works', () => {
+      assert.isTrue(isWithinHalfYear(new Date('May 08 2020 12:00:00'),
+          new Date('Feb 08 2020 12:00:00')));
+      assert.isFalse(isWithinHalfYear(new Date('May 08 2020 12:00:00'),
+          new Date('Nov 07 2019 12:00:00')));
+    });
+  });
+
+  suite('formatDate', () => {
+    test('works for standard format', () => {
+      const stdFormat = 'MMM DD, YYYY';
+      assert.equal('May 08, 2020',
+          formatDate(new Date('May 08 2020 12:00:00'), stdFormat));
+      assert.equal('Feb 28, 2020',
+          formatDate(new Date('Feb 28 2020 12:00:00'), stdFormat));
+
+      const time24Format = 'HH:mm:ss';
+      assert.equal('Feb 28, 2020 12:01:12',
+          formatDate(new Date('Feb 28 2020 12:01:12'), stdFormat + ' '
+          + time24Format));
+    });
+    test('works for euro format', () => {
+      const euroFormat = 'DD.MM.YYYY';
+      assert.equal('01.12.2019',
+          formatDate(new Date('Dec 01 2019 12:00:00'), euroFormat));
+      assert.equal('20.01.2002',
+          formatDate(new Date('Jan 20 2002 12:00:00'), euroFormat));
+
+      const time24Format = 'HH:mm:ss';
+      assert.equal('28.02.2020 00:01:12',
+          formatDate(new Date('Feb 28 2020 00:01:12'), euroFormat + ' '
+          + time24Format));
+    });
+    test('works for iso format', () => {
+      const isoFormat = 'YYYY-MM-DD';
+      assert.equal('2015-01-01',
+          formatDate(new Date('Jan 01 2015 12:00:00'), isoFormat));
+      assert.equal('2013-07-03',
+          formatDate(new Date('Jul 03 2013 12:00:00'), isoFormat));
+
+      const timeFormat = 'h:mm:ss A';
+      assert.equal('2013-07-03 5:00:00 AM',
+          formatDate(new Date('Jul 03 2013 05:00:00'), isoFormat + ' '
+          + timeFormat));
+      assert.equal('2013-07-03 5:00:00 PM',
+          formatDate(new Date('Jul 03 2013 17:00:00'), isoFormat + ' '
+          + timeFormat));
+    });
+    test('h:mm:ss A shows correctly midnight and midday', () => {
+      const timeFormat = 'h:mm A';
+      assert.equal('12:14 PM',
+          formatDate(new Date('Jul 03 2013 12:14:00'), timeFormat));
+      assert.equal('12:15 AM',
+          formatDate(new Date('Jul 03 2013 00:15:00'), timeFormat));
+    });
+  });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/dom-util.js b/polygerrit-ui/app/utils/dom-util.js
new file mode 100644
index 0000000..a9f080f
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util.js
@@ -0,0 +1,178 @@
+/**
+ * @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.
+ */
+
+function getPathFromNode(el) {
+  if (!el.tagName || el.tagName === 'GR-APP'
+        || el instanceof DocumentFragment
+        || el instanceof HTMLSlotElement) {
+    return '';
+  }
+  let path = el.tagName.toLowerCase();
+  if (el.id) path += `#${el.id}`;
+  if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
+  return path;
+}
+
+/**
+ * Get computed style value.
+ *
+ * If ShadyCSS is provided, use ShadyCSS api.
+ * If `getComputedStyleValue` is provided on the element, use it.
+ * Otherwise fallback to native method (in polymer 2).
+ *
+ */
+export function getComputedStyleValue(name, el) {
+  let style;
+  if (window.ShadyCSS) {
+    style = ShadyCSS.getComputedStyleValue(el, name);
+  } else if (el.getComputedStyleValue) {
+    style = el.getComputedStyleValue(name);
+  } else {
+    style = getComputedStyle(el).getPropertyValue(name);
+  }
+  return style;
+}
+
+/**
+ * Query selector on a dom element.
+ *
+ * This is shadow DOM compatible, but only works when selector is within
+ * one shadow host, won't work if your selector is crossing
+ * multiple shadow hosts.
+ *
+ */
+export function querySelector(el, selector) {
+  let nodes = [el];
+  let result = null;
+  while (nodes.length) {
+    const node = nodes.pop();
+
+    // Skip if it's an invalid node.
+    if (!node || !node.querySelector) continue;
+
+    // Try find it with native querySelector directly
+    result = node.querySelector(selector);
+
+    if (result) {
+      break;
+    }
+
+    // Add all nodes with shadowRoot and loop through
+    const allShadowNodes = [...node.querySelectorAll('*')]
+        .filter(child => !!child.shadowRoot)
+        .map(child => child.shadowRoot);
+    nodes = nodes.concat(allShadowNodes);
+
+    // Add shadowRoot of current node if has one
+    // as its not included in node.querySelectorAll('*')
+    if (node.shadowRoot) {
+      nodes.push(node.shadowRoot);
+    }
+  }
+  return result;
+}
+
+/**
+ * Query selector all dom elements matching with certain selector.
+ *
+ * This is shadow DOM compatible, but only works when selector is within
+ * one shadow host, won't work if your selector is crossing
+ * multiple shadow hosts.
+ *
+ * Note: this can be very expensive, only use when have to.
+ */
+export function querySelectorAll(el, selector) {
+  let nodes = [el];
+  const results = new Set();
+  while (nodes.length) {
+    const node = nodes.pop();
+
+    if (!node || !node.querySelectorAll) continue;
+
+    // Try find all from regular children
+    [...node.querySelectorAll(selector)]
+        .forEach(el => results.add(el));
+
+    // Add all nodes with shadowRoot and loop through
+    const allShadowNodes = [...node.querySelectorAll('*')]
+        .filter(child => !!child.shadowRoot)
+        .map(child => child.shadowRoot);
+    nodes = nodes.concat(allShadowNodes);
+
+    // Add shadowRoot of current node if has one
+    // as its not included in node.querySelectorAll('*')
+    if (node.shadowRoot) {
+      nodes.push(node.shadowRoot);
+    }
+  }
+  return [...results];
+}
+
+/**
+ * Retrieves the dom path of the current event.
+ *
+ * If the event object contains a `path` property, then use it,
+ * otherwise, construct the dom path based on the event target.
+ *
+ * @param {!Event} e
+ * @return {string}
+ * @example
+ *
+ * domNode.onclick = e => {
+ *  getEventPath(e); // eg: div.class1>p#pid.class2
+ * }
+ */
+export function getEventPath(e) {
+  if (!e) return '';
+
+  let path = e.path;
+  if (!path || !path.length) {
+    path = [];
+    let el = e.target;
+    while (el) {
+      path.push(el);
+      el = el.parentNode || el.host;
+    }
+  }
+
+  return path.reduce((domPath, curEl) => {
+    const pathForEl = getPathFromNode(curEl);
+    if (!pathForEl) return domPath;
+    return domPath ? `${pathForEl}>${domPath}` : pathForEl;
+  }, '');
+}
+
+/**
+ * Are any ancestors of the element (or the element itself) members of the
+ * given class.
+ *
+ * @param {!Element} element
+ * @param {string} className
+ * @param {Element=} opt_stopElement If provided, stop traversing the
+ *     ancestry when the stop element is reached. The stop element's class
+ *     is not checked.
+ * @return {boolean}
+ */
+export function descendedFromClass(element, className, opt_stopElement) {
+  let isDescendant = element.classList.contains(className);
+  while (!isDescendant && element.parentElement &&
+        (!opt_stopElement || element.parentElement !== opt_stopElement)) {
+    isDescendant = element.classList.contains(className);
+    element = element.parentElement;
+  }
+  return isDescendant;
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/dom-util_test.js b/polygerrit-ui/app/utils/dom-util_test.js
new file mode 100644
index 0000000..c317578
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util_test.js
@@ -0,0 +1,139 @@
+/**
+ * @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 {getComputedStyleValue, querySelector, querySelectorAll, descendedFromClass, getEventPath} from './dom-util.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+class TestEle extends PolymerElement {
+  static get is() {
+    return 'dom-util-test-element';
+  }
+
+  static get template() {
+    return html`
+    <div>
+      <div class="a">
+        <div class="b">
+          <div class="c"></div>
+        </div>
+        <span class="ss"></span>
+      </div>
+      <span class="ss"></span>
+    </div>
+    `;
+  }
+}
+
+customElements.define(TestEle.is, TestEle);
+
+const basicFixture = fixtureFromTemplate(html`
+  <div id="test" class="a b c">
+    <a class="testBtn" style="color:red;"></a>
+    <dom-util-test-element></dom-util-test-element>
+    <span class="ss"></span>
+  </div>
+`);
+
+suite('dom-util tests', () => {
+  suite('getEventPath', () => {
+    test('empty event', () => {
+      assert.equal(getEventPath(), '');
+      assert.equal(getEventPath(null), '');
+      assert.equal(getEventPath(undefined), '');
+      assert.equal(getEventPath({}), '');
+    });
+
+    test('event with fake path', () => {
+      assert.equal(getEventPath({path: []}), '');
+      assert.equal(getEventPath({path: [
+        {tagName: 'dd'},
+      ]}), 'dd');
+    });
+
+    test('event with fake complicated path', () => {
+      assert.equal(getEventPath({path: [
+        {tagName: 'dd', id: 'test', className: 'a b'},
+        {tagName: 'DIV', id: 'test2', className: 'a b c'},
+      ]}), 'div#test2.a.b.c>dd#test.a.b');
+    });
+
+    test('event with fake target', () => {
+      const fakeTargetParent2 = {
+        tagName: 'DIV', id: 'test2', className: 'a b c',
+      };
+      const fakeTargetParent1 = {
+        parentNode: fakeTargetParent2,
+        tagName: 'dd',
+        id: 'test',
+        className: 'a b',
+      };
+      const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
+      assert.equal(
+          getEventPath({target: fakeTarget}),
+          'div#test2.a.b.c>dd#test.a.b>span'
+      );
+    });
+
+    test('event with real click', () => {
+      const element = basicFixture.instantiate();
+      const aLink = element.querySelector('a');
+      let path;
+      aLink.onclick = e => path = getEventPath(e);
+      MockInteractions.click(aLink);
+      assert.equal(
+          path,
+          `html>body>test-fixture#${basicFixture.fixtureId}>` +
+          'div#test.a.b.c>a.testBtn'
+      );
+    });
+  });
+
+  suite('querySelector and querySelectorAll', () => {
+    test('query cross shadow dom', () => {
+      const element = basicFixture.instantiate();
+      const theFirstEl = querySelector(element, '.ss');
+      const allEls = querySelectorAll(element, '.ss');
+      assert.equal(allEls.length, 3);
+      assert.equal(theFirstEl, allEls[0]);
+    });
+  });
+
+  suite('getComputedStyleValue', () => {
+    test('color style', () => {
+      const element = basicFixture.instantiate();
+      const testBtn = querySelector(element, '.testBtn');
+      assert.equal(
+          getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)'
+      );
+    });
+  });
+
+  suite('descendedFromClass', () => {
+    test('basic tests', () => {
+      const element = basicFixture.instantiate();
+      const testEl = querySelector(element, 'dom-util-test-element');
+      // .c is a child of .a and not vice versa.
+      assert.isTrue(descendedFromClass(querySelector(testEl, '.c'), 'a'));
+      assert.isFalse(descendedFromClass(querySelector(testEl, '.a'), 'c'));
+
+      // Stops at stop element.
+      assert.isFalse(descendedFromClass(querySelector(testEl, '.c'), 'a',
+          querySelector(testEl, '.b')));
+    });
+  });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
index 42b98ab..829a507 100755
--- a/polygerrit-ui/app/wct_test.sh
+++ b/polygerrit-ui/app/wct_test.sh
@@ -14,7 +14,7 @@
 
 # Copy ui_npm, so it will override ui_dev_npm modules (in case of conflicts)
 # Because browser always requests specific exact files (i.e. not a directory),
-# it always receives file from ui_npm. It can broke WCT itself but luckely it works.
+# it always receives file from ui_npm. It can broke WCT itself but luckily it works.
 cp -R -L ./external/ui_npm/node_modules/* $t/node_modules
 
 cp -R -L ./polygerrit-ui/app/* $t/
@@ -25,13 +25,13 @@
 echo "export const config=$JSON_CONFIG;" > ./test/suite_conf.js
 echo "export const testsPerFileString=\`" >> ./test/suite_conf.js
 # Count number of tests in each file.
-# We don't need accurate data, use simpliest method
+# We don't need accurate data, use simplest method
 # TODO(dmfilippov): collect data only once
 # In the current implementation, the same data is collected for each split,
 # It takes less than a second which many times less than the overall wct test time
 grep -rnw '.' --include=\*_test.html -e "test(" -c >> ./test/suite_conf.js
 echo "\`;" >>./test/suite_conf.js
 
-# If wct doesn't receive any paramenters, it fails (can't find files)
+# If wct doesn't receive any parameters, it fails (can't find files)
 # Pass --config-file as a parameter to have some arguments in command line
 $root_dir/$1 --config-file wct.conf.js ${WCT_ARGS}
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 5724ffa..8fb3eea 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -328,21 +328,11 @@
 "ba-linkify@file:../../lib/ba-linkify/src":
   version "1.0.0"
 
-es6-promise@^3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
-  integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=
-
 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=
 
-moment@^2.24.0:
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
-  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
-
 page@^1.11.5:
   version "1.11.5"
   resolved "https://registry.yarnpkg.com/page/-/page-1.11.5.tgz#0cfc8608be337f26f4377f31df0787aef0ca1af7"
@@ -367,8 +357,3 @@
   dependencies:
     "@polymer/polymer" "^3.0.2"
     "@webcomponents/webcomponentsjs" "^2.0.3"
-
-whatwg-fetch@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
-  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
diff --git a/polygerrit-ui/karma.conf.js b/polygerrit-ui/karma.conf.js
new file mode 100644
index 0000000..744c234
--- /dev/null
+++ b/polygerrit-ui/karma.conf.js
@@ -0,0 +1,152 @@
+/**
+ * @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.
+ */
+
+const runUnderBazel = !!process.env["RUNFILES_DIR"];
+const path = require('path');
+
+function getModulesDir() {
+  if(runUnderBazel) {
+    // Run under bazel
+    return [
+      `external/ui_npm/node_modules`,
+      `external/ui_dev_npm/node_modules`
+    ];
+  }
+
+  // Run from intellij or npm run test:kdebug
+  return [
+    path.join(__dirname, 'app/node_modules'),
+    path.join(__dirname, 'node_modules'),
+  ];
+}
+
+function getUiDevNpmFilePath(importPath) {
+  if(runUnderBazel) {
+    return `external/ui_dev_npm/node_modules/${importPath}`;
+  }
+  else {
+    return `polygerrit-ui/node_modules/${importPath}`
+  }
+}
+
+module.exports = function(config) {
+  const testFilesLocationPattern =
+      'polygerrit-ui/app/**/!(template_test_srcs)/';
+  // 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"
+  // script)
+  const testFilesPattern = (typeof config.testFiles == 'string') ?
+      testFilesLocationPattern + config.testFiles :
+      testFilesLocationPattern + '*_test.js';
+  config.set({
+    // base path that will be used to resolve all patterns (eg. files, exclude)
+    basePath: '../',
+    plugins: [
+      // Do not use karma-* to load all installed plugin
+      // This can lead to unexpected behavior under bazel
+      // if you forget to add a plugin in a bazel rule.
+      require.resolve('@open-wc/karma-esm'),
+      'karma-mocha',
+      'karma-chrome-launcher',
+      'karma-mocha-reporter',
+    ],
+    // frameworks to use
+    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+    frameworks: ['mocha', 'esm'],
+
+    // list of files / patterns to load in the browser
+    files: [
+      getUiDevNpmFilePath('accessibility-developer-tools/dist/js/axs_testing.js'),
+      getUiDevNpmFilePath('sinon/pkg/sinon.js'),
+      { pattern: testFilesPattern, type: 'module' },
+    ],
+    esm: {
+      nodeResolve: true,
+      moduleDirs: getModulesDir(),
+      // Bazel and yarn uses symlinks for files.
+      // preserveSymlinks is necessary for correct modules paths resolving
+      preserveSymlinks: true,
+      // By default, esm-dev-server uses 'auto' compatibility mode.
+      // In the 'auto' mode it incorrectly applies polyfills and
+      // breaks tests in some browser versions
+      // (for example, Chrome 69 on gerrit-ci).
+      compatibility: 'none',
+    },
+    // test results reporter to use
+    // possible values: 'dots', 'progress'
+    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+    reporters: ['mocha'],
+
+
+    // web server port
+    port: 9876,
+
+
+    // enable / disable colors in the output (reporters and logs)
+    colors: true,
+
+
+    // level of logging
+    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: false,
+
+
+    // start these browsers
+    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+    browsers: ["CustomChromeHeadless"],
+    browserForDebugging: "CustomChromeHeadlessWithDebugPort",
+
+
+    // Continuous Integration mode
+    // if true, Karma captures browsers, runs the tests and exits
+    singleRun: true,
+
+    // Concurrency level
+    // how many browser should be started simultaneous
+    concurrency: Infinity,
+
+    client: {
+      mocha: {
+        ui: 'tdd',
+        timeout: 5000,
+      }
+    },
+
+    customLaunchers: {
+      // Based on https://developers.google.com/web/updates/2017/06/headless-karma-mocha-chai
+      "CustomChromeHeadless": {
+        base: 'ChromeHeadless',
+        flags: ['--disable-translate', '--disable-extensions'],
+      },
+      "ChromeDev": {
+        base: 'Chrome',
+        flags: ['--disable-extensions', ' --auto-open-devtools-for-tabs'],
+      },
+      "CustomChromeHeadlessWithDebugPort": {
+        base: 'CustomChromeHeadless',
+        flags: ['--remote-debugging-port=9222'],
+      }
+    }
+  });
+};
diff --git a/polygerrit-ui/karma_test.sh b/polygerrit-ui/karma_test.sh
new file mode 100755
index 0000000..5fab442
--- /dev/null
+++ b/polygerrit-ui/karma_test.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+set -euo pipefail
+./$1 start $2 --single-run
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 3d35e3e..527763b 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -4,12 +4,23 @@
   "browser": true,
   "dependencies": {},
   "devDependencies": {
+    "@open-wc/karma-esm": "^2.13.21",
     "@polymer/iron-test-helpers": "^3.0.1",
+    "@polymer/test-fixture": "^4.0.2",
+    "accessibility-developer-tools": "^2.12.0",
     "chai": "^4.2.0",
-    "mocha": "^6.2.2",
+    "karma": "^4.4.1",
+    "karma-chrome-launcher": "^3.1.0",
+    "karma-mocha": "^2.0.1",
+    "karma-mocha-reporter": "^2.2.5",
+    "lodash": "^4.17.15",
+    "mocha": "^7.1.1",
     "wct-browser-legacy": "^1.0.2",
     "web-component-tester": "^6.9.2"
   },
+  "scripts": {
+    "postinstall": "selenium-standalone install"
+  },
   "license": "Apache-2.0",
   "private": true
 }
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 120aff5..c44493d 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -61,6 +61,7 @@
 
 	dirListingMux := http.NewServeMux()
 	dirListingMux.Handle("/styles/", http.StripPrefix("/styles/", http.FileServer(http.Dir("app/styles"))))
+	dirListingMux.Handle("/samples/", http.StripPrefix("/samples/", http.FileServer(http.Dir("app/samples"))))
 	dirListingMux.Handle("/elements/", http.StripPrefix("/elements/", http.FileServer(http.Dir("app/elements"))))
 	dirListingMux.Handle("/behaviors/", http.StripPrefix("/behaviors/", http.FileServer(http.Dir("app/behaviors"))))
 
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 12d39aa..5ad28b6 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -9,6 +9,15 @@
   dependencies:
     "@babel/highlight" "^7.8.3"
 
+"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c"
+  integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==
+  dependencies:
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    semver "^5.5.0"
+
 "@babel/core@^7.0.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941"
@@ -30,6 +39,28 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
+  integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helpers" "^7.9.0"
+    "@babel/parser" "^7.9.0"
+    "@babel/template" "^7.8.6"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    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"
@@ -40,6 +71,16 @@
     lodash "^4.17.13"
     source-map "^0.5.0"
 
+"@babel/generator@^7.4.0", "@babel/generator@^7.9.0":
+  version "7.9.4"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce"
+  integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==
+  dependencies:
+    "@babel/types" "^7.9.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"
@@ -64,6 +105,17 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helper-compilation-targets@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde"
+  integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==
+  dependencies:
+    "@babel/compat-data" "^7.8.6"
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    levenary "^1.1.1"
+    semver "^5.5.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"
@@ -72,6 +124,15 @@
     "@babel/helper-regex" "^7.8.3"
     regexpu-core "^4.6.0"
 
+"@babel/helper-create-regexp-features-plugin@^7.8.8":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087"
+  integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-regex" "^7.8.3"
+    regexpu-core "^4.7.0"
+
 "@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"
@@ -138,6 +199,19 @@
     "@babel/types" "^7.8.3"
     lodash "^4.17.13"
 
+"@babel/helper-module-transforms@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
+  integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-simple-access" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/template" "^7.8.6"
+    "@babel/types" "^7.9.0"
+    lodash "^4.17.13"
+
 "@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"
@@ -145,7 +219,7 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
-"@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+"@babel/helper-plugin-utils@^7.0.0", "@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==
@@ -178,6 +252,16 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helper-replace-supers@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
+  integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/traverse" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
 "@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"
@@ -193,6 +277,11 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
+"@babel/helper-validator-identifier@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed"
+  integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==
+
 "@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"
@@ -212,6 +301,15 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helpers@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
+  integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+
 "@babel/highlight@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797"
@@ -221,6 +319,11 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
+"@babel/parser@^7.4.3", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+  version "7.9.4"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
+  integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
+
 "@babel/parser@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081"
@@ -233,7 +336,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-proposal-async-generator-functions@^7.0.0":
+"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3":
   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==
@@ -242,6 +345,38 @@
     "@babel/helper-remap-async-to-generator" "^7.8.3"
     "@babel/plugin-syntax-async-generators" "^7.8.0"
 
+"@babel/plugin-proposal-dynamic-import@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054"
+  integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+
+"@babel/plugin-proposal-json-strings@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b"
+  integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2"
+  integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8"
+  integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+
 "@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"
@@ -250,6 +385,38 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
 
+"@babel/plugin-proposal-object-rest-spread@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f"
+  integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9"
+  integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58"
+  integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d"
+  integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.8"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.8.0":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -257,20 +424,48 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-dynamic-import@^7.0.0":
+"@babel/plugin-syntax-class-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7"
+  integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
   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":
+"@babel/plugin-syntax-import-meta@^7.0.0", "@babel/plugin-syntax-import-meta@^7.8.3":
   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==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-syntax-json-strings@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f"
+  integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
   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"
@@ -278,14 +473,35 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-transform-arrow-functions@^7.0.0":
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391"
+  integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.8.3":
   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==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-async-to-generator@^7.0.0":
+"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.8.3":
   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==
@@ -294,14 +510,14 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-remap-async-to-generator" "^7.8.3"
 
-"@babel/plugin-transform-block-scoped-functions@^7.0.0":
+"@babel/plugin-transform-block-scoped-functions@^7.0.0", "@babel/plugin-transform-block-scoped-functions@^7.8.3":
   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==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-block-scoping@^7.0.0":
+"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.8.3":
   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==
@@ -323,7 +539,21 @@
     "@babel/helper-split-export-declaration" "^7.8.3"
     globals "^11.1.0"
 
-"@babel/plugin-transform-computed-properties@^7.0.0":
+"@babel/plugin-transform-classes@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d"
+  integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ==
+  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.6"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.8.3":
   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==
@@ -337,14 +567,29 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-duplicate-keys@^7.0.0":
+"@babel/plugin-transform-destructuring@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b"
+  integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e"
+  integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-duplicate-keys@^7.0.0", "@babel/plugin-transform-duplicate-keys@^7.8.3":
   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==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-exponentiation-operator@^7.0.0":
+"@babel/plugin-transform-exponentiation-operator@^7.0.0", "@babel/plugin-transform-exponentiation-operator@^7.8.3":
   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==
@@ -359,7 +604,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-function-name@^7.0.0":
+"@babel/plugin-transform-for-of@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e"
+  integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.8.3":
   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==
@@ -374,13 +626,20 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-literals@^7.0.0":
+"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.8.3":
   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==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-transform-member-expression-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410"
+  integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@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"
@@ -390,7 +649,58 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     babel-plugin-dynamic-import-node "^2.3.0"
 
-"@babel/plugin-transform-object-super@^7.0.0":
+"@babel/plugin-transform-modules-amd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4"
+  integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940"
+  integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-simple-access" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90"
+  integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==
+  dependencies:
+    "@babel/helper-hoist-variables" "^7.8.3"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-umd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697"
+  integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c"
+  integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+
+"@babel/plugin-transform-new-target@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43"
+  integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.8.3":
   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==
@@ -407,6 +717,21 @@
     "@babel/helper-get-function-arity" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-transform-parameters@^7.8.7":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a"
+  integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-property-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263"
+  integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@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"
@@ -414,21 +739,35 @@
   dependencies:
     regenerator-transform "^0.14.0"
 
-"@babel/plugin-transform-shorthand-properties@^7.0.0":
+"@babel/plugin-transform-regenerator@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8"
+  integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==
+  dependencies:
+    regenerator-transform "^0.14.2"
+
+"@babel/plugin-transform-reserved-words@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5"
+  integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.8.3":
   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==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-spread@^7.0.0":
+"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8"
   integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-sticky-regex@^7.0.0":
+"@babel/plugin-transform-sticky-regex@^7.0.0", "@babel/plugin-transform-sticky-regex@^7.8.3":
   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==
@@ -436,7 +775,7 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-regex" "^7.8.3"
 
-"@babel/plugin-transform-template-literals@^7.0.0":
+"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.8.3":
   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==
@@ -451,7 +790,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-unicode-regex@^7.0.0":
+"@babel/plugin-transform-typeof-symbol@^7.8.4":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412"
+  integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.8.3":
   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==
@@ -459,6 +805,99 @@
     "@babel/helper-create-regexp-features-plugin" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/preset-env@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
+  integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==
+  dependencies:
+    "@babel/compat-data" "^7.9.0"
+    "@babel/helper-compilation-targets" "^7.8.7"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-proposal-async-generator-functions" "^7.8.3"
+    "@babel/plugin-proposal-dynamic-import" "^7.8.3"
+    "@babel/plugin-proposal-json-strings" "^7.8.3"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-proposal-numeric-separator" "^7.8.3"
+    "@babel/plugin-proposal-object-rest-spread" "^7.9.0"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-proposal-optional-chaining" "^7.9.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.8.3"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.8.3"
+    "@babel/plugin-transform-arrow-functions" "^7.8.3"
+    "@babel/plugin-transform-async-to-generator" "^7.8.3"
+    "@babel/plugin-transform-block-scoped-functions" "^7.8.3"
+    "@babel/plugin-transform-block-scoping" "^7.8.3"
+    "@babel/plugin-transform-classes" "^7.9.0"
+    "@babel/plugin-transform-computed-properties" "^7.8.3"
+    "@babel/plugin-transform-destructuring" "^7.8.3"
+    "@babel/plugin-transform-dotall-regex" "^7.8.3"
+    "@babel/plugin-transform-duplicate-keys" "^7.8.3"
+    "@babel/plugin-transform-exponentiation-operator" "^7.8.3"
+    "@babel/plugin-transform-for-of" "^7.9.0"
+    "@babel/plugin-transform-function-name" "^7.8.3"
+    "@babel/plugin-transform-literals" "^7.8.3"
+    "@babel/plugin-transform-member-expression-literals" "^7.8.3"
+    "@babel/plugin-transform-modules-amd" "^7.9.0"
+    "@babel/plugin-transform-modules-commonjs" "^7.9.0"
+    "@babel/plugin-transform-modules-systemjs" "^7.9.0"
+    "@babel/plugin-transform-modules-umd" "^7.9.0"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3"
+    "@babel/plugin-transform-new-target" "^7.8.3"
+    "@babel/plugin-transform-object-super" "^7.8.3"
+    "@babel/plugin-transform-parameters" "^7.8.7"
+    "@babel/plugin-transform-property-literals" "^7.8.3"
+    "@babel/plugin-transform-regenerator" "^7.8.7"
+    "@babel/plugin-transform-reserved-words" "^7.8.3"
+    "@babel/plugin-transform-shorthand-properties" "^7.8.3"
+    "@babel/plugin-transform-spread" "^7.8.3"
+    "@babel/plugin-transform-sticky-regex" "^7.8.3"
+    "@babel/plugin-transform-template-literals" "^7.8.3"
+    "@babel/plugin-transform-typeof-symbol" "^7.8.4"
+    "@babel/plugin-transform-unicode-regex" "^7.8.3"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.9.0"
+    browserslist "^4.9.1"
+    core-js-compat "^3.6.2"
+    invariant "^2.2.2"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/preset-modules@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
+  integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
+
+"@babel/runtime@^7.8.4":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
+  integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/template@^7.4.0", "@babel/template@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
+  integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/parser" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
 "@babel/template@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
@@ -483,6 +922,21 @@
     globals "^11.1.0"
     lodash "^4.17.13"
 
+"@babel/traverse@^7.4.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892"
+  integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/parser" "^7.9.0"
+    "@babel/types" "^7.9.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"
@@ -492,6 +946,62 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5"
+  integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
+"@open-wc/building-utils@^2.16.1":
+  version "2.16.1"
+  resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.16.1.tgz#093d74881b996fe9497d628cdf55b6757422d894"
+  integrity sha512-0nUktFyelvSbCc8+T4w4PCyIy3i8blOFS0/EiG5xbVJ0HejDPQLTSmRBpZkn6X57tHhwUfjIdv0EAQuo2sbHEw==
+  dependencies:
+    "@babel/core" "^7.9.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+    "@webcomponents/shadycss" "^1.9.4"
+    "@webcomponents/webcomponentsjs" "^2.4.0"
+    arrify "^2.0.1"
+    browserslist "^4.9.1"
+    chokidar "^3.0.0"
+    clean-css "^4.2.1"
+    clone "^2.1.2"
+    core-js-bundle "^3.6.0"
+    deepmerge "^3.2.0"
+    es-module-shims "^0.4.6"
+    html-minifier "^4.0.0"
+    lru-cache "^5.1.1"
+    minimatch "^3.0.4"
+    parse5 "^5.1.1"
+    path-is-inside "^1.0.2"
+    regenerator-runtime "^0.13.3"
+    resolve "^1.11.1"
+    rimraf "^3.0.0"
+    shady-css-scoped-element "^0.0.2"
+    systemjs "^4.0.0"
+    terser "^4.6.4"
+    valid-url "^1.0.9"
+    whatwg-fetch "^3.0.0"
+    whatwg-url "^7.0.0"
+
+"@open-wc/karma-esm@^2.13.21":
+  version "2.13.21"
+  resolved "https://registry.yarnpkg.com/@open-wc/karma-esm/-/karma-esm-2.13.21.tgz#bef38b4e153b5728a6934de8a926d8bd9b9bb4db"
+  integrity sha512-qJREvj5HbYpUb6IeQXXiylPtqSnknUhBeK3PmhlnVdsXCeuPucmKJHbInd8ThYjX5/UJSp/cWe/Dt4H8GqHPHw==
+  dependencies:
+    "@open-wc/building-utils" "^2.16.1"
+    babel-plugin-istanbul "^5.1.4"
+    chokidar "^3.0.0"
+    deepmerge "^3.2.0"
+    es-dev-server "^1.46.0"
+    minimatch "^3.0.4"
+    node-fetch "^2.6.0"
+    portfinder "^1.0.21"
+    request "^2.88.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"
@@ -526,6 +1036,29 @@
   resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-3.0.0-pre.21.tgz#85152207cb0bf57caebc191c80bb0fdb6952614e"
   integrity sha512-IxzUe6YzaORzUksafHAXHprV29YncOJgr0+1zNAifl0/f+cb5iAd4IWUrnsnVFHG5UGTLjvis5RgV6vvIZPDrA==
 
+"@polymer/test-fixture@^4.0.2":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-4.0.2.tgz#2f4777ecdcfb22ee000db35a05e0edf27c722c19"
+  integrity sha512-tLX8tFE4mkc4p84YG5239G0hbgTVv2irZYrSyO0OblUqIRbRoCPmbydm3HRFQkJeAB3rPCtyeZ2roJULsmTG3A==
+
+"@rollup/plugin-node-resolve@^6.1.0":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-6.1.0.tgz#0d2909f4bf606ae34d43a9bc8be06a9b0c850cf0"
+  integrity sha512-Cv7PDIvxdE40SWilY5WgZpqfIUEaDxFxs89zCAHjqyRwlTSuql4M5hjIuc5QYJkOH0/vyiyNXKD72O+LhRipGA==
+  dependencies:
+    "@rollup/pluginutils" "^3.0.0"
+    "@types/resolve" "0.0.8"
+    builtin-modules "^3.1.0"
+    is-module "^1.0.0"
+    resolve "^1.11.1"
+
+"@rollup/pluginutils@^3.0.0":
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.0.8.tgz#4e94d128d94b90699e517ef045422960d18c8fde"
+  integrity sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==
+  dependencies:
+    estree-walker "^1.0.1"
+
 "@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"
@@ -719,7 +1252,7 @@
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
   integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
 
-"@types/minimatch@*", "@types/minimatch@^3.0.1":
+"@types/minimatch@*", "@types/minimatch@^3.0.1", "@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==
@@ -799,6 +1332,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/resolve@0.0.8":
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
+  integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==
+  dependencies:
+    "@types/node" "*"
+
 "@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"
@@ -867,6 +1407,11 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.4.tgz#4f9d8ea1526bab084c60b53d4854dc39fdb2bb48"
   integrity sha512-tgNcVEaKssyeZPbUBjVQf4aryO5Fi7fxRvOxV982ZJuRVDcefmIblBh0SXAbcvAAlQ2zpNEP4SuQUnr8uApIpw==
 
+"@webcomponents/shadycss@^1.9.4":
+  version "1.9.6"
+  resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.6.tgz#a8c5db867e49200a05cf8d5008029c09b7861979"
+  integrity sha512-5fFjvP0jQJZoXK6YzYeYcIDGJ5oEsdjr1L9VaYLw5yxNd4aRz4srMpwCwldeNG0A6Hvr9igbG7fCsBeiiCXd7A==
+
 "@webcomponents/webcomponentsjs@^1.0.7":
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
@@ -877,7 +1422,17 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.1.tgz#7baadec56ed2fd79b94ddfd509132d8c0c295c5c"
   integrity sha512-7jxBb+KoWncKb/JGFyTY40PjV4yRx2zd35ZLuvRP+6WndJDL7X32ZIZ7bN3sSQIl+NzJkCo7chfXJyzn+6WZaQ==
 
-accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
+"@webcomponents/webcomponentsjs@^2.4.0":
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.3.tgz#384f4f6d54563ba465fb4df21fe89e78a76fc530"
+  integrity sha512-cV4+sAmshf8ysU2USutrSRYQkJzEYKHsRCGa0CkMElGpG5747VHtkfsW3NdVIBV/m2MDKXTDydT4lkrysH7IFA==
+
+abortcontroller-polyfill@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4"
+  integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA==
+
+accepts@^1.3.5, 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==
@@ -983,11 +1538,19 @@
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
   integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
 
-any-promise@^1.0.0:
+any-promise@^1.0.0, any-promise@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
   integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
 
+anymatch@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 append-field@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
@@ -1063,6 +1626,11 @@
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
   integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
 
+array-back@^4.0.0, array-back@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.1.tgz#9b80312935a52062e1a233a9c7abeb5481b30e90"
+  integrity sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==
+
 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"
@@ -1088,6 +1656,11 @@
   resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
   integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
 
+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"
@@ -1224,6 +1797,16 @@
   dependencies:
     object.assign "^4.1.0"
 
+babel-plugin-istanbul@^5.1.4:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
+  integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    find-up "^3.0.0"
+    istanbul-lib-instrument "^3.3.0"
+    test-exclude "^5.2.3"
+
 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"
@@ -1456,6 +2039,11 @@
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
 
+base64id@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
+  integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=
+
 base64id@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
@@ -1488,6 +2076,11 @@
   dependencies:
     callsite "1.0.0"
 
+binary-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
 bl@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493"
@@ -1508,7 +2101,12 @@
   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:
+bluebird@^3.3.0:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+body-parser@1.19.0, body-parser@^1.16.1, 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==
@@ -1581,6 +2179,13 @@
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  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"
@@ -1599,6 +2204,25 @@
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
   integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
 
+browserslist-useragent@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.0.2.tgz#f0e209b2742baa5de0e451b52e678e8b4402617c"
+  integrity sha512-/UPzK9xZnk5mwwWx4wcuBKAKx/mD3MNY8sUuZ2NPqnr4RVFWZogX+8mOP0cQEYo8j78sHk0hiDNaVXZ1U3hM9A==
+  dependencies:
+    browserslist "^4.6.6"
+    semver "^6.3.0"
+    useragent "^2.3.0"
+
+browserslist@^4.0.0, browserslist@^4.6.6, browserslist@^4.8.3, browserslist@^4.9.1:
+  version "4.11.1"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.1.tgz#92f855ee88d6e050e7e7311d987992014f1a1f1b"
+  integrity sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g==
+  dependencies:
+    caniuse-lite "^1.0.30001038"
+    electron-to-chromium "^1.3.390"
+    node-releases "^1.1.53"
+    pkg-up "^2.0.0"
+
 browserstack@^1.2.0:
   version "1.5.3"
   resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.3.tgz#93ab48799a12ef99dbd074dd595410ddb196a7ac"
@@ -1606,11 +2230,29 @@
   dependencies:
     https-proxy-agent "^2.2.1"
 
+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.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -1624,6 +2266,11 @@
     base64-js "^1.0.2"
     ieee754 "^1.1.4"
 
+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==
+
 busboy@^0.2.11:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
@@ -1637,7 +2284,7 @@
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
   integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
 
-bytes@3.1.0:
+bytes@3.1.0, bytes@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
@@ -1657,12 +2304,20 @@
     union-value "^1.0.0"
     unset-value "^1.0.0"
 
+cache-content-type@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
+  integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==
+  dependencies:
+    mime-types "^2.1.18"
+    ylru "^1.2.0"
+
 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:
+camel-case@3.0.x, camel-case@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
   integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
@@ -1688,7 +2343,7 @@
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
   integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
 
-camelcase@^5.0.0:
+camelcase@^5.0.0, camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -1700,6 +2355,21 @@
   dependencies:
     "@types/node" "^4.0.30"
 
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001033, caniuse-lite@^1.0.30001038:
+  version "1.0.30001038"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001038.tgz#44da3cbca2ab6cb6aa83d1be5d324e17f141caff"
+  integrity sha512-zii9quPo96XfOiRD4TrfYGs+QsGZpb2cGiMAzPjtf/hpFgB6zCPZgJb7I1+EATeMw/o+lG8FyRAnI+CWStHcaQ==
+
 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"
@@ -1742,7 +2412,7 @@
     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.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -1770,6 +2440,36 @@
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
   integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
 
+chokidar@3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6"
+  integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.2.0"
+  optionalDependencies:
+    fsevents "~2.1.1"
+
+chokidar@^3.0.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
+  integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.3.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
 ci-info@^1.5.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
@@ -1792,6 +2492,13 @@
   dependencies:
     source-map "~0.6.0"
 
+clean-css@^4.2.1:
+  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"
@@ -1821,11 +2528,16 @@
   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.0.0, clone@^2.1.0, clone@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
   integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
 
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -1872,7 +2584,7 @@
   resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96"
   integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
 
-colors@^1.2.1:
+colors@^1.1.0, 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==
@@ -1912,6 +2624,16 @@
     table-layout "^0.4.3"
     typical "^2.6.1"
 
+command-line-usage@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.0.tgz#f28376a3da3361ff3d36cfd31c3c22c9a64c7cb6"
+  integrity sha512-Ew1clU4pkUeo6AFVDFxCbnN7GIZfXl48HIOQeFQnkO3oOqvpI7wdqtLRwv9iOCZ/7A+z4csVZeiDdEcj8g6Wiw==
+  dependencies:
+    array-back "^4.0.0"
+    chalk "^2.4.2"
+    table-layout "^1.0.0"
+    typical "^5.2.0"
+
 commander@2.17.x:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
@@ -1924,7 +2646,7 @@
   dependencies:
     graceful-readlink ">= 1.0.0"
 
-commander@^2.19.0:
+commander@^2.19.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==
@@ -1964,7 +2686,7 @@
     normalize-path "^3.0.0"
     readable-stream "^2.3.6"
 
-compressible@~2.0.16:
+compressible@^2.0.0, compressible@~2.0.16:
   version "2.0.18"
   resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
   integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
@@ -2011,14 +2733,24 @@
     write-file-atomic "^2.0.0"
     xdg-basedir "^3.0.0"
 
-content-disposition@0.5.3:
+connect@^3.6.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
+  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
+  dependencies:
+    debug "2.6.9"
+    finalhandler "1.1.2"
+    parseurl "~1.3.3"
+    utils-merge "1.0.1"
+
+content-disposition@0.5.3, content-disposition@~0.5.2:
   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:
+content-type@^1.0.2, content-type@^1.0.4, 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==
@@ -2045,11 +2777,32 @@
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
   integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
 
+cookies@~0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
+  integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+  dependencies:
+    depd "~2.0.0"
+    keygrip "~1.1.0"
+
 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-bundle@^3.6.0:
+  version "3.6.4"
+  resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.6.4.tgz#d4e098323c035f4a1b61f00db0b8def04c243920"
+  integrity sha512-qDHS3GbIEs5dZaBiCVhhtCoF79KU/ek0w+H7zfJf9RuGN0GiKfxHZfAtDy4zFtQ6X00t7Wvvr3wHzMj+/IgbPg==
+
+core-js-compat@^3.6.2:
+  version "3.6.4"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17"
+  integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==
+  dependencies:
+    browserslist "^4.8.3"
+    semver "7.0.0"
+
 core-js@^2.4.0:
   version "2.6.11"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
@@ -2143,6 +2896,11 @@
   dependencies:
     array-find-index "^1.0.1"
 
+custom-event@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+  integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
+
 dashdash@^1.12.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2150,6 +2908,16 @@
   dependencies:
     assert-plus "^1.0.0"
 
+date-format@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"
+  integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==
+
+debounce@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
+  integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==
+
 debug@2.6.8:
   version "2.6.8"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
@@ -2164,7 +2932,7 @@
   dependencies:
     ms "2.0.0"
 
-debug@3.2.6, debug@^3.0.0, debug@^3.1.0:
+debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
   integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -2209,11 +2977,21 @@
   dependencies:
     type-detect "^4.0.0"
 
+deep-equal@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+  integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
+
 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==
 
+deepmerge@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.3.0.tgz#d3c47fd6f3a93d517b14426b0628a17b0125f5f7"
+  integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==
+
 define-properties@^1.1.2, define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -2248,12 +3026,22 @@
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-depd@~1.1.2:
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@^1.1.2, depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
   integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
 
-destroy@~1.0.4:
+depd@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@^1.0.4, destroy@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
@@ -2275,6 +3063,11 @@
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
   integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
 
+di@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+  integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
+
 diagnostics@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a"
@@ -2309,6 +3102,16 @@
   dependencies:
     esutils "^2.0.2"
 
+dom-serialize@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
+  integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
+  dependencies:
+    custom-event "~1.0.0"
+    ent "~2.2.0"
+    extend "^3.0.0"
+    void-elements "^2.0.0"
+
 dom-urls@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/dom-urls/-/dom-urls-1.1.0.tgz#001ddf81628cd1e706125c7176f53ccec55d918e"
@@ -2354,6 +3157,11 @@
     readable-stream "^2.0.0"
     stream-shift "^1.0.0"
 
+dynamic-import-polyfill@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/dynamic-import-polyfill/-/dynamic-import-polyfill-0.1.1.tgz#e1f9eb1876ee242bd56572f8ed4df768e143083f"
+  integrity sha512-m953zv0w5oDagTItWm6Auhmk/pY7EiejaqiVbnzSS3HIjh1FCUeK7WzuaVtWPNs58A+/xpIE+/dVk6pKsrua8g==
+
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -2367,6 +3175,11 @@
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
+electron-to-chromium@^1.3.390:
+  version "1.3.392"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.392.tgz#280ab4f7a3ae47419cfabb15dbfc1567be7f1111"
+  integrity sha512-/hsgeVdReDsyTBE0aU9FRdh1wnNPrX3xlz3t61F+CJPOT+Umfi9DXHsCX85TEgWZQqlow0Rw44/4/jbU2Sqgkg==
+
 emitter-component@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
@@ -2384,18 +3197,35 @@
   dependencies:
     env-variable "0.0.x"
 
-encodeurl@~1.0.2:
+encodeurl@^1.0.2, 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.4.1:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   version "1.4.4"
   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"
 
+engine.io-client@~3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36"
+  integrity sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==
+  dependencies:
+    component-emitter "1.2.1"
+    component-inherit "0.0.3"
+    debug "~3.1.0"
+    engine.io-parser "~2.1.1"
+    has-cors "1.1.0"
+    indexof "0.0.1"
+    parseqs "0.0.5"
+    parseuri "0.0.5"
+    ws "~3.3.1"
+    xmlhttprequest-ssl "~1.5.4"
+    yeast "0.1.2"
+
 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"
@@ -2413,6 +3243,17 @@
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
 
+engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6"
+  integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==
+  dependencies:
+    after "0.8.2"
+    arraybuffer.slice "~0.0.7"
+    base64-arraybuffer "0.1.5"
+    blob "0.0.5"
+    has-binary2 "~1.0.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"
@@ -2424,6 +3265,18 @@
     blob "0.0.5"
     has-binary2 "~1.0.2"
 
+engine.io@~3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2"
+  integrity sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==
+  dependencies:
+    accepts "~1.3.4"
+    base64id "1.0.0"
+    cookie "0.3.1"
+    debug "~3.1.0"
+    engine.io-parser "~2.1.0"
+    ws "~3.3.1"
+
 engine.io@~3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
@@ -2436,18 +3289,28 @@
     engine.io-parser "~2.2.0"
     ws "^7.1.2"
 
+ent@~2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+  integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
+
 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==
 
-error-ex@^1.2.0:
+error-ex@^1.2.0, error-ex@^1.3.1:
   version "1.3.2"
   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-inject@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
+  integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
+
 es-abstract@^1.17.0-next.1:
   version "1.17.4"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184"
@@ -2465,6 +3328,67 @@
     string.prototype.trimleft "^2.1.1"
     string.prototype.trimright "^2.1.1"
 
+es-dev-server@^1.46.0:
+  version "1.46.0"
+  resolved "https://registry.yarnpkg.com/es-dev-server/-/es-dev-server-1.46.0.tgz#6fa8615604d8bfaa6a181f3bfb8b62d9f4b3dd81"
+  integrity sha512-+6RDz/YeBEkEHcf84I2pS+JYY1ov3d24JAda8hVMFQtpI21+G4/t0YA3lZSIYlPZ6AIoTgmb/+pKQh6JzmOUVA==
+  dependencies:
+    "@babel/core" "^7.9.0"
+    "@babel/plugin-proposal-dynamic-import" "^7.8.3"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-proposal-optional-chaining" "^7.9.0"
+    "@babel/plugin-syntax-class-properties" "^7.8.3"
+    "@babel/plugin-syntax-import-meta" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+    "@babel/plugin-transform-template-literals" "^7.8.3"
+    "@babel/preset-env" "^7.9.0"
+    "@open-wc/building-utils" "^2.16.1"
+    "@rollup/plugin-node-resolve" "^6.1.0"
+    "@rollup/pluginutils" "^3.0.0"
+    "@types/minimatch" "^3.0.3"
+    browserslist "^4.9.1"
+    browserslist-useragent "^3.0.2"
+    builtin-modules "^3.1.0"
+    camelcase "^5.3.1"
+    caniuse-api "^3.0.0"
+    caniuse-lite "^1.0.30001033"
+    chokidar "^3.0.0"
+    command-line-args "^5.0.2"
+    command-line-usage "^6.1.0"
+    debounce "^1.2.0"
+    deepmerge "^3.2.0"
+    es-module-lexer "^0.3.13"
+    get-stream "^5.1.0"
+    is-stream "^2.0.0"
+    isbinaryfile "^4.0.2"
+    koa "^2.7.0"
+    koa-compress "^3.0.0"
+    koa-etag "^3.0.0"
+    koa-static "^5.0.0"
+    lru-cache "^5.1.1"
+    minimatch "^3.0.4"
+    opn "^5.4.0"
+    parse5 "^5.1.1"
+    path-is-inside "^1.0.2"
+    polyfills-loader "^1.5.2"
+    portfinder "^1.0.21"
+    strip-ansi "^5.2.0"
+    systemjs "^4.0.0"
+    useragent "^2.3.0"
+    whatwg-url "^7.0.0"
+
+es-module-lexer@^0.3.13:
+  version "0.3.17"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.17.tgz#a248dec2870934d9054420fead19db095ea21537"
+  integrity sha512-nwvMtzyEB6FhlyXBlV+BW2By3Vn2sUvlQBYP4LvdK8YpdbFQUOiBoeuB7/ip1+EbjmgNydkJ8+dIlyO09VP9BA==
+
+es-module-shims@^0.4.6:
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-0.4.6.tgz#5decb313d52e5c62f6c19ed7e664ee9d66317d8a"
+  integrity sha512-EzVhnLyA/zvmGrAy2RU8m9xpxX7u2yb2by1GZH80SHF6lakG21YAm3Vo56KsLIXaIjT9QabqjYpQU1S5FkM8+Q==
+
 es-to-primitive@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@@ -2514,12 +3438,17 @@
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
+estree-walker@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
+  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
+
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-etag@~1.8.1:
+etag@^1.3.0, etag@~1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
@@ -2716,7 +3645,14 @@
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
-finalhandler@~1.1.2:
+fill-range@^7.0.1:
+  version "7.0.1"
+  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"
+
+finalhandler@1.1.2, finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
   integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@@ -2758,6 +3694,13 @@
     path-exists "^2.0.0"
     pinkie-promise "^2.0.0"
 
+find-up@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+  integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+  dependencies:
+    locate-path "^2.0.0"
+
 findup-sync@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
@@ -2780,6 +3723,11 @@
   dependencies:
     is-buffer "~2.0.3"
 
+flatted@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
+  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+
 follow-redirects@^1.0.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
@@ -2849,7 +3797,7 @@
   resolved "https://registry.yarnpkg.com/freeport/-/freeport-1.0.5.tgz#255e8ab84170c33ba85d990e821ae5f4a1a9bc5d"
   integrity sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=
 
-fresh@0.5.2:
+fresh@0.5.2, fresh@~0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
@@ -2859,11 +3807,25 @@
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
   integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
 
+fs-extra@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
+  integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
+fsevents@~2.1.1, fsevents@~2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
+  integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -2894,6 +3856,13 @@
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
   integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
 
+get-stream@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
+  integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
+  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"
@@ -2929,6 +3898,13 @@
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
+glob-parent@~5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  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"
@@ -3044,7 +4020,7 @@
     unzip-response "^2.0.1"
     url-parse-lax "^1.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:
+graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, 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==
@@ -3101,7 +4077,7 @@
   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.0, har-validator@~5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
   integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
@@ -3191,7 +4167,7 @@
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
   integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
 
-he@1.2.0, he@1.2.x:
+he@1.2.0, he@1.2.x, he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -3231,6 +4207,27 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
+html-minifier@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56"
+  integrity sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==
+  dependencies:
+    camel-case "^3.0.0"
+    clean-css "^4.2.1"
+    commander "^2.19.0"
+    he "^1.2.0"
+    param-case "^2.1.1"
+    relateurl "^0.2.7"
+    uglify-js "^3.5.1"
+
+http-assert@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878"
+  integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==
+  dependencies:
+    deep-equal "~1.0.1"
+    http-errors "~1.7.2"
+
 http-deceiver@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -3247,17 +4244,7 @@
     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:
+http-errors@^1.6.3, 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==
@@ -3268,6 +4255,16 @@
     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-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"
@@ -3278,7 +4275,7 @@
     lodash "^4.17.2"
     micromatch "^2.3.11"
 
-http-proxy@^1.16.2:
+http-proxy@^1.13.0, 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==
@@ -3374,7 +4371,12 @@
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
 
-invariant@^2.2.2:
+intersection-observer@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.7.0.tgz#ee16bee978db53516ead2f0a8154b09b400bbdc9"
+  integrity sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg==
+
+invariant@^2.2.2, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -3415,6 +4417,13 @@
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
   integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
 
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
 is-buffer@^1.1.5, is-buffer@~1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -3503,7 +4512,7 @@
   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.0, is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
@@ -3539,6 +4548,13 @@
   dependencies:
     is-extglob "^2.1.0"
 
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  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-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"
@@ -3547,6 +4563,11 @@
     global-dirs "^0.1.0"
     is-path-inside "^1.0.0"
 
+is-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+  integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
+
 is-npm@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
@@ -3571,6 +4592,11 @@
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
   integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
 
+is-number@^7.0.0:
+  version "7.0.0"
+  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"
@@ -3622,6 +4648,11 @@
   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.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
 is-symbol@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@@ -3649,6 +4680,11 @@
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
   integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
 
+is-wsl@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+  integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -3664,6 +4700,18 @@
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
   integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
 
+isbinaryfile@^3.0.0:
+  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.2:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.5.tgz#7193454fdd7fc0b12855c36c48d4ac7368fa3ec9"
+  integrity sha512-Jvz0gpTh1AILHMCBUyqq7xv1ZOQrxTDwyp1/QUq1xFpOBvp4AH5uEobPePJht8KnBGqQIH7We6OR73mXsjG0cA==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -3686,6 +4734,24 @@
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
+istanbul-lib-coverage@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
+  integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+
+istanbul-lib-instrument@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
+  integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
+  dependencies:
+    "@babel/generator" "^7.4.0"
+    "@babel/parser" "^7.4.3"
+    "@babel/template" "^7.4.0"
+    "@babel/traverse" "^7.4.3"
+    "@babel/types" "^7.4.0"
+    istanbul-lib-coverage "^2.0.5"
+    semver "^6.0.0"
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3724,6 +4790,11 @@
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
 
+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-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"
@@ -3756,6 +4827,20 @@
   dependencies:
     minimist "^1.2.0"
 
+json5@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
+  integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
 jsonschema@^1.1.0, jsonschema@^1.1.1:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.5.tgz#bab69d97fa28946aec0a56a9cc266d23fe80ae61"
@@ -3771,6 +4856,68 @@
     json-schema "0.2.3"
     verror "1.10.0"
 
+karma-chrome-launcher@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
+  integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==
+  dependencies:
+    which "^1.2.1"
+
+karma-mocha-reporter@^2.2.5:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz#15120095e8ed819186e47a0b012f3cd741895560"
+  integrity sha1-FRIAlejtgZGG5HoLAS8810GJVWA=
+  dependencies:
+    chalk "^2.1.0"
+    log-symbols "^2.1.0"
+    strip-ansi "^4.0.0"
+
+karma-mocha@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d"
+  integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==
+  dependencies:
+    minimist "^1.2.3"
+
+karma@^4.4.1:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/karma/-/karma-4.4.1.tgz#6d9aaab037a31136dc074002620ee11e8c2e32ab"
+  integrity sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A==
+  dependencies:
+    bluebird "^3.3.0"
+    body-parser "^1.16.1"
+    braces "^3.0.2"
+    chokidar "^3.0.0"
+    colors "^1.1.0"
+    connect "^3.6.0"
+    di "^0.0.1"
+    dom-serialize "^2.2.0"
+    flatted "^2.0.0"
+    glob "^7.1.1"
+    graceful-fs "^4.1.2"
+    http-proxy "^1.13.0"
+    isbinaryfile "^3.0.0"
+    lodash "^4.17.14"
+    log4js "^4.0.0"
+    mime "^2.3.1"
+    minimatch "^3.0.2"
+    optimist "^0.6.1"
+    qjobs "^1.1.4"
+    range-parser "^1.2.0"
+    rimraf "^2.6.0"
+    safe-buffer "^5.0.1"
+    socket.io "2.1.1"
+    source-map "^0.6.1"
+    tmp "0.0.33"
+    useragent "2.3.0"
+
+keygrip@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
+  integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
+  dependencies:
+    tsscmp "1.0.6"
+
 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"
@@ -3795,6 +4942,97 @@
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
+koa-compose@^3.0.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
+  integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
+  dependencies:
+    any-promise "^1.1.0"
+
+koa-compose@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
+  integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
+
+koa-compress@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-3.0.0.tgz#3194059c215cbc24e59bbc84c2c7453a4c88564f"
+  integrity sha512-xol+LkNB1mozKJkB5Kj6nYXbJXhkLkZlXl9BsGBPjujVfZ8MsIXwU4GHRTT7TlSfUcl2DU3JtC+j6wOWcovfuQ==
+  dependencies:
+    bytes "^3.0.0"
+    compressible "^2.0.0"
+    koa-is-json "^1.0.0"
+    statuses "^1.0.0"
+
+koa-convert@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
+  integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
+  dependencies:
+    co "^4.6.0"
+    koa-compose "^3.0.0"
+
+koa-etag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-3.0.0.tgz#9ef7382ddd5a82ab0deb153415c915836f771d3f"
+  integrity sha1-nvc4Ld1agqsN6xU0FckVg293HT8=
+  dependencies:
+    etag "^1.3.0"
+    mz "^2.1.0"
+
+koa-is-json@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
+  integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
+
+koa-send@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb"
+  integrity sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ==
+  dependencies:
+    debug "^3.1.0"
+    http-errors "^1.6.3"
+    mz "^2.7.0"
+    resolve-path "^1.4.0"
+
+koa-static@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943"
+  integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==
+  dependencies:
+    debug "^3.1.0"
+    koa-send "^5.0.0"
+
+koa@^2.7.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4"
+  integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA==
+  dependencies:
+    accepts "^1.3.5"
+    cache-content-type "^1.0.0"
+    content-disposition "~0.5.2"
+    content-type "^1.0.4"
+    cookies "~0.8.0"
+    debug "~3.1.0"
+    delegates "^1.0.0"
+    depd "^1.1.2"
+    destroy "^1.0.4"
+    encodeurl "^1.0.2"
+    error-inject "^1.0.0"
+    escape-html "^1.0.3"
+    fresh "~0.5.2"
+    http-assert "^1.3.0"
+    http-errors "^1.6.3"
+    is-generator-function "^1.0.7"
+    koa-compose "^4.1.0"
+    koa-convert "^1.2.0"
+    on-finished "^2.3.0"
+    only "~0.0.2"
+    parseurl "^1.3.2"
+    statuses "^1.5.0"
+    type-is "^1.6.16"
+    vary "^1.1.2"
+
 kuler@1.0.x:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6"
@@ -3830,6 +5068,18 @@
   dependencies:
     readable-stream "^2.0.5"
 
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levenary@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
+  integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
+  dependencies:
+    leven "^3.1.0"
+
 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"
@@ -3841,6 +5091,24 @@
     pinkie-promise "^2.0.0"
     strip-bom "^2.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"
+  integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^4.0.0"
+    pify "^3.0.0"
+    strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+  version "2.0.0"
+  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"
@@ -3940,6 +5208,11 @@
     lodash.isarguments "^3.0.0"
     lodash.isarray "^3.0.0"
 
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
 lodash.padend@^4.6.1:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
@@ -3970,6 +5243,11 @@
   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"
@@ -3980,13 +5258,31 @@
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
-log-symbols@2.2.0:
+log-symbols@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
+  integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
+  dependencies:
+    chalk "^2.4.2"
+
+log-symbols@^2.1.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"
 
+log4js@^4.0.0:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5"
+  integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==
+  dependencies:
+    date-format "^2.0.0"
+    debug "^4.1.1"
+    flatted "^2.0.0"
+    rfdc "^1.1.4"
+    streamroller "^1.0.6"
+
 logform@^1.9.1:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/logform/-/logform-1.10.0.tgz#c9d5598714c92b546e23f4e78147c40f1e02012e"
@@ -4044,7 +5340,7 @@
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
   integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
 
-lru-cache@^4.0.1, lru-cache@^4.0.2:
+lru-cache@4.1.x, 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==
@@ -4052,6 +5348,13 @@
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
+lru-cache@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+  dependencies:
+    yallist "^3.0.2"
+
 magic-string@^0.22.4:
   version "0.22.5"
   resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
@@ -4185,7 +5488,7 @@
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
   integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
 
-mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
+mime-types@^2.1.12, mime-types@^2.1.18, 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==
@@ -4236,6 +5539,11 @@
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
+minimist@^1.2.3, 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==
+
 minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
@@ -4256,6 +5564,13 @@
   dependencies:
     minimist "0.0.8"
 
+mkdirp@0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.3.tgz#5a514b7179259287952881e94410ec5465659f8c"
+  integrity sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==
+  dependencies:
+    minimist "^1.2.5"
+
 mocha@^3.4.2:
   version "3.5.3"
   resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
@@ -4274,13 +5589,14 @@
     mkdirp "0.5.1"
     supports-color "3.1.2"
 
-mocha@^6.2.2:
-  version "6.2.2"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20"
-  integrity sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==
+mocha@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.1.tgz#89fbb30d09429845b1bb893a830bf5771049a441"
+  integrity sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==
   dependencies:
     ansi-colors "3.2.3"
     browser-stdout "1.3.1"
+    chokidar "3.3.0"
     debug "3.2.6"
     diff "3.5.0"
     escape-string-regexp "1.0.5"
@@ -4289,18 +5605,18 @@
     growl "1.10.5"
     he "1.2.0"
     js-yaml "3.13.1"
-    log-symbols "2.2.0"
+    log-symbols "3.0.0"
     minimatch "3.0.4"
-    mkdirp "0.5.1"
+    mkdirp "0.5.3"
     ms "2.1.1"
-    node-environment-flags "1.0.5"
+    node-environment-flags "1.0.6"
     object.assign "4.1.0"
     strip-json-comments "2.0.1"
     supports-color "6.0.0"
     which "1.3.1"
     wide-align "1.1.3"
-    yargs "13.3.0"
-    yargs-parser "13.1.1"
+    yargs "13.3.2"
+    yargs-parser "13.1.2"
     yargs-unparser "1.6.0"
 
 mout@^1.0.0:
@@ -4345,7 +5661,7 @@
     duplexer2 "^0.1.2"
     object-assign "^4.1.0"
 
-mz@^2.4.0, mz@^2.6.0:
+mz@^2.1.0, mz@^2.4.0, mz@^2.6.0, mz@^2.7.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
   integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
@@ -4393,14 +5709,24 @@
   dependencies:
     lower-case "^1.1.1"
 
-node-environment-flags@1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a"
-  integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==
+node-environment-flags@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
+  integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==
   dependencies:
     object.getownpropertydescriptors "^2.0.3"
     semver "^5.7.0"
 
+node-fetch@^2.6.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-releases@^1.1.53:
+  version "1.1.53"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4"
+  integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==
+
 nomnom@^1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
@@ -4426,7 +5752,7 @@
   dependencies:
     remove-trailing-separator "^1.0.1"
 
-normalize-path@^3.0.0:
+normalize-path@^3.0.0, 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==
@@ -4544,7 +5870,7 @@
   resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
   integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
 
-once@^1.3.0, once@^1.4.0:
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@@ -4556,6 +5882,11 @@
   resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e"
   integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=
 
+only@~0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
+  integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
+
 opn@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a"
@@ -4563,6 +5894,13 @@
   dependencies:
     object-assign "^4.0.1"
 
+opn@^5.4.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+  integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+  dependencies:
+    is-wsl "^1.1.0"
+
 optimist@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -4584,7 +5922,7 @@
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
 
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@@ -4602,6 +5940,13 @@
   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.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:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
@@ -4609,6 +5954,13 @@
   dependencies:
     p-try "^2.0.0"
 
+p-locate@^2.0.0:
+  version "2.0.0"
+  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"
@@ -4616,6 +5968,11 @@
   dependencies:
     p-limit "^2.0.0"
 
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+  integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
 p-try@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
@@ -4631,7 +5988,7 @@
     registry-url "^3.0.3"
     semver "^5.1.0"
 
-param-case@2.1.x:
+param-case@2.1.x, param-case@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
   integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
@@ -4655,6 +6012,14 @@
   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"
+  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
 parse-passwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
@@ -4665,6 +6030,11 @@
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
   integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
 
+parse5@^5.1.1:
+  version "5.1.1"
+  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"
@@ -4679,7 +6049,7 @@
   dependencies:
     better-assert "~1.0.0"
 
-parseurl@~1.3.3:
+parseurl@^1.3.2, 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==
@@ -4706,7 +6076,7 @@
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
   integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
 
-path-is-absolute@^1.0.0:
+path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
@@ -4747,6 +6117,13 @@
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+  dependencies:
+    pify "^3.0.0"
+
 pathval@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
@@ -4772,6 +6149,11 @@
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
+picomatch@^2.0.4, picomatch@^2.0.7:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -4794,6 +6176,13 @@
   resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
   integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
 
+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:
+    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"
@@ -4812,6 +6201,28 @@
     winston "^3.0.0"
     winston-transport "^4.2.0"
 
+polyfills-loader@^1.5.2:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/polyfills-loader/-/polyfills-loader-1.5.2.tgz#2fe63063da0d74aa69b611bd189d64ee443358c7"
+  integrity sha512-bcv8Id4Ylae0eSmnlMpKfba36TNr1Mh7uMGN4OEIspHLGR5/IBGSBteQp/NpbZ45T7UWQuTBvVJNQCS16E1N4A==
+  dependencies:
+    "@babel/core" "^7.9.0"
+    "@open-wc/building-utils" "^2.16.1"
+    "@webcomponents/webcomponentsjs" "^2.4.0"
+    abortcontroller-polyfill "^1.4.0"
+    core-js-bundle "^3.6.0"
+    deepmerge "^3.2.0"
+    dynamic-import-polyfill "^0.1.1"
+    es-module-shims "^0.4.6"
+    html-minifier "^4.0.0"
+    intersection-observer "^0.7.0"
+    parse5 "^5.1.1"
+    regenerator-runtime "^0.13.3"
+    resize-observer-polyfill "^1.5.1"
+    systemjs "^4.0.0"
+    terser "^4.6.4"
+    whatwg-fetch "^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"
@@ -5001,6 +6412,15 @@
     send "^0.16.2"
     spdy "^3.3.3"
 
+portfinder@^1.0.21:
+  version "1.0.25"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
+  integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==
+  dependencies:
+    async "^2.6.2"
+    debug "^3.1.1"
+    mkdirp "^0.5.1"
+
 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"
@@ -5021,7 +6441,7 @@
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
   integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=
 
-private@^0.1.6:
+private@^0.1.6, private@^0.1.8:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
   integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
@@ -5054,12 +6474,25 @@
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
   integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
 
+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@^3.0.0:
+  version "3.0.0"
+  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"
+
 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.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==
@@ -5069,6 +6502,11 @@
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
 
+qjobs@^1.1.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
+  integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
+
 qs@6.7.0:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -5088,7 +6526,7 @@
     kind-of "^6.0.0"
     math-random "^1.0.1"
 
-range-parser@~1.2.0, range-parser@~1.2.1:
+range-parser@^1.2.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"
   integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
@@ -5121,6 +6559,14 @@
     find-up "^1.0.0"
     read-pkg "^1.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@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -5130,6 +6576,15 @@
     normalize-package-data "^2.3.2"
     path-type "^1.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"
+  integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+  dependencies:
+    load-json-file "^4.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^3.0.0"
+
 readable-stream@1.1.x:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -5172,6 +6627,20 @@
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
+readdirp@~3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"
+  integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==
+  dependencies:
+    picomatch "^2.0.4"
+
+readdirp@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
+  integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+  dependencies:
+    picomatch "^2.0.7"
+
 redent@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -5185,6 +6654,11 @@
   resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
   integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=
 
+reduce-flatten@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27"
+  integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==
+
 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"
@@ -5192,6 +6666,13 @@
   dependencies:
     regenerate "^1.4.0"
 
+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"
@@ -5202,6 +6683,11 @@
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
+regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
+  version "0.13.5"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
+  integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+
 regenerator-transform@^0.14.0:
   version "0.14.1"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb"
@@ -5209,6 +6695,14 @@
   dependencies:
     private "^0.1.6"
 
+regenerator-transform@^0.14.2:
+  version "0.14.4"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
+  integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==
+  dependencies:
+    "@babel/runtime" "^7.8.4"
+    private "^0.1.8"
+
 regex-cache@^0.4.2:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
@@ -5236,6 +6730,18 @@
     unicode-match-property-ecmascript "^1.0.4"
     unicode-match-property-value-ecmascript "^1.1.0"
 
+regexpu-core@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
+  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+  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"
+
 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"
@@ -5251,7 +6757,7 @@
   dependencies:
     rc "^1.0.1"
 
-regjsgen@^0.5.0:
+regjsgen@^0.5.0, regjsgen@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
   integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
@@ -5263,7 +6769,14 @@
   dependencies:
     jsesc "~0.5.0"
 
-relateurl@0.2.x:
+regjsparser@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
+  integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
+  dependencies:
+    jsesc "~0.5.0"
+
+relateurl@0.2.x, relateurl@^0.2.7:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
   integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
@@ -5321,6 +6834,32 @@
     tunnel-agent "^0.6.0"
     uuid "^3.3.2"
 
+request@^2.88.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-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -5341,6 +6880,11 @@
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 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"
@@ -5349,6 +6893,14 @@
     expand-tilde "^2.0.0"
     global-modules "^1.0.0"
 
+resolve-path@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
+  integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=
+  dependencies:
+    http-errors "~1.6.2"
+    path-is-absolute "1.0.1"
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -5361,12 +6913,24 @@
   dependencies:
     path-parse "^1.0.6"
 
+resolve@^1.11.1:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
+  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+  dependencies:
+    path-parse "^1.0.6"
+
 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==
 
-rimraf@^2.5.4:
+rfdc@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
+  integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==
+
+rimraf@^2.5.4, rimraf@^2.6.0:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -5480,6 +7044,16 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
+semver@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
+  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
+
+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==
+
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -5568,6 +7142,11 @@
   resolved "https://registry.yarnpkg.com/shady-css-parser/-/shady-css-parser-0.1.0.tgz#534dc79c8ca5884c5ed92a4e5a13d6d863bca428"
   integrity sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA==
 
+shady-css-scoped-element@^0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/shady-css-scoped-element/-/shady-css-scoped-element-0.0.2.tgz#c538fcfe2317e979cd02dfec533898b95b4ea8fe"
+  integrity sha512-Dqfl70x6JiwYDujd33ZTbtCK0t52E7+H2swdWQNSTzfsolSa6LJHnTpN4T9OpJJEq4bxuzHRLFO9RBcy/UfrMQ==
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -5656,6 +7235,26 @@
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
   integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
 
+socket.io-client@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f"
+  integrity sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==
+  dependencies:
+    backo2 "1.0.2"
+    base64-arraybuffer "0.1.5"
+    component-bind "1.0.0"
+    component-emitter "1.2.1"
+    debug "~3.1.0"
+    engine.io-client "~3.2.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"
+    socket.io-parser "~3.2.0"
+    to-array "0.1.4"
+
 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"
@@ -5676,6 +7275,15 @@
     socket.io-parser "~3.3.0"
     to-array "0.1.4"
 
+socket.io-parser@~3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077"
+  integrity sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==
+  dependencies:
+    component-emitter "1.2.1"
+    debug "~3.1.0"
+    isarray "2.0.1"
+
 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"
@@ -5694,6 +7302,18 @@
     debug "~4.1.0"
     isarray "2.0.1"
 
+socket.io@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980"
+  integrity sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==
+  dependencies:
+    debug "~3.1.0"
+    engine.io "~3.2.0"
+    has-binary2 "~1.0.2"
+    socket.io-adapter "~1.1.0"
+    socket.io-client "2.1.1"
+    socket.io-parser "~3.2.0"
+
 socket.io@^2.0.3:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
@@ -5717,6 +7337,14 @@
     source-map-url "^0.4.0"
     urix "^0.1.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==
+  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"
@@ -5727,7 +7355,7 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
 
-source-map@^0.6.1, 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, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -5836,7 +7464,7 @@
     define-property "^0.2.5"
     object-copy "^0.1.0"
 
-"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.0.0, statuses@^1.5.0, statuses@~1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
@@ -5858,6 +7486,17 @@
   dependencies:
     emitter-component "^1.1.1"
 
+streamroller@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9"
+  integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==
+  dependencies:
+    async "^2.6.2"
+    date-format "^2.0.0"
+    debug "^3.2.6"
+    fs-extra "^7.0.1"
+    lodash "^4.17.14"
+
 streamsearch@0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@@ -5956,6 +7595,11 @@
   dependencies:
     is-utf8 "^0.2.0"
 
+strip-bom@^3.0.0:
+  version "3.0.0"
+  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"
@@ -6028,6 +7672,11 @@
     path-to-regexp "^1.0.1"
     serviceworker-cache-polyfill "^4.0.0"
 
+systemjs@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-4.1.1.tgz#c90061456f9707478d487b47f3b92b9896032889"
+  integrity sha512-/0x3bcMrl1pxDCLw6sJWEKPVy0ZGEu7I0nItFSHxfPoDU2Lll6TUyB1wqltvbm7n5y5jVOoK4lei4oMpmW7XJQ==
+
 table-layout@^0.4.3:
   version "0.4.5"
   resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378"
@@ -6039,6 +7688,16 @@
     typical "^2.6.1"
     wordwrapjs "^3.0.0"
 
+table-layout@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.1.tgz#8411181ee951278ad0638aea2f779a9ce42894f9"
+  integrity sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==
+  dependencies:
+    array-back "^4.0.1"
+    deep-extend "~0.6.0"
+    typical "^5.2.0"
+    wordwrapjs "^4.0.0"
+
 tar-stream@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.0.0.tgz#8829bbf83067bc0288a9089db49c56be395b6aea"
@@ -6085,6 +7744,25 @@
     merge-stream "^1.0.0"
     through2 "^2.0.1"
 
+terser@^4.6.4:
+  version "4.6.10"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.10.tgz#90f5bd069ff456ddbc9503b18e52f9c493d3b7c2"
+  integrity sha512-qbF/3UOo11Hggsbsqm2hPa6+L4w7bkr+09FNseEe8xrcVD3APGLFqE+Oz1ZKAxjYnFsj80rLOfgAtJ0LNJjtTA==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.12"
+
+test-exclude@^5.2.3:
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
+  integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
+  dependencies:
+    glob "^7.1.3"
+    minimatch "^3.0.4"
+    read-pkg-up "^4.0.0"
+    require-main-filename "^2.0.0"
+
 text-encoding@0.6.4:
   version "0.6.4"
   resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
@@ -6146,6 +7824,13 @@
   resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
   integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
 
+tmp@0.0.33, tmp@0.0.x:
+  version "0.0.33"
+  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"
@@ -6183,6 +7868,13 @@
     is-number "^3.0.0"
     repeat-string "^1.6.1"
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  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"
@@ -6206,6 +7898,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"
@@ -6228,6 +7928,11 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
+tsscmp@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
+  integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
+
 tunnel-agent@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -6255,7 +7960,7 @@
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
+type-is@^1.6.16, 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==
@@ -6278,6 +7983,11 @@
   resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
   integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
 
+typical@^5.0.0, typical@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
+  integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
+
 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"
@@ -6291,6 +8001,19 @@
     commander "~2.19.0"
     source-map "~0.6.1"
 
+uglify-js@^3.5.1:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.1.tgz#43bb15ce6f545eaa0a64c49fd29375ea09fa0f93"
+  integrity sha512-W7KxyzeaQmZvUFbGj4+YFshhVrMBGSg2IbcYAjGWGvx8DHvJMclbTDMpffdxFUGPBHjIytk7KJUR/KUXstUGDw==
+  dependencies:
+    commander "~2.20.3"
+    source-map "~0.6.1"
+
+ultron@~1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
+  integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
+
 underscore@^1.8.3:
   version "1.9.2"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f"
@@ -6319,6 +8042,11 @@
   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"
@@ -6349,6 +8077,11 @@
   dependencies:
     crypto-random-string "^1.0.0"
 
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -6424,6 +8157,14 @@
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+useragent@2.3.0, useragent@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
+  integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==
+  dependencies:
+    lru-cache "4.1.x"
+    tmp "0.0.x"
+
 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"
@@ -6455,6 +8196,11 @@
   resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6"
   integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=
 
+valid-url@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
+  integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -6468,7 +8214,7 @@
   resolved "https://registry.yarnpkg.com/vargs/-/vargs-0.1.0.tgz#6b6184da6520cc3204ce1b407cac26d92609ebff"
   integrity sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=
 
-vary@^1, vary@~1.1.2:
+vary@^1, vary@^1.1.2, vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
@@ -6519,6 +8265,11 @@
   resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
   integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
 
+void-elements@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+  integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
 vscode-uri@=1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d"
@@ -6631,6 +8382,11 @@
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
   integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
 
+whatwg-fetch@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+
 whatwg-url@^6.4.0:
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
@@ -6640,12 +8396,21 @@
     tr46 "^1.0.1"
     webidl-conversions "^4.0.2"
 
+whatwg-url@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
-which@1.3.1, which@^1.0.8, which@^1.2.14, which@^1.2.9, which@^1.3.1:
+which@1.3.1, which@^1.0.8, which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -6702,6 +8467,14 @@
     reduce-flatten "^1.0.1"
     typical "^2.6.1"
 
+wordwrapjs@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.0.tgz#9aa9394155993476e831ba8e59fb5795ebde6800"
+  integrity sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==
+  dependencies:
+    reduce-flatten "^2.0.0"
+    typical "^5.0.0"
+
 wrap-ansi@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
@@ -6730,6 +8503,15 @@
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
   integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
 
+ws@~3.3.1:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
+  integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
+  dependencies:
+    async-limiter "~1.0.0"
+    safe-buffer "~5.1.0"
+    ultron "~1.1.0"
+
 ws@~6.1.0:
   version "6.1.4"
   resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
@@ -6772,7 +8554,20 @@
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
 
-yargs-parser@13.1.1, yargs-parser@^13.1.1:
+yallist@^3.0.2:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+  integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yargs-parser@13.1.2, yargs-parser@^13.1.2:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@^13.1.1:
   version "13.1.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
   integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
@@ -6789,7 +8584,23 @@
     lodash "^4.17.15"
     yargs "^13.3.0"
 
-yargs@13.3.0, yargs@^13.3.0:
+yargs@13.3.2:
+  version "13.3.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+  dependencies:
+    cliui "^5.0.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^13.1.2"
+
+yargs@^13.3.0:
   version "13.3.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
   integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@@ -6818,6 +8629,11 @@
   resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
   integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
 
+ylru@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
+  integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
+
 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/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index c9ac0fe..2b473bc 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -23,6 +23,7 @@
     "//lib/bouncycastle:bcprov",
     "//lib/bouncycastle:bcpg",
     "//lib/log:impl-log4j",
+    "//lib:jgit-ssh-jsch",
     "//prolog:gerrit-prolog-common",
     "//resources:log4j-config",
 ]
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 9915a6e..f360fa5 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -181,6 +181,8 @@
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/resources')
 
     def classpathentry(kind, path, src=None, out=None, exported=None, excluding=None):
         e = doc.createElement('classpathentry')
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index f68afdb..8748250 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.2.3-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 6a087bd..ae31ac9 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.2.3-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 46514e0..dc25c80 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.2.3-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 19dbd0e..d21f88c 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.2.3-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 2af06b4..6fafe63 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -3,8 +3,8 @@
   "description": "Gerrit Build Tools",
   "browser": false,
   "dependencies": {
-    "@bazel/rollup": "^0.41.0",
-    "@bazel/typescript": "^1.0.1",
+    "@bazel/rollup": "^1.6.1",
+    "@bazel/typescript": "^1.6.1",
     "@types/node": "^10.17.12",
     "@types/parse5": "^4.0.0",
     "@types/parse5-html-rewriting-stream": "^5.1.2",
@@ -16,7 +16,7 @@
     "rollup": "^1.27.5",
     "rollup-plugin-node-resolve": "^5.2.0",
     "rollup-plugin-terser": "^5.1.3",
-    "typescript": "^3.7.4"
+    "typescript": "3.8.2"
   },
   "devDependencies": {},
   "license": "Apache-2.0",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 0648c8d..78349fa 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -492,15 +492,15 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
-"@bazel/rollup@^0.41.0":
-  version "0.41.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-0.41.0.tgz#8dfaccc239f3efbae1c816b0ce2aeb6069d23582"
-  integrity sha512-M+ybGfcxTXnAS1QiaijLEfUznNYLA0cqeGXnYHSRrOhq2U7yesfavxbBtfLSKtg32ktmlHts5te8Zg82BS4DPQ==
+"@bazel/rollup@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.6.1.tgz#7ec9d39a3fca23256fca55410339724804802616"
+  integrity sha512-FhblJkpd8VKl9txhAAIotSsIOHRpPd2FgJG7Op3uV7LfaCVBmUs3XDBZCgfwt5wmEpd3lwCHA1Ei+O/URS2+5w==
 
-"@bazel/typescript@^1.0.1":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.1.0.tgz#b57ac6c6d627577f394a60fb540fbbdf53bcff0d"
-  integrity sha512-QnTdb6rwZUR+KfUuAdyazpkA7BOvrWRe7tkPDdyIZHJdBPYdpJW+AapnFSfxvXEIP0Nwesl5KP6Saau0GPiBLg==
+"@bazel/typescript@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.6.1.tgz#1bf83c20021d359bc9b532181981ac540584a30c"
+  integrity sha512-wQ9AASRcG1jLQOpJfNOMjZzPpwIV/9qTOxCFvp55ga6A5a2qveQr8JJ7jHHbBM0LtK+slEPixXmVmtEOwfKsIg==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
@@ -949,11 +949,16 @@
   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", "@types/node@^10.17.12":
+"@types/node@^10.1.0":
   version "10.17.13"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c"
   integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==
 
+"@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@^4.0.30":
   version "4.9.4"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-4.9.4.tgz#75ef91733afaa856b01e12da6ecf48aa9d5e221f"
@@ -7871,10 +7876,10 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@^3.7.4:
-  version "3.7.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
-  integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
+typescript@3.8.2:
+  version "3.8.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a"
+  integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==
 
 typical@^2.6.0, typical@^2.6.1:
   version "2.6.1"
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 81fcd5c..1da5004 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -109,24 +109,24 @@
     # Google internal dependencies: these are developed at Google, so there is
     # no concern about version skew.
 
-    FLOGGER_VERS = "0.4"
+    FLOGGER_VERS = "0.5.1"
 
     maven_jar(
         name = "flogger",
         artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
-        sha1 = "9c8863dcc913b56291c0c88e6d4ca9715b43df98",
+        sha1 = "71d1e2cef9cc604800825583df56b8ef5c053f14",
     )
 
     maven_jar(
         name = "flogger-log4j-backend",
         artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
-        sha1 = "17aa5e31daa1354187e14b6978597d630391c028",
+        sha1 = "5e2794b75c88223f263f1c1a9d7ea51e2dc45732",
     )
 
     maven_jar(
         name = "flogger-system-backend",
         artifact = "com.google.flogger:flogger-system-backend:" + FLOGGER_VERS,
-        sha1 = "287b569d76abcd82f9de87fe41829fbc7ebd8ac9",
+        sha1 = "b66d3bedb14da604828a8693bb24fd78e36b0e9e",
     )
 
     # Test-only dependencies below.
diff --git a/version.bzl b/version.bzl
index 3ed3f27..78b286b 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.2.3-SNAPSHOT"
+GERRIT_VERSION = "3.3.0-SNAPSHOT"
diff --git a/yarn.lock b/yarn.lock
index 8379dd7..02ac32e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -485,15 +485,15 @@
     lodash "^4.17.11"
     to-fast-properties "^2.0.0"
 
-"@bazel/rollup@^1.1.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.2.0.tgz#8b9569ed6f1c00d2a833567901f8ee4600a389fb"
-  integrity sha512-yrXW+AAUoqc9qN/CweD5p8OEN9bNKFjXnXPBRE4w84LxpkmaJFx+yQJ++c1F57zWMoq2o9EV4CM7y+mK8zxwUg==
+"@bazel/rollup@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.6.1.tgz#7ec9d39a3fca23256fca55410339724804802616"
+  integrity sha512-FhblJkpd8VKl9txhAAIotSsIOHRpPd2FgJG7Op3uV7LfaCVBmUs3XDBZCgfwt5wmEpd3lwCHA1Ei+O/URS2+5w==
 
-"@bazel/typescript@^1.0.1":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.2.0.tgz#ab2016e1d6eb7a86b44536e887f51eaf3d75f1a7"
-  integrity sha512-hPEG8K0psyEcs6HFRiqZNQwXL/dQ8sXKdrNFWv87+rh+YUNfd58uktoynhllympOPThcbUZcZicLWBEFQOc8nA==
+"@bazel/typescript@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.6.1.tgz#1bf83c20021d359bc9b532181981ac540584a30c"
+  integrity sha512-wQ9AASRcG1jLQOpJfNOMjZzPpwIV/9qTOxCFvp55ga6A5a2qveQr8JJ7jHHbBM0LtK+slEPixXmVmtEOwfKsIg==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
@@ -907,9 +907,9 @@
   integrity sha512-rp7La3m845mSESCgsJePNL/JQyhkOJA6G4vcwvVgkDAwHhGdq5GCumxmPjEk1MZf+8p5ZQAUE7tqgQRQTXN7uQ==
 
 "@types/node@^10.1.0":
-  version "10.17.13"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c"
-  integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==
+  version "10.17.24"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944"
+  integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA==
 
 "@types/node@^4.0.30":
   version "4.9.3"
@@ -8596,7 +8596,12 @@
     source-map "^0.5.6"
     source-map-support "^0.4.2"
 
-tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.8.1:
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+
+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==
@@ -8657,16 +8662,16 @@
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
+typescript@3.8.2:
+  version "3.8.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a"
+  integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==
+
 typescript@^2.4.1:
   version "2.9.2"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
   integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
 
-typescript@^3.7.4:
-  version "3.7.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
-  integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
-
 typical@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"