Merge "Preload Highlight.js on Diff Page"
diff --git a/.bazelversion b/.bazelversion
index fd2a018..47b322c 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-3.1.0
+3.4.1
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index c9e2ed5..8bb5d54 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -869,6 +869,14 @@
 private changes (even without having the `View Private Changes` access
 right assigned).
 
+[[category_toggle_work_in_progress_state]]
+=== Toggle Work In Progress state
+
+This category controls who is able to flip the Work In Progress bit.
+
+Change owner, server administrators and project owners can always flip
+the Work In Progress bit of the change (even without having the
+`Toggle Work In Progress state` access right assigned).
 
 [[category_delete_own_changes]]
 === Delete Own Changes
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 5d22c0b..85006dc 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -109,10 +109,10 @@
 reverting a change.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
 ChangeFooter.
 
-=== RegisterNewEmail.soy
+=== RegisterNewEmail.soy and RegisterNewEmailHtml.soy
 
-The `RegisterNewEmail.soy` template will determine the contents of the email
-related to registering new email accounts.
+Those templates will determine the contents of the email related to registering
+new email accounts.
 
 === ReplacePatchSet.soy and ReplacePatchSetHtml.soy
 
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
index 9bc7f0b..02ac2f5 100644
--- a/Documentation/dev-crafting-changes.txt
+++ b/Documentation/dev-crafting-changes.txt
@@ -10,6 +10,37 @@
 beyond simple spacing issues.  Blame it on our short attention
 spans, we really do want your code.
 
+
+[[branch]]
+== Branch
+
+Gerrit provides support for more than one version, which naturally
+raises the question of which branch you should start your contribution
+on. There are no hard and fast rules, but below we try to outline some
+guidelines:
+
+* Genuinely new and/or disruptive features, should generally start on
+  `master`. Also consider submitting a
+  link:dev-design-docs.html[design doc] beforehand to allow discussion
+  by the ESC and the community.
+* Improvements of existing features should also generally go into
+  `master`. But we understand that if you cannot run `master`, it
+  might take a while until you could benefit from it. In that case,
+  start on the newest `stable-*` branch that you can run.
+* Bug-fixes should generally at least cover the oldest affected and
+  still supported version. If you're affected and run an even older
+  version, you're welcome to upload to that older version, even if
+  it is no longer officially supported, bearing in mind that
+  verification and release may happen only once merged upstream.
+
+Regardless of the above, changes might get moved to a different branch
+before being submitted or might get cherry-picked/re-merged to a
+different branch even after they've landed.
+
+For each of the above items, you'll find ad-hoc exceptions. The point
+is: We'd much rather see your code and fixes than not see them.
+
+
 [[commit-message]]
 == Commit Message
 
@@ -116,7 +147,7 @@
 link:https://github.com/google/google-java-format[`google-java-format`,role=external,window=_blank]
 tool (version 1.7), and to format Bazel BUILD, WORKSPACE and .bzl files the
 link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`,role=external,window=_blank]
-tool (version 3.2.1). Unused dependencies are found and removed using the
+tool (version 3.3.0). Unused dependencies are found and removed using the
 link:https://github.com/bazelbuild/buildtools/tree/master/unused_deps[`unused_deps`,role=external,window=_blank]
 build tool, a sibling of `buildifier`.
 
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index 8fe8f4e..78b2c15 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -116,7 +116,7 @@
     "cmd": "clone"
   },
   {
-    "url": "http://HOSTNAME:HTTP_PORT/_PROJECT",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/_PROJECT",
     "cmd": "clone"
   }
 ]
@@ -153,10 +153,12 @@
 * `-Dcom.google.gerrit.scenarios.hostname=localhost`
 * `-Dcom.google.gerrit.scenarios.ssh_port=29418`
 * `-Dcom.google.gerrit.scenarios.http_port=8080`
+* `-Dcom.google.gerrit.scenarios.http_scheme=http`
 
 Above, the properties can be set with values matching specific deployment topologies under test.
-The example values shown above are the currently coded default ones. The framework could support
-differing or more properties over time.
+The example values shown above are the currently coded default ones. For example, the `http` scheme
+above could be replaced with `https`. The framework could support differing or more properties over
+time.
 
 Plugin or otherwise non-core scenarios may do so just as well. The core java package
 `com.google.gerrit.scenarios` from the example above has to be replaced with the one under which
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index c8c2dff..c3df396 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2092,6 +2092,14 @@
 No Guice bindings or modules are required. Gerrit will automatically
 discover and bind the implementation.
 
+[[gerrit-replica]]
+== Gerrit Replica
+
+Gerrit can be run as a read-only replica. Some plugins may need to know
+whether Gerrit is run as a primary- or a replica instance. For that purpose
+Gerrit exposes the `@GerritIsReplica` annotation. A boolean annotated with
+this annotation will indicate whether Gerrit is run as a replica.
+
 [[accountcreation]]
 == Account Creation
 
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 2483ba3..eb2025c 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -590,8 +590,9 @@
 reply-modal.
 
 Change owners, project owners, site administrators and members of a group that
-was granted "Toggle Work In Progress state" permission can mark changes as
-`work-in-progress` and `ready`.
+was granted link:access-control.html#category_toggle_work_in_progress_state[
+Toggle Work In Progress state] permission can mark changes as `work-in-progress`
+and `ready`.
 
 [[private-changes]]
 == Private Changes
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 6fbedb0..86f546d 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1284,6 +1284,7 @@
   )]}'
   {
     "changes_per_page": 25,
+    "theme": "LIGHT",
     "date_format": "STD",
     "time_format": "HHMM_12",
     "diff_view": "SIDE_BY_SIDE",
@@ -1336,6 +1337,7 @@
 
   {
     "changes_per_page": 50,
+    "theme": "DARK",
     "expand_inline_diffs": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
@@ -1383,6 +1385,7 @@
   )]}'
   {
     "changes_per_page": 50,
+    "theme" "DARK",
     "expand_inline_diffs": true,
     "date_format": "STD",
     "time_format": "HHMM_12",
@@ -2759,6 +2762,9 @@
 |`changes_per_page`             ||
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
+|`theme`                        ||
+Which theme to use.
+Allowed values are `DARK` or `LIGHT`.
 |`expand_inline_diffs`          |not set if `false`|
 Whether to expand diffs inline instead of opening as separate page
 (PolyGerrit only).
@@ -2821,6 +2827,9 @@
 |`changes_per_page`             |optional|
 The number of changes to show on each page.
 Allowed values are `10`, `25`, `50`, `100`.
+|`theme`                        |optional|
+Which theme to use.
+Allowed values are `DARK` or `LIGHT`.
 |`expand_inline_diffs`          |not set if `false`|
 Whether to expand diffs inline instead of opening as separate page
 (PolyGerrit only).
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 3bb6564..e9bdc25 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6530,6 +6530,9 @@
 Available with published comments. Contains the
 link:rest-api-changes.html#change-message-info[id] of the change message
 that this comment is linked to.
+|`commit_id` |optional|
+Hex commit SHA1 (40 characters string) of the commit of the patchset to which
+this comment applies.
 |===========================
 
 [[comment-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index f76e0b8..4473a8d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -2011,7 +2011,7 @@
 UserConfigInfo] entity.
 |`default_theme`           |optional|
 URL to a default PolyGerrit UI theme plugin, if available.
-Located in `/static/gerrit-theme.html` by default.
+Located in `/static/gerrit-theme.js` by default.
 |=======================================
 
 [[sshd-info]]
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index 77b180e..ce26280 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -48,6 +48,7 @@
       "id": "delete-project",
       "index_url": "plugins/delete-project/",
       "filename": "delete-project.jar",
+      "api_version": "2.9.3-SNAPSHOT",
       "version": "2.9-SNAPSHOT"
     }
   }
@@ -455,12 +456,13 @@
 
 [options="header",cols="1,^2,4"]
 |=======================
-|Field Name ||Description
-|`id`       ||The ID of the plugin.
-|`version`  ||The version of the plugin.
-|`index_url`|optional|URL of the plugin's default page.
-|`filename` |optional|The plugin's filename.
-|`disabled` |not set if `false`|Whether the plugin is disabled.
+|Field Name   ||Description
+|`id`         ||The ID of the plugin.
+|`version`    ||The version of the plugin.
+|`api_version`|optional|The version of the Gerrit Api used by the plugin.
+|`index_url`  |optional|URL of the plugin's default page.
+|`filename`   |optional|The plugin's filename.
+|`disabled`   |not set if `false`|Whether the plugin is disabled.
 |=======================
 
 [[plugin-input]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 723b45a..ff022c5 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1253,6 +1253,57 @@
   }
 ----
 
+[[create-change]]
+=== Create Change for review.
+
+This endpoint is functionally equivalent to
+link:rest-api-changes.html#create-change[create change in the change
+API], but it has the project name in the URL, which is easier to route
+in sharded deployments.
+
+.Request
+----
+  POST /projects/myProject/create.change HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "subject" : "Let's support 100% Gerrit workflow direct in browser",
+    "branch" : "master",
+    "topic" : "create-change-in-browser",
+    "status" : "NEW"
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that describes
+the resulting change.
+
+.Response
+----
+  HTTP/1.1 201 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "project": "myProject",
+    "branch": "master",
+    "topic": "create-change-in-browser",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+    "subject": "Let's support 100% Gerrit workflow direct in browser",
+    "status": "NEW",
+    "created": "2014-05-05 07:15:44.639000000",
+    "updated": "2014-05-05 07:15:44.639000000",
+    "mergeable": true,
+    "insertions": 0,
+    "deletions": 0,
+    "_number": 4711,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
 [[create-access-change]]
 === Create Access Rights Change for review.
 --
diff --git a/WORKSPACE b/WORKSPACE
index 41d6ef8..63a0852 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -64,8 +64,8 @@
 
 http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "84abf7ac4234a70924628baa9a73a5a5cbad944c4358cf9abdb4aab29c9a5b77",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.7.0/rules_nodejs-1.7.0.tar.gz"],
+    sha256 = "5bf77cc2d13ddf9124f4c1453dd96063774d755d4fc75d922471540d1c9a8ea8",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.0.0/rules_nodejs-2.0.0.tar.gz"],
 )
 
 # Golang support for PolyGerrit local dev server.
@@ -626,18 +626,18 @@
     sha1 = "a3ae34e57fa8a4040e28247291d0cc3d6b8c7bcf",
 )
 
-AUTO_VALUE_VERSION = "1.7.3"
+AUTO_VALUE_VERSION = "1.7.4"
 
 maven_jar(
     name = "auto-value",
     artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
-    sha1 = "cbd30873f839545c7c9264bed61d500bf85bd33e",
+    sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
 )
 
 maven_jar(
     name = "auto-value-annotations",
     artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
-    sha1 = "59ce5ee6aea918f674229f1147da95fdf7f31ce6",
+    sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
 )
 
 declare_nongoogle_deps()
@@ -1003,11 +1003,6 @@
     yarn_lock = "//:plugins/yarn.lock",
 )
 
-# Install all Bazel dependencies needed for npm packages that supply Bazel rules
-load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
-
-install_bazel_dependencies()
-
 load("//tools/bzl:js.bzl", "bower_archive", "npm_binary")
 
 # NPM binaries bundled along with their dependencies.
@@ -1199,8 +1194,4 @@
     version = "6.5.1",
 )
 
-load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace")
-
-ts_setup_workspace()
-
 external_plugin_deps()
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
index 3577a6a..665cc4d 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
@@ -1,6 +1,6 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/changes/",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
     "number": "NUMBER"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json
index 54c54f8..5b892aa 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckMasterBranchReplica1.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT1/a/projects/PROJECT/branches/master"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT1/a/projects/PROJECT/branches/master"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json
index 6210deb..467661b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CheckProjectsCacheFlushEntries.json
@@ -1,6 +1,6 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/config/server/caches/projects",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects",
     "entries": "PROJECTS_ENTRIES"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
index 2389124..30f5f23 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json
@@ -4,7 +4,7 @@
     "cmd": "clone"
   },
   {
-    "url": "http://HOSTNAME:HTTP_PORT/_PROJECT",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/_PROJECT",
     "cmd": "clone"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
index b4ee549..70e79ca 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
@@ -1,6 +1,6 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/changes/",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
     "project": "PROJECT"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
index 40e5a45..cd90739 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateProject.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/projects/PROJECT"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
index 3577a6a..665cc4d 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
@@ -1,6 +1,6 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/changes/",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/",
     "number": "NUMBER"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
index 7cc8293..5720f53 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteProject.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/projects/PROJECT/delete-project~delete"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/delete-project~delete"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json
index 9ff15a7..e30a2cf 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/FlushProjectsCache.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/config/server/caches/projects/flush"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects/flush"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json
index 2b8809a..86a3c28 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetMasterBranchRevision.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/projects/PROJECT/branches/master"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/projects/PROJECT/branches/master"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json
index fcf4bc9..e4e2643 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/GetProjectsCacheEntries.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/config/server/caches/projects"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/config/server/caches/projects"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
index 2389124..30f5f23 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ReplayRecordsFromFeeder.json
@@ -4,7 +4,7 @@
     "cmd": "clone"
   },
   {
-    "url": "http://HOSTNAME:HTTP_PORT/_PROJECT",
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/_PROJECT",
     "cmd": "clone"
   }
 ]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
index a371757..301c65b 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
@@ -1,5 +1,5 @@
 [
   {
-    "url": "http://HOSTNAME:HTTP_PORT/a/changes/"
+    "url": "HTTP_SCHEME://HOSTNAME:HTTP_PORT/a/changes/"
   }
 ]
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
index fc68f97..4832392 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
@@ -62,6 +62,7 @@
       var in = replaceOverride(url.toString)
       in = replaceProperty("hostname", "localhost", in)
       in = replaceProperty("http_port", 8080, in)
+      in = replaceProperty("http_scheme", "http", in)
       replaceProperty("ssh_port", 29418, in)
     case ("number", number) =>
       val precedes = replaceKeyWith("_number", 0, number.toString)
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 9d79855..0cd0402 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -50,19 +50,21 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.EmailHeader;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
@@ -89,8 +91,6 @@
 import com.google.gerrit.index.project.ProjectIndex;
 import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index bb3901e..452df67 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -28,6 +28,10 @@
 import com.google.common.truth.Truth;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
+import com.google.gerrit.entities.EmailHeader.AddressList;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
@@ -36,10 +40,6 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
-import com.google.gerrit.mail.EmailHeader.AddressList;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/acceptance/PushOneCommit.java b/java/com/google/gerrit/acceptance/PushOneCommit.java
index 3ccbe4d..afd451a 100644
--- a/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -25,6 +25,8 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -91,6 +93,14 @@
         @Assisted String subject,
         @Assisted Map<String, String> files);
 
+    @UsedAt(Project.PLUGIN_CODE_OWNERS)
+    PushOneCommit create(
+        PersonIdent i,
+        TestRepository<?> testRepo,
+        @Assisted("subject") String subject,
+        @Assisted Map<String, String> files,
+        @Assisted("changeId") String changeId);
+
     PushOneCommit create(
         PersonIdent i,
         TestRepository<?> testRepo,
@@ -227,15 +237,16 @@
         changeId);
   }
 
-  private PushOneCommit(
+  @AssistedInject
+  PushOneCommit(
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       Provider<InternalChangeQuery> queryProvider,
-      PersonIdent i,
-      TestRepository<?> testRepo,
-      String subject,
-      Map<String, String> files,
-      String changeId)
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
+      @Assisted("subject") String subject,
+      @Assisted Map<String, String> files,
+      @Nullable @Assisted("changeId") String changeId)
       throws Exception {
     this.testRepo = testRepo;
     this.notesFactory = notesFactory;
diff --git a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
index b985e40..0b2282e 100644
--- a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
+++ b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index a7a4a89..e9c0899 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -22,7 +22,7 @@
 import com.google.common.net.InetAddresses;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
 import org.apache.http.client.utils.URIBuilder;
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index a3ddd98..60b3720 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -27,10 +27,10 @@
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestLabelPermission;
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
diff --git a/java/com/google/gerrit/common/UsedAt.java b/java/com/google/gerrit/common/UsedAt.java
index 9f8b255..e38ad9a 100644
--- a/java/com/google/gerrit/common/UsedAt.java
+++ b/java/com/google/gerrit/common/UsedAt.java
@@ -34,6 +34,7 @@
     GOOGLE,
     COLLABNET,
     PLUGIN_CHECKS,
+    PLUGIN_CODE_OWNERS,
     PLUGIN_DELETE_PROJECT,
     PLUGIN_SERVICEUSER,
     PLUGINS_ALL, // Use this project if a method/type is generally made available to all plugins.
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
index 18682e3..0974c47 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -98,7 +98,7 @@
   public abstract static class Builder {
     private final List<Permission.Builder> permissionBuilders;
 
-    public Builder() {
+    protected Builder() {
       permissionBuilders = new ArrayList<>();
     }
 
@@ -161,6 +161,6 @@
 
     protected abstract ImmutableList<Permission> getPermissions();
 
-    protected abstract Builder setPermissions(ImmutableList<Permission> permissions);
+    abstract Builder setPermissions(ImmutableList<Permission> permissions);
   }
 }
diff --git a/java/com/google/gerrit/common/data/ContributorAgreement.java b/java/com/google/gerrit/common/data/ContributorAgreement.java
index ed05203..0f10367 100644
--- a/java/com/google/gerrit/common/data/ContributorAgreement.java
+++ b/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -17,7 +17,9 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
+import java.util.List;
 
 /** Portion of a {@link Project} describing a single contributor agreement. */
 @AutoValue
@@ -71,9 +73,9 @@
 
     public abstract Builder setAgreementUrl(@Nullable String agreementUrl);
 
-    public abstract Builder setExcludeProjectsRegexes(ImmutableList<String> excludeProjectsRegexes);
+    public abstract Builder setExcludeProjectsRegexes(List<String> excludeProjectsRegexes);
 
-    public abstract Builder setMatchProjectsRegexes(ImmutableList<String> matchProjectsRegexes);
+    public abstract Builder setMatchProjectsRegexes(List<String> matchProjectsRegexes);
 
     public abstract ContributorAgreement build();
   }
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index 3e58be7..d272810 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSetApproval;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -228,7 +229,7 @@
 
     public abstract Builder setIgnoreSelfApproval(boolean ignoreSelfApproval);
 
-    public abstract Builder setRefPatterns(@Nullable ImmutableList<String> refPatterns);
+    public abstract Builder setRefPatterns(@Nullable List<String> refPatterns);
 
     public abstract Builder setValues(List<LabelValue> values);
 
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index 93155d5..6190957 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -19,6 +19,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.GroupReference;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
diff --git a/java/com/google/gerrit/common/data/PermissionRule.java b/java/com/google/gerrit/common/data/PermissionRule.java
index c411652..1d2230c 100644
--- a/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/java/com/google/gerrit/common/data/PermissionRule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.GroupReference;
 
 @AutoValue
 public abstract class PermissionRule implements Comparable<PermissionRule> {
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index d841aa6..beb62b4 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -20,8 +20,8 @@
 import com.google.common.truth.FailureMetadata;
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 
 public class GroupReferenceSubject extends Subject {
 
diff --git a/java/com/google/gerrit/mail/Address.java b/java/com/google/gerrit/entities/Address.java
similarity index 98%
rename from java/com/google/gerrit/mail/Address.java
rename to java/com/google/gerrit/entities/Address.java
index 520a4c8..2324330 100644
--- a/java/com/google/gerrit/mail/Address.java
+++ b/java/com/google/gerrit/entities/Address.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.mail;
+package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
diff --git a/java/com/google/gerrit/entities/BUILD b/java/com/google/gerrit/entities/BUILD
index 26265ae..66d1869 100644
--- a/java/com/google/gerrit/entities/BUILD
+++ b/java/com/google/gerrit/entities/BUILD
@@ -16,6 +16,7 @@
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
         "//lib/errorprone:annotations",
+        "//lib/flogger:api",
         "//proto:cache_java_proto",
         "//proto:entities_java_proto",
     ],
diff --git a/java/com/google/gerrit/server/git/BranchOrderSection.java b/java/com/google/gerrit/entities/BranchOrderSection.java
similarity index 96%
rename from java/com/google/gerrit/server/git/BranchOrderSection.java
rename to java/com/google/gerrit/entities/BranchOrderSection.java
index 826067f..f964e59 100644
--- a/java/com/google/gerrit/server/git/BranchOrderSection.java
+++ b/java/com/google/gerrit/entities/BranchOrderSection.java
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.entities;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.entities.RefNames;
 import java.util.Collection;
 
 /**
diff --git a/java/com/google/gerrit/server/project/ConfiguredMimeTypes.java b/java/com/google/gerrit/entities/ConfiguredMimeTypes.java
similarity index 66%
rename from java/com/google/gerrit/server/project/ConfiguredMimeTypes.java
rename to java/com/google/gerrit/entities/ConfiguredMimeTypes.java
index 0447edb..c28a573 100644
--- a/java/com/google/gerrit/server/project/ConfiguredMimeTypes.java
+++ b/java/com/google/gerrit/entities/ConfiguredMimeTypes.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.project;
+package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
+import java.util.Objects;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
@@ -34,7 +35,7 @@
 
   protected abstract ImmutableList<TypeMatcher> matchers();
 
-  static ConfiguredMimeTypes create(String projectName, Config rc) {
+  public static ConfiguredMimeTypes create(String projectName, Config rc) {
     Set<String> types = rc.getSubsections(MIMETYPE);
     ImmutableList.Builder<TypeMatcher> matchers = ImmutableList.builder();
     if (!types.isEmpty()) {
@@ -67,21 +68,31 @@
     return null;
   }
 
-  protected abstract static class TypeMatcher {
+  public abstract static class TypeMatcher {
     private final String type;
+    private final String pattern;
 
-    private TypeMatcher(String type) {
+    private TypeMatcher(String type, String pattern) {
       this.type = type;
+      this.pattern = pattern;
+    }
+
+    public String getPattern() {
+      return pattern;
+    }
+
+    public String getType() {
+      return type;
     }
 
     protected abstract boolean matches(String path);
   }
 
-  protected static class FnType extends TypeMatcher {
+  public static class FnType extends TypeMatcher {
     private final FileNameMatcher matcher;
 
-    private FnType(String type, String pattern) throws InvalidPatternException {
-      super(type);
+    public FnType(String type, String pattern) throws InvalidPatternException {
+      super(type, pattern);
       this.matcher = new FileNameMatcher(pattern, null);
     }
 
@@ -91,13 +102,28 @@
       m.append(input);
       return m.isMatch();
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof FnType)) {
+        return false;
+      }
+      FnType other = (FnType) o;
+      return Objects.equals(other.getType(), getType())
+          && Objects.equals(other.getPattern(), getPattern());
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(getType(), getPattern());
+    }
   }
 
-  protected static class ReType extends TypeMatcher {
+  public static class ReType extends TypeMatcher {
     private final Pattern re;
 
-    private ReType(String type, String pattern) throws PatternSyntaxException {
-      super(type);
+    public ReType(String type, String pattern) throws PatternSyntaxException {
+      super(type, pattern);
       this.re = Pattern.compile(pattern);
     }
 
@@ -105,5 +131,20 @@
     protected boolean matches(String input) {
       return re.matcher(input).matches();
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof ReType)) {
+        return false;
+      }
+      ReType other = (ReType) o;
+      return Objects.equals(other.getType(), getType())
+          && Objects.equals(other.getPattern(), getPattern());
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(getType(), getPattern());
+    }
   }
 }
diff --git a/java/com/google/gerrit/mail/EmailHeader.java b/java/com/google/gerrit/entities/EmailHeader.java
similarity index 99%
rename from java/com/google/gerrit/mail/EmailHeader.java
rename to java/com/google/gerrit/entities/EmailHeader.java
index 9b11101..71414c7 100644
--- a/java/com/google/gerrit/mail/EmailHeader.java
+++ b/java/com/google/gerrit/entities/EmailHeader.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.mail;
+package com.google.gerrit.entities;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
diff --git a/java/com/google/gerrit/common/data/GroupDescription.java b/java/com/google/gerrit/entities/GroupDescription.java
similarity index 93%
rename from java/com/google/gerrit/common/data/GroupDescription.java
rename to java/com/google/gerrit/entities/GroupDescription.java
index ed8b39d..e950257 100644
--- a/java/com/google/gerrit/common/data/GroupDescription.java
+++ b/java/com/google/gerrit/entities/GroupDescription.java
@@ -12,11 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.entities;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.AccountGroup;
 import java.sql.Timestamp;
 import java.util.Set;
 
diff --git a/java/com/google/gerrit/common/data/GroupReference.java b/java/com/google/gerrit/entities/GroupReference.java
similarity index 94%
rename from java/com/google/gerrit/common/data/GroupReference.java
rename to java/com/google/gerrit/entities/GroupReference.java
index 2620138..9185d53 100644
--- a/java/com/google/gerrit/common/data/GroupReference.java
+++ b/java/com/google/gerrit/entities/GroupReference.java
@@ -12,15 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.entities;
 
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.AccountGroup;
 
-/** Describes a group within a projects {@link AccessSection}s. */
+/** Describes a group within a projects {@link com.google.gerrit.common.data.AccessSection}s. */
 @AutoValue
 public abstract class GroupReference implements Comparable<GroupReference> {
 
diff --git a/java/com/google/gerrit/common/data/LabelValue.java b/java/com/google/gerrit/entities/LabelValue.java
similarity index 97%
rename from java/com/google/gerrit/common/data/LabelValue.java
rename to java/com/google/gerrit/entities/LabelValue.java
index ec16fb2..ec5a37e 100644
--- a/java/com/google/gerrit/common/data/LabelValue.java
+++ b/java/com/google/gerrit/entities/LabelValue.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common.data;
+package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
 
diff --git a/java/com/google/gerrit/server/git/NotifyConfig.java b/java/com/google/gerrit/entities/NotifyConfig.java
similarity index 90%
rename from java/com/google/gerrit/server/git/NotifyConfig.java
rename to java/com/google/gerrit/entities/NotifyConfig.java
index 1a1bbb6..17da81f 100644
--- a/java/com/google/gerrit/server/git/NotifyConfig.java
+++ b/java/com/google/gerrit/entities/NotifyConfig.java
@@ -12,17 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.common.Nullable;
 import java.util.EnumSet;
 import java.util.Set;
-import org.eclipse.jgit.annotations.Nullable;
 
 @AutoValue
 public abstract class NotifyConfig implements Comparable<NotifyConfig> {
@@ -32,7 +29,17 @@
     BCC
   }
 
-  @Nullable
+  public enum NotifyType {
+    // sort by name, except 'ALL' which should stay last
+    ABANDONED_CHANGES,
+    ALL_COMMENTS,
+    NEW_CHANGES,
+    NEW_PATCHSETS,
+    SUBMITTED_CHANGES,
+
+    ALL
+  }
+
   public abstract String getName();
 
   public abstract ImmutableSet<NotifyType> getNotify();
diff --git a/java/com/google/gerrit/server/project/StoredCommentLinkInfo.java b/java/com/google/gerrit/entities/StoredCommentLinkInfo.java
similarity index 96%
rename from java/com/google/gerrit/server/project/StoredCommentLinkInfo.java
rename to java/com/google/gerrit/entities/StoredCommentLinkInfo.java
index 4e311b8..ce24d31 100644
--- a/java/com/google/gerrit/server/project/StoredCommentLinkInfo.java
+++ b/java/com/google/gerrit/entities/StoredCommentLinkInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.project;
+package com.google.gerrit.entities;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -68,7 +68,7 @@
   }
 
   /** Creates and returns a new {@link StoredCommentLinkInfo} instance with the same values. */
-  static StoredCommentLinkInfo fromInfo(CommentLinkInfo src, boolean enabled) {
+  public static StoredCommentLinkInfo fromInfo(CommentLinkInfo src, Boolean enabled) {
     return builder(src.name)
         .setMatch(src.match)
         .setLink(src.link)
@@ -79,7 +79,7 @@
   }
 
   /** Returns an {@link CommentLinkInfo} instance with the same values. */
-  CommentLinkInfo toInfo() {
+  public CommentLinkInfo toInfo() {
     CommentLinkInfo info = new CommentLinkInfo();
     info.name = getName();
     info.match = getMatch();
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index d5fbf89..faa9f69 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -39,6 +39,12 @@
   public String message;
   public Boolean unresolved;
 
+  /**
+   * Hex commit SHA1 (as 40 characters hex string) of the commit of the patchset to which this
+   * comment applies.
+   */
+  public String commitId;
+
   public static class Range implements Comparable<Range> {
     private static final Comparator<Range> RANGE_COMPARATOR =
         Comparator.<Range>comparingInt(range -> range.startLine)
@@ -122,7 +128,8 @@
           && Objects.equals(inReplyTo, c.inReplyTo)
           && Objects.equals(updated, c.updated)
           && Objects.equals(message, c.message)
-          && Objects.equals(unresolved, c.unresolved);
+          && Objects.equals(unresolved, c.unresolved)
+          && Objects.equals(commitId, c.commitId);
     }
     return false;
   }
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 212f6da..c6555b9 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -102,6 +102,11 @@
     }
   }
 
+  public enum Theme {
+    DARK,
+    LIGHT
+  }
+
   public enum TimeFormat {
     /** 12-hour clock: 1:15 am, 2:13 pm */
     HHMM_12("h:mm a"),
@@ -125,6 +130,7 @@
   /** Type of download URL the user prefers to use. */
   public String downloadScheme;
 
+  public Theme theme;
   public DateFormat dateFormat;
   public TimeFormat timeFormat;
   public Boolean expandInlineDiffs;
@@ -182,6 +188,7 @@
     GeneralPreferencesInfo p = new GeneralPreferencesInfo();
     p.changesPerPage = DEFAULT_PAGESIZE;
     p.downloadScheme = null;
+    p.theme = Theme.LIGHT;
     p.dateFormat = DateFormat.STD;
     p.timeFormat = TimeFormat.HHMM_12;
     p.expandInlineDiffs = false;
diff --git a/java/com/google/gerrit/extensions/common/PluginInfo.java b/java/com/google/gerrit/extensions/common/PluginInfo.java
index 0df6235..47f9b6a 100644
--- a/java/com/google/gerrit/extensions/common/PluginInfo.java
+++ b/java/com/google/gerrit/extensions/common/PluginInfo.java
@@ -17,13 +17,21 @@
 public class PluginInfo {
   public final String id;
   public final String version;
+  public final String apiVersion;
   public final String indexUrl;
   public final String filename;
   public final Boolean disabled;
 
-  public PluginInfo(String id, String version, String indexUrl, String filename, Boolean disabled) {
+  public PluginInfo(
+      String id,
+      String version,
+      String apiVersion,
+      String indexUrl,
+      String filename,
+      Boolean disabled) {
     this.id = id;
     this.version = version;
+    this.apiVersion = apiVersion;
     this.indexUrl = indexUrl;
     this.filename = filename;
     this.disabled = disabled;
diff --git a/java/com/google/gerrit/mail/MailMessage.java b/java/com/google/gerrit/mail/MailMessage.java
index bb83dfd..2ce6cbb 100644
--- a/java/com/google/gerrit/mail/MailMessage.java
+++ b/java/com/google/gerrit/mail/MailMessage.java
@@ -17,6 +17,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Address;
 import java.time.Instant;
 
 /**
diff --git a/java/com/google/gerrit/mail/RawMailParser.java b/java/com/google/gerrit/mail/RawMailParser.java
index 4e005a5..213cc3f 100644
--- a/java/com/google/gerrit/mail/RawMailParser.java
+++ b/java/com/google/gerrit/mail/RawMailParser.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.io.CharStreams;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Address;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 0333942..ca28255 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -19,9 +19,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.pgm.init.api.AllUsersNameOnInitProvider;
 import com.google.gerrit.pgm.init.api.InitFlags;
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index cf208ae..effb4c6 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -17,8 +17,8 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 846bb82..fff33e5 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -133,6 +133,7 @@
     extractMailExample("NewChange.soy");
     extractMailExample("NewChangeHtml.soy");
     extractMailExample("RegisterNewEmail.soy");
+    extractMailExample("RegisterNewEmailHtml.soy");
     extractMailExample("ReplacePatchSet.soy");
     extractMailExample("ReplacePatchSetHtml.soy");
     extractMailExample("Restored.soy");
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index a831b8e..21ce2d1 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -18,8 +18,8 @@
 
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.common.AccountVisibility;
 import com.google.gerrit.extensions.config.FactoryModule;
diff --git a/java/com/google/gerrit/server/ReviewerByEmailSet.java b/java/com/google/gerrit/server/ReviewerByEmailSet.java
index caae45e..4a317c3 100644
--- a/java/com/google/gerrit/server/ReviewerByEmailSet.java
+++ b/java/com/google/gerrit/server/ReviewerByEmailSet.java
@@ -17,7 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Table;
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import java.sql.Timestamp;
 
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 76d9471..e95bc1c 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -22,9 +22,9 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 1e9914d..b7a54f4 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -20,10 +20,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdNotes;
diff --git a/java/com/google/gerrit/server/account/CachedAccountDetails.java b/java/com/google/gerrit/server/account/CachedAccountDetails.java
index 2eb5770..f23a766 100644
--- a/java/com/google/gerrit/server/account/CachedAccountDetails.java
+++ b/java/com/google/gerrit/server/account/CachedAccountDetails.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.cache.proto.Cache;
@@ -80,7 +81,7 @@
   abstract Account account();
 
   /** Projects that the user has configured to watch. */
-  abstract ImmutableMap<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
+  abstract ImmutableMap<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
       projectWatches();
 
   /** Preferences that this user has. Serialized as Git-config style string. */
@@ -88,7 +89,7 @@
 
   static CachedAccountDetails create(
       Account account,
-      ImmutableMap<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
+      ImmutableMap<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
           projectWatches,
       CachedPreferences preferences) {
     return new AutoValue_CachedAccountDetails(account, projectWatches, preferences);
@@ -115,8 +116,8 @@
               .setMetaId(Strings.nullToEmpty(account.metaId()));
       serialized.setAccount(accountProto);
 
-      for (Map.Entry<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
-          watch : cachedAccountDetails.projectWatches().entrySet()) {
+      for (Map.Entry<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> watch :
+          cachedAccountDetails.projectWatches().entrySet()) {
         Cache.ProjectWatchProto.Builder proto =
             Cache.ProjectWatchProto.newBuilder().setProject(watch.getKey().project().get());
         if (watch.getKey().filter() != null) {
@@ -127,9 +128,7 @@
             .forEach(
                 n ->
                     proto.addNotifyType(
-                        Enums.stringConverter(ProjectWatches.NotifyType.class)
-                            .reverse()
-                            .convert(n)));
+                        Enums.stringConverter(NotifyConfig.NotifyType.class).reverse().convert(n)));
         serialized.addProjectWatchProto(proto);
       }
 
@@ -153,7 +152,7 @@
               .setMetaId(Strings.emptyToNull(proto.getAccount().getMetaId()))
               .build();
 
-      ImmutableMap.Builder<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
+      ImmutableMap.Builder<ProjectWatches.ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
           projectWatches = ImmutableMap.builder();
       proto.getProjectWatchProtoList().stream()
           .forEach(
@@ -162,9 +161,7 @@
                       ProjectWatches.ProjectWatchKey.create(
                           Project.nameKey(p.getProject()), p.getFilter()),
                       p.getNotifyTypeList().stream()
-                          .map(
-                              e ->
-                                  Enums.stringConverter(ProjectWatches.NotifyType.class).convert(e))
+                          .map(e -> Enums.stringConverter(NotifyConfig.NotifyType.class).convert(e))
                           .collect(toImmutableSet())));
 
       return CachedAccountDetails.create(
diff --git a/java/com/google/gerrit/server/account/CapabilityCollection.java b/java/com/google/gerrit/server/account/CapabilityCollection.java
index de522ae..c1a8f73 100644
--- a/java/com/google/gerrit/server/account/CapabilityCollection.java
+++ b/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -20,10 +20,10 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.config.AdministrateServerGroups;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/account/GroupBackend.java b/java/com/google/gerrit/server/account/GroupBackend.java
index 3a874bb..545da6e 100644
--- a/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/java/com/google/gerrit/server/account/GroupBackend.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectState;
diff --git a/java/com/google/gerrit/server/account/GroupBackends.java b/java/com/google/gerrit/server/account/GroupBackends.java
index 1b15512..26b3a82 100644
--- a/java/com/google/gerrit/server/account/GroupBackends.java
+++ b/java/com/google/gerrit/server/account/GroupBackends.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.project.ProjectState;
 import java.util.Collection;
 import java.util.Comparator;
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 64fd7c6..d42db60 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
diff --git a/java/com/google/gerrit/server/account/InternalAccountUpdate.java b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
index bfbe917..4f9202f 100644
--- a/java/com/google/gerrit/server/account/InternalAccountUpdate.java
+++ b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
@@ -20,10 +20,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
diff --git a/java/com/google/gerrit/server/account/InternalGroupBackend.java b/java/com/google/gerrit/server/account/InternalGroupBackend.java
index ddd3da2..c520c96 100644
--- a/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -17,9 +17,9 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.group.InternalGroup;
 import com.google.gerrit.server.group.InternalGroupDescription;
diff --git a/java/com/google/gerrit/server/account/ProjectWatches.java b/java/com/google/gerrit/server/account/ProjectWatches.java
index 6d84f20..42137c1 100644
--- a/java/com/google/gerrit/server/account/ProjectWatches.java
+++ b/java/com/google/gerrit/server/account/ProjectWatches.java
@@ -31,6 +31,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
 import java.util.ArrayList;
@@ -89,17 +90,6 @@
     public abstract @Nullable String filter();
   }
 
-  public enum NotifyType {
-    // sort by name, except 'ALL' which should stay last
-    ABANDONED_CHANGES,
-    ALL_COMMENTS,
-    NEW_CHANGES,
-    NEW_PATCHSETS,
-    SUBMITTED_CHANGES,
-
-    ALL
-  }
-
   public static final String FILTER_ALL = "*";
 
   public static final String WATCH_CONFIG = "watch.config";
@@ -110,7 +100,7 @@
   private final Config cfg;
   private final ValidationError.Sink validationErrorSink;
 
-  private ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches;
+  private ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> projectWatches;
 
   ProjectWatches(Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
     this.accountId = requireNonNull(accountId, "accountId");
@@ -118,7 +108,7 @@
     this.validationErrorSink = requireNonNull(validationErrorSink, "validationErrorSink");
   }
 
-  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
+  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> getProjectWatches() {
     if (projectWatches == null) {
       parse();
     }
@@ -152,9 +142,9 @@
    * @return the parsed project watches
    */
   @VisibleForTesting
-  public static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> parse(
+  public static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> parse(
       Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
-    Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>();
+    Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches = new HashMap<>();
     for (String projectName : cfg.getSubsections(PROJECT)) {
       String[] notifyValues = cfg.getStringList(PROJECT, projectName, KEY_NOTIFY);
       for (String nv : notifyValues) {
@@ -171,7 +161,7 @@
         ProjectWatchKey key =
             ProjectWatchKey.create(Project.nameKey(projectName), notifyValue.filter());
         if (!projectWatches.containsKey(key)) {
-          projectWatches.put(key, EnumSet.noneOf(NotifyType.class));
+          projectWatches.put(key, EnumSet.noneOf(NotifyConfig.NotifyType.class));
         }
         projectWatches.get(key).addAll(notifyValue.notifyTypes());
       }
@@ -179,7 +169,7 @@
     return immutableCopyOf(projectWatches);
   }
 
-  public Config save(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
+  public Config save(Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches) {
     this.projectWatches = immutableCopyOf(projectWatches);
 
     for (String projectName : cfg.getSubsections(PROJECT)) {
@@ -188,7 +178,7 @@
 
     ListMultimap<String, String> notifyValuesByProject =
         MultimapBuilder.hashKeys().arrayListValues().build();
-    for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches.entrySet()) {
+    for (Map.Entry<ProjectWatchKey, Set<NotifyConfig.NotifyType>> e : projectWatches.entrySet()) {
       NotifyValue notifyValue = NotifyValue.create(e.getKey().filter(), e.getValue());
       notifyValuesByProject.put(e.getKey().project().get(), notifyValue.toString());
     }
@@ -200,9 +190,10 @@
     return cfg;
   }
 
-  private static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> immutableCopyOf(
-      Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
-    ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyType>> b = ImmutableMap.builder();
+  private static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
+      immutableCopyOf(Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches) {
+    ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> b =
+        ImmutableMap.builder();
     projectWatches.entrySet().stream()
         .forEach(e -> b.put(e.getKey(), ImmutableSet.copyOf(e.getValue())));
     return b.build();
@@ -231,13 +222,14 @@
         filter = null;
       }
 
-      Set<NotifyType> notifyTypes = EnumSet.noneOf(NotifyType.class);
+      Set<NotifyConfig.NotifyType> notifyTypes = EnumSet.noneOf(NotifyConfig.NotifyType.class);
       if (i + 1 < notifyValue.length() - 2) {
         for (String nt :
             Splitter.on(',')
                 .trimResults()
                 .splitToList(notifyValue.substring(i + 1, notifyValue.length() - 1))) {
-          NotifyType notifyType = Enums.getIfPresent(NotifyType.class, nt).orNull();
+          NotifyConfig.NotifyType notifyType =
+              Enums.getIfPresent(NotifyConfig.NotifyType.class, nt).orNull();
           if (notifyType == null) {
             validationErrorSink.error(
                 ValidationError.create(
@@ -254,18 +246,19 @@
       return create(filter, notifyTypes);
     }
 
-    public static NotifyValue create(@Nullable String filter, Collection<NotifyType> notifyTypes) {
+    public static NotifyValue create(
+        @Nullable String filter, Collection<NotifyConfig.NotifyType> notifyTypes) {
       return new AutoValue_ProjectWatches_NotifyValue(
           Strings.emptyToNull(filter), Sets.immutableEnumSet(notifyTypes));
     }
 
     public abstract @Nullable String filter();
 
-    public abstract ImmutableSet<NotifyType> notifyTypes();
+    public abstract ImmutableSet<NotifyConfig.NotifyType> notifyTypes();
 
     @Override
     public final String toString() {
-      List<NotifyType> notifyTypes = new ArrayList<>(notifyTypes());
+      List<NotifyConfig.NotifyType> notifyTypes = new ArrayList<>(notifyTypes());
       StringBuilder notifyValue = new StringBuilder();
       notifyValue.append(firstNonNull(filter(), FILTER_ALL)).append(" [");
       Joiner.on(", ").appendTo(notifyValue, notifyTypes);
diff --git a/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index fddbd2b..a35b0ac 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -24,9 +24,9 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StartupCheck;
 import com.google.gerrit.server.StartupException;
diff --git a/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java b/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
index 20e8441..e88f6df 100644
--- a/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
@@ -16,9 +16,9 @@
 
 import static com.google.gerrit.util.cli.Localizable.localizable;
 
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
diff --git a/java/com/google/gerrit/server/auth/ldap/FakeLdapGroupBackend.java b/java/com/google/gerrit/server/auth/ldap/FakeLdapGroupBackend.java
index c2123cb..63cd426 100644
--- a/java/com/google/gerrit/server/auth/ldap/FakeLdapGroupBackend.java
+++ b/java/com/google/gerrit/server/auth/ldap/FakeLdapGroupBackend.java
@@ -17,9 +17,9 @@
 import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index e86439a..180612c 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -24,10 +24,10 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 1421f17..b5972e2 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -20,10 +20,10 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.server.account.AbstractRealm;
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/AccessSectionSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/AccessSectionSerializer.java
new file mode 100644
index 0000000..af5fefd
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/AccessSectionSerializer.java
@@ -0,0 +1,45 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class AccessSectionSerializer {
+  public static AccessSection deserialize(Cache.AccessSectionProto proto) {
+    AccessSection.Builder builder = AccessSection.builder(proto.getName());
+    proto.getPermissionsList().stream()
+        .map(PermissionSerializer::deserialize)
+        .map(Permission::toBuilder)
+        .forEach(p -> builder.addPermission(p));
+    return builder.build();
+  }
+
+  public static Cache.AccessSectionProto serialize(AccessSection autoValue) {
+    return Cache.AccessSectionProto.newBuilder()
+        .setName(autoValue.getName())
+        .addAllPermissions(
+            autoValue.getPermissions().stream()
+                .map(PermissionSerializer::serialize)
+                .collect(toImmutableList()))
+        .build();
+  }
+
+  private AccessSectionSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/AddressSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/AddressSerializer.java
new file mode 100644
index 0000000..cc86109
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/AddressSerializer.java
@@ -0,0 +1,37 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.nullToEmpty;
+
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class AddressSerializer {
+  public static Address deserialize(Cache.AddressProto proto) {
+    return Address.create(emptyToNull(proto.getName()), proto.getEmail());
+  }
+
+  public static Cache.AddressProto serialize(Address autoValue) {
+    return Cache.AddressProto.newBuilder()
+        .setName(nullToEmpty(autoValue.name()))
+        .setEmail(autoValue.email())
+        .build();
+  }
+
+  private AddressSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/BUILD b/java/com/google/gerrit/server/cache/serialize/entities/BUILD
index 81f8f6b..5fd28ec 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/BUILD
+++ b/java/com/google/gerrit/server/cache/serialize/entities/BUILD
@@ -6,6 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/git",
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/BranchOrderSectionSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/BranchOrderSectionSerializer.java
new file mode 100644
index 0000000..e86db74
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/BranchOrderSectionSerializer.java
@@ -0,0 +1,33 @@
+// 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.cache.serialize.entities;
+
+import com.google.gerrit.entities.BranchOrderSection;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class BranchOrderSectionSerializer {
+  public static BranchOrderSection deserialize(Cache.BranchOrderSectionProto proto) {
+    return BranchOrderSection.create(proto.getBranchesInOrderList());
+  }
+
+  public static Cache.BranchOrderSectionProto serialize(BranchOrderSection autoValue) {
+    return Cache.BranchOrderSectionProto.newBuilder()
+        .addAllBranchesInOrder(autoValue.order())
+        .build();
+  }
+
+  private BranchOrderSectionSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/ConfiguredMimeTypeSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/ConfiguredMimeTypeSerializer.java
new file mode 100644
index 0000000..6e0c923
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/ConfiguredMimeTypeSerializer.java
@@ -0,0 +1,40 @@
+// 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.cache.serialize.entities;
+
+import com.google.gerrit.entities.ConfiguredMimeTypes;
+import com.google.gerrit.server.cache.proto.Cache;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.InvalidPatternException;
+
+public class ConfiguredMimeTypeSerializer {
+  public static ConfiguredMimeTypes.TypeMatcher deserialize(Cache.ConfiguredMimeTypeProto proto) {
+    try {
+      return proto.getIsRegularExpression()
+          ? new ConfiguredMimeTypes.ReType(proto.getType(), proto.getPattern())
+          : new ConfiguredMimeTypes.FnType(proto.getType(), proto.getPattern());
+    } catch (PatternSyntaxException | InvalidPatternException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  public static Cache.ConfiguredMimeTypeProto serialize(ConfiguredMimeTypes.TypeMatcher value) {
+    return Cache.ConfiguredMimeTypeProto.newBuilder()
+        .setType(value.getType())
+        .setPattern(value.getPattern())
+        .setIsRegularExpression(value instanceof ConfiguredMimeTypes.ReType)
+        .build();
+  }
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/ContributorAgreementSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/ContributorAgreementSerializer.java
new file mode 100644
index 0000000..54d0703
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/ContributorAgreementSerializer.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.cache.serialize.entities;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.nullToEmpty;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class ContributorAgreementSerializer {
+  public static ContributorAgreement deserialize(Cache.ContributorAgreementProto proto) {
+    ContributorAgreement.Builder builder =
+        ContributorAgreement.builder(proto.getName())
+            .setDescription(emptyToNull(proto.getDescription()))
+            .setAccepted(
+                proto.getAcceptedList().stream()
+                    .map(PermissionRuleSerializer::deserialize)
+                    .collect(toImmutableList()))
+            .setAgreementUrl(emptyToNull(proto.getUrl()))
+            .setExcludeProjectsRegexes(proto.getExcludeRegularExpressionsList())
+            .setMatchProjectsRegexes(proto.getMatchRegularExpressionsList());
+    if (proto.hasAutoVerify()) {
+      builder.setAutoVerify(GroupReferenceSerializer.deserialize(proto.getAutoVerify()));
+    }
+    return builder.build();
+  }
+
+  public static Cache.ContributorAgreementProto serialize(ContributorAgreement autoValue) {
+    Cache.ContributorAgreementProto.Builder builder =
+        Cache.ContributorAgreementProto.newBuilder()
+            .setName(autoValue.getName())
+            .setDescription(nullToEmpty(autoValue.getDescription()))
+            .addAllAccepted(
+                autoValue.getAccepted().stream()
+                    .map(PermissionRuleSerializer::serialize)
+                    .collect(toImmutableList()))
+            .setUrl(nullToEmpty(autoValue.getAgreementUrl()))
+            .addAllExcludeRegularExpressions(autoValue.getExcludeProjectsRegexes())
+            .addAllMatchRegularExpressions(autoValue.getMatchProjectsRegexes());
+    if (autoValue.getAutoVerify() != null) {
+      builder.setAutoVerify(GroupReferenceSerializer.serialize(autoValue.getAutoVerify()));
+    }
+    return builder.build();
+  }
+
+  private ContributorAgreementSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/GroupReferenceSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/GroupReferenceSerializer.java
new file mode 100644
index 0000000..c5d4d07
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/GroupReferenceSerializer.java
@@ -0,0 +1,38 @@
+// 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.cache.serialize.entities;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class GroupReferenceSerializer {
+  public static GroupReference deserialize(Cache.GroupReferenceProto proto) {
+    if (!proto.getUuid().isEmpty()) {
+      return GroupReference.create(AccountGroup.uuid(proto.getUuid()), proto.getName());
+    }
+    return GroupReference.create(proto.getName());
+  }
+
+  public static Cache.GroupReferenceProto serialize(GroupReference autoValue) {
+    return Cache.GroupReferenceProto.newBuilder()
+        .setName(autoValue.getName())
+        .setUuid(autoValue.getUUID() == null ? "" : autoValue.getUUID().get())
+        .build();
+  }
+
+  private GroupReferenceSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
new file mode 100644
index 0000000..1566e22
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
@@ -0,0 +1,88 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Shorts;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class LabelTypeSerializer {
+  private static final Converter<String, LabelFunction> FUNCTION_CONVERTER =
+      Enums.stringConverter(LabelFunction.class);
+
+  public static LabelType deserialize(Cache.LabelTypeProto proto) {
+    return LabelType.builder(
+            proto.getName(),
+            proto.getValuesList().stream()
+                .map(LabelValueSerializer::deserialize)
+                .collect(toImmutableList()))
+        .setFunction(FUNCTION_CONVERTER.convert(proto.getFunction()))
+        .setAllowPostSubmit(proto.getAllowPostSubmit())
+        .setIgnoreSelfApproval(proto.getIgnoreSelfApproval())
+        .setDefaultValue(Shorts.saturatedCast(proto.getDefaultValue()))
+        .setCopyAnyScore(proto.getCopyAnyScore())
+        .setCopyMinScore(proto.getCopyMinScore())
+        .setCopyMaxScore(proto.getCopyMaxScore())
+        .setCopyAllScoresOnMergeFirstParentUpdate(proto.getCopyAllScoresOnMergeFirstParentUpdate())
+        .setCopyAllScoresOnTrivialRebase(proto.getCopyAllScoresOnTrivialRebase())
+        .setCopyAllScoresIfNoCodeChange(proto.getCopyAllScoresIfNoCodeChange())
+        .setCopyAllScoresIfNoChange(proto.getCopyAllScoresIfNoChange())
+        .setCopyValues(
+            proto.getCopyValuesList().stream()
+                .map(Shorts::saturatedCast)
+                .collect(toImmutableList()))
+        .setMaxNegative(Shorts.saturatedCast(proto.getMaxNegative()))
+        .setMaxPositive(Shorts.saturatedCast(proto.getMaxPositive()))
+        .setRefPatterns(proto.getRefPatternsList())
+        .build();
+  }
+
+  public static Cache.LabelTypeProto serialize(LabelType autoValue) {
+    return Cache.LabelTypeProto.newBuilder()
+        .setName(autoValue.getName())
+        .addAllValues(
+            autoValue.getValues().stream()
+                .map(LabelValueSerializer::serialize)
+                .collect(toImmutableList()))
+        .setFunction(FUNCTION_CONVERTER.reverse().convert(autoValue.getFunction()))
+        .setCopyAnyScore(autoValue.isCopyAnyScore())
+        .setCopyMinScore(autoValue.isCopyMinScore())
+        .setCopyMaxScore(autoValue.isCopyMaxScore())
+        .setCopyAllScoresOnMergeFirstParentUpdate(
+            autoValue.isCopyAllScoresOnMergeFirstParentUpdate())
+        .setCopyAllScoresOnTrivialRebase(autoValue.isCopyAllScoresOnTrivialRebase())
+        .setCopyAllScoresIfNoCodeChange(autoValue.isCopyAllScoresIfNoCodeChange())
+        .setCopyAllScoresIfNoChange(autoValue.isCopyAllScoresIfNoChange())
+        .addAllCopyValues(
+            autoValue.getCopyValues().stream().map(c -> (int) c).collect(toImmutableList()))
+        .setAllowPostSubmit(autoValue.isAllowPostSubmit())
+        .setIgnoreSelfApproval(autoValue.isIgnoreSelfApproval())
+        .setDefaultValue(Shorts.saturatedCast(autoValue.getDefaultValue()))
+        .setMaxNegative(Shorts.saturatedCast(autoValue.getMaxNegative()))
+        .setMaxPositive(Shorts.saturatedCast(autoValue.getMaxPositive()))
+        .addAllRefPatterns(
+            autoValue.getRefPatterns() == null ? ImmutableList.of() : autoValue.getRefPatterns())
+        .build();
+  }
+
+  private LabelTypeSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/LabelValueSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/LabelValueSerializer.java
new file mode 100644
index 0000000..c1ca9a1
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/LabelValueSerializer.java
@@ -0,0 +1,35 @@
+// 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.cache.serialize.entities;
+
+import com.google.common.primitives.Shorts;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class LabelValueSerializer {
+  public static LabelValue deserialize(Cache.LabelValueProto proto) {
+    return LabelValue.create(Shorts.saturatedCast(proto.getValue()), proto.getText());
+  }
+
+  public static Cache.LabelValueProto serialize(LabelValue autoValue) {
+    return Cache.LabelValueProto.newBuilder()
+        .setText(autoValue.getText())
+        .setValue(autoValue.getValue())
+        .build();
+  }
+
+  private LabelValueSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/NotifyConfigSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/NotifyConfigSerializer.java
new file mode 100644
index 0000000..f0f7d905
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/NotifyConfigSerializer.java
@@ -0,0 +1,79 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.nullToEmpty;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.gerrit.entities.NotifyConfig;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class NotifyConfigSerializer {
+  private static final Converter<String, NotifyConfig.Header> HEADER_CONVERTER =
+      Enums.stringConverter(NotifyConfig.Header.class);
+
+  private static final Converter<String, NotifyConfig.NotifyType> NOTIFY_TYPE_CONVERTER =
+      Enums.stringConverter(NotifyConfig.NotifyType.class);
+
+  public static NotifyConfig deserialize(Cache.NotifyConfigProto proto) {
+    NotifyConfig.Builder builder =
+        NotifyConfig.builder()
+            .setName(emptyToNull(proto.getName()))
+            .setNotify(
+                proto.getTypeList().stream()
+                    .map(t -> NOTIFY_TYPE_CONVERTER.convert(t))
+                    .collect(toImmutableSet()))
+            .setFilter(emptyToNull(proto.getFilter()))
+            .setHeader(
+                proto.getHeader().isEmpty() ? null : HEADER_CONVERTER.convert(proto.getHeader()));
+    proto.getGroupsList().stream()
+        .map(GroupReferenceSerializer::deserialize)
+        .forEach(g -> builder.addGroup(g));
+    proto.getAddressesList().stream()
+        .map(AddressSerializer::deserialize)
+        .forEach(a -> builder.addAddress(a));
+    return builder.build();
+  }
+
+  public static Cache.NotifyConfigProto serialize(NotifyConfig autoValue) {
+    return Cache.NotifyConfigProto.newBuilder()
+        .setName(nullToEmpty(autoValue.getName()))
+        .addAllType(
+            autoValue.getNotify().stream()
+                .map(t -> NOTIFY_TYPE_CONVERTER.reverse().convert(t))
+                .collect(toImmutableSet()))
+        .setFilter(nullToEmpty(autoValue.getFilter()))
+        .setHeader(
+            autoValue.getHeader() == null
+                ? ""
+                : HEADER_CONVERTER.reverse().convert(autoValue.getHeader()))
+        .addAllGroups(
+            autoValue.getGroups().stream()
+                .map(GroupReferenceSerializer::serialize)
+                .collect(toImmutableSet()))
+        .addAllAddresses(
+            autoValue.getAddresses().stream()
+                .map(AddressSerializer::serialize)
+                .collect(toImmutableList()))
+        .build();
+  }
+
+  private NotifyConfigSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/PermissionRuleSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/PermissionRuleSerializer.java
new file mode 100644
index 0000000..d310f18
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/PermissionRuleSerializer.java
@@ -0,0 +1,47 @@
+// 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.cache.serialize.entities;
+
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class PermissionRuleSerializer {
+  private static final Converter<String, PermissionRule.Action> ACTION_CONVERTER =
+      Enums.stringConverter(PermissionRule.Action.class);
+
+  public static PermissionRule deserialize(Cache.PermissionRuleProto proto) {
+    return PermissionRule.builder(GroupReferenceSerializer.deserialize(proto.getGroup()))
+        .setAction(ACTION_CONVERTER.convert(proto.getAction()))
+        .setForce(proto.getForce())
+        .setMin(proto.getMin())
+        .setMax(proto.getMax())
+        .build();
+  }
+
+  public static Cache.PermissionRuleProto serialize(PermissionRule autoValue) {
+    return Cache.PermissionRuleProto.newBuilder()
+        .setAction(ACTION_CONVERTER.reverse().convert(autoValue.getAction()))
+        .setForce(autoValue.getForce())
+        .setMin(autoValue.getMin())
+        .setMax(autoValue.getMax())
+        .setGroup(GroupReferenceSerializer.serialize(autoValue.getGroup()))
+        .build();
+  }
+
+  private PermissionRuleSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/PermissionSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/PermissionSerializer.java
new file mode 100644
index 0000000..983d926
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/PermissionSerializer.java
@@ -0,0 +1,47 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class PermissionSerializer {
+  public static Permission deserialize(Cache.PermissionProto proto) {
+    Permission.Builder builder =
+        Permission.builder(proto.getName()).setExclusiveGroup(proto.getExclusiveGroup());
+    proto.getRulesList().stream()
+        .map(PermissionRuleSerializer::deserialize)
+        .map(PermissionRule::toBuilder)
+        .forEach(rule -> builder.add(rule));
+    return builder.build();
+  }
+
+  public static Cache.PermissionProto serialize(Permission autoValue) {
+    return Cache.PermissionProto.newBuilder()
+        .setName(autoValue.getName())
+        .setExclusiveGroup(autoValue.getExclusiveGroup())
+        .addAllRules(
+            autoValue.getRules().stream()
+                .map(PermissionRuleSerializer::serialize)
+                .collect(toImmutableList()))
+        .build();
+  }
+
+  private PermissionSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/StoredCommentLinkInfoSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/StoredCommentLinkInfoSerializer.java
new file mode 100644
index 0000000..d7bd373
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/StoredCommentLinkInfoSerializer.java
@@ -0,0 +1,47 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.nullToEmpty;
+
+import com.google.gerrit.entities.StoredCommentLinkInfo;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class StoredCommentLinkInfoSerializer {
+  public static StoredCommentLinkInfo deserialize(Cache.StoredCommentLinkInfoProto proto) {
+    return StoredCommentLinkInfo.builder(proto.getName())
+        .setMatch(emptyToNull(proto.getMatch()))
+        .setLink(emptyToNull(proto.getLink()))
+        .setHtml(emptyToNull(proto.getHtml()))
+        .setEnabled(proto.getEnabled())
+        .setOverrideOnly(proto.getOverrideOnly())
+        .build();
+  }
+
+  public static Cache.StoredCommentLinkInfoProto serialize(StoredCommentLinkInfo autoValue) {
+    return Cache.StoredCommentLinkInfoProto.newBuilder()
+        .setName(autoValue.getName())
+        .setMatch(nullToEmpty(autoValue.getMatch()))
+        .setLink(nullToEmpty(autoValue.getLink()))
+        .setHtml(nullToEmpty(autoValue.getHtml()))
+        .setEnabled(autoValue.getEnabled())
+        .setOverrideOnly(autoValue.getOverrideOnly())
+        .build();
+  }
+
+  private StoredCommentLinkInfoSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/SubscribeSectionSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/SubscribeSectionSerializer.java
new file mode 100644
index 0000000..6125818d
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/SubscribeSectionSerializer.java
@@ -0,0 +1,40 @@
+// 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.cache.serialize.entities;
+
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.cache.proto.Cache;
+
+/** Helper to (de)serialize values for caches. */
+public class SubscribeSectionSerializer {
+  public static SubscribeSection deserialize(Cache.SubscribeSectionProto proto) {
+    SubscribeSection.Builder builder =
+        SubscribeSection.builder(Project.nameKey(proto.getProjectName()));
+    proto.getMatchingRefSpecsList().forEach(rs -> builder.addMatchingRefSpec(rs));
+    proto.getMultiMatchRefSpecsList().forEach(rs -> builder.addMultiMatchRefSpec(rs));
+    return builder.build();
+  }
+
+  public static Cache.SubscribeSectionProto serialize(SubscribeSection autoValue) {
+    Cache.SubscribeSectionProto.Builder builder =
+        Cache.SubscribeSectionProto.newBuilder().setProjectName(autoValue.project().get());
+    autoValue.multiMatchRefSpecsAsString().forEach(rs -> builder.addMultiMatchRefSpecs(rs));
+    autoValue.matchingRefSpecsAsString().forEach(rs -> builder.addMatchingRefSpecs(rs));
+    return builder.build();
+  }
+
+  private SubscribeSectionSerializer() {}
+}
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index 9e228d9..6c39ed0 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -114,14 +114,16 @@
   public void postUpdate(Context ctx) {
     NotifyResolver.Result notify = ctx.getNotify(change.getId());
     try {
-      ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId());
+      ReplyToChangeSender emailSender =
+          abandonedSenderFactory.create(ctx.getProject(), change.getId());
       if (accountState != null) {
-        cm.setFrom(accountState.account().id());
+        emailSender.setFrom(accountState.account().id());
       }
-      cm.setChangeMessage(message.getMessage(), ctx.getWhen());
-      cm.setNotify(notify);
-      cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
-      cm.send();
+      emailSender.setChangeMessage(message.getMessage(), ctx.getWhen());
+      emailSender.setNotify(notify);
+      emailSender.setMessageId(
+          messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
+      emailSender.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 ae3851e..4a3f638 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -19,9 +19,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SendEmailExecutor;
 import com.google.gerrit.server.mail.send.AddReviewerSender;
@@ -83,17 +83,18 @@
         sendEmailsExecutor.submit(
             () -> {
               try {
-                AddReviewerSender cm = addReviewerSenderFactory.create(projectNameKey, cId);
-                cm.setNotify(notify);
-                cm.setFrom(userId);
-                cm.addReviewers(immutableToMail);
-                cm.addReviewersByEmail(immutableAddedByEmail);
-                cm.addExtraCC(immutableToCopy);
-                cm.addExtraCCByEmail(immutableCopiedByEmail);
-                cm.setMessageId(
+                AddReviewerSender emailSender =
+                    addReviewerSenderFactory.create(projectNameKey, cId);
+                emailSender.setNotify(notify);
+                emailSender.setFrom(userId);
+                emailSender.addReviewers(immutableToMail);
+                emailSender.addReviewersByEmail(immutableAddedByEmail);
+                emailSender.addExtraCC(immutableToCopy);
+                emailSender.addExtraCCByEmail(immutableCopiedByEmail);
+                emailSender.setMessageId(
                     messageIdGenerator.fromChangeUpdate(
                         change.getProject(), change.currentPatchSetId()));
-                cm.send();
+                emailSender.send();
               } catch (Exception err) {
                 logger.atSevere().withCause(err).log(
                     "Cannot send email to new reviewers of change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index 7b87a29..ff8e5c6 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -29,12 +29,12 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 467c4a2..b749270 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -465,23 +465,24 @@
             @Override
             public void run() {
               try {
-                CreateChangeSender cm =
+                CreateChangeSender emailSender =
                     createChangeSenderFactory.create(change.getProject(), change.getId());
-                cm.setFrom(change.getOwner());
-                cm.setPatchSet(patchSet, patchSetInfo);
-                cm.setNotify(notify);
-                cm.addReviewers(
+                emailSender.setFrom(change.getOwner());
+                emailSender.setPatchSet(patchSet, patchSetInfo);
+                emailSender.setNotify(notify);
+                emailSender.addReviewers(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
                         .map(PatchSetApproval::accountId)
                         .collect(toImmutableSet()));
-                cm.addReviewersByEmail(
+                emailSender.addReviewersByEmail(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewersByEmail));
-                cm.addExtraCC(reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
-                cm.addExtraCCByEmail(
+                emailSender.addExtraCC(
+                    reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
+                emailSender.addExtraCCByEmail(
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCsByEmail));
-                cm.setMessageId(
+                emailSender.setMessageId(
                     messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
-                cm.send();
+                emailSender.send();
               } catch (Exception e) {
                 logger.atSevere().withCause(e).log(
                     "Cannot send email for new change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index d0e9288..31df6a4 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.entities.PatchSet;
@@ -73,7 +74,6 @@
 import com.google.gerrit.extensions.common.TrackingIdInfo;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.index.query.QueryResult;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.Description.Units;
 import com.google.gerrit.metrics.MetricMaker;
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index e52375f..255e13a 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.entities.PatchSet;
-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;
@@ -79,15 +79,15 @@
       if (!notify.shouldNotify()) {
         return;
       }
-      DeleteReviewerSender cm =
+      DeleteReviewerSender emailSender =
           deleteReviewerSenderFactory.create(ctx.getProject(), change.getId());
-      cm.setFrom(ctx.getAccountId());
-      cm.addReviewersByEmail(Collections.singleton(reviewer));
-      cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
-      cm.setNotify(notify);
-      cm.setMessageId(
+      emailSender.setFrom(ctx.getAccountId());
+      emailSender.addReviewersByEmail(Collections.singleton(reviewer));
+      emailSender.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+      emailSender.setNotify(notify);
+      emailSender.setMessageId(
           messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-      cm.send();
+      emailSender.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 5a9fb99..68d9184 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -224,12 +224,14 @@
       // The user knows they removed themselves, don't bother emailing them.
       return;
     }
-    DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
-    cm.setFrom(userId);
-    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();
+    DeleteReviewerSender emailSender =
+        deleteReviewerSenderFactory.create(projectName, change.getId());
+    emailSender.setFrom(userId);
+    emailSender.addReviewers(Collections.singleton(reviewer.account().id()));
+    emailSender.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+    emailSender.setNotify(notify);
+    emailSender.setMessageId(
+        messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
+    emailSender.send();
   }
 }
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index b30a6a3..fe254e0 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -128,16 +128,17 @@
   public void run() {
     RequestContext old = requestContext.setContext(this);
     try {
-      CommentSender cm = commentSenderFactory.create(notes.getProjectName(), notes.getChangeId());
-      cm.setFrom(user.getAccountId());
-      cm.setPatchSet(patchSet, patchSetInfoFactory.get(notes.getProjectName(), patchSet));
-      cm.setChangeMessage(message.getMessage(), message.getWrittenOn());
-      cm.setComments(comments);
-      cm.setPatchSetComment(patchSetComment);
-      cm.setLabels(labels);
-      cm.setNotify(notify);
-      cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, patchSet.id()));
-      cm.send();
+      CommentSender emailSender =
+          commentSenderFactory.create(notes.getProjectName(), notes.getChangeId());
+      emailSender.setFrom(user.getAccountId());
+      emailSender.setPatchSet(patchSet, patchSetInfoFactory.get(notes.getProjectName(), patchSet));
+      emailSender.setChangeMessage(message.getMessage(), message.getWrittenOn());
+      emailSender.setComments(comments);
+      emailSender.setPatchSetComment(patchSetComment);
+      emailSender.setLabels(labels);
+      emailSender.setNotify(notify);
+      emailSender.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, patchSet.id()));
+      emailSender.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.id());
     } finally {
diff --git a/java/com/google/gerrit/server/change/LabelNormalizer.java b/java/com/google/gerrit/server/change/LabelNormalizer.java
index 67cd0df..619b939 100644
--- a/java/com/google/gerrit/server/change/LabelNormalizer.java
+++ b/java/com/google/gerrit/server/change/LabelNormalizer.java
@@ -24,8 +24,8 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 739e263..d9e81b1 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -34,9 +34,9 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index d4d74a3..882352d 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -288,15 +288,17 @@
     if (notify.shouldNotify() && sendEmail) {
       requireNonNull(changeMessage);
       try {
-        ReplacePatchSetSender cm = replacePatchSetFactory.create(ctx.getProject(), change.getId());
-        cm.setFrom(ctx.getAccountId());
-        cm.setPatchSet(patchSet, patchSetInfo);
-        cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
-        cm.addReviewers(oldReviewers.byState(REVIEWER));
-        cm.addExtraCC(oldReviewers.byState(CC));
-        cm.setNotify(notify);
-        cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
-        cm.send();
+        ReplacePatchSetSender emailSender =
+            replacePatchSetFactory.create(ctx.getProject(), change.getId());
+        emailSender.setFrom(ctx.getAccountId());
+        emailSender.setPatchSet(patchSet, patchSetInfo);
+        emailSender.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
+        emailSender.addReviewers(oldReviewers.byState(REVIEWER));
+        emailSender.addExtraCC(oldReviewers.byState(CC));
+        emailSender.setNotify(notify);
+        emailSender.setMessageId(
+            messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
+        emailSender.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
             "Cannot send email for new patch set on change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index d9462bf..c271651 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -31,12 +31,13 @@
 import com.google.common.collect.Streams;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -48,7 +49,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/change/ReviewerJson.java b/java/com/google/gerrit/server/change/ReviewerJson.java
index 39e5f74..d493fd0 100644
--- a/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.gerrit.common.data.LabelValue.formatValue;
+import static com.google.gerrit.entities.LabelValue.formatValue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
@@ -22,11 +22,11 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.permissions.LabelPermission;
diff --git a/java/com/google/gerrit/server/change/ReviewerResource.java b/java/com/google/gerrit/server/change/ReviewerResource.java
index df0a03f..7a98f2b 100644
--- a/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -18,10 +18,10 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.Assisted;
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index 74536aa..411c9b6 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -122,13 +122,13 @@
   @Override
   public void postUpdate(Context ctx) {
     try {
-      SetAssigneeSender cm =
+      SetAssigneeSender emailSender =
           setAssigneeSenderFactory.create(
               change.getProject(), change.getId(), newAssignee.getAccountId());
-      cm.setFrom(user.get().getAccountId());
-      cm.setMessageId(
+      emailSender.setFrom(user.get().getAccountId());
+      emailSender.setMessageId(
           messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-      cm.send();
+      emailSender.send();
     } catch (Exception err) {
       logger.atSevere().withCause(err).log(
           "Cannot send email to new assignee of change %s", change.getId());
diff --git a/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java b/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
index d6e61c4..64937db 100644
--- a/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
+++ b/java/com/google/gerrit/server/config/AdministrateServerGroupsProvider.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.util.RequestContext;
diff --git a/java/com/google/gerrit/server/config/GerritIsReplica.java b/java/com/google/gerrit/server/config/GerritIsReplica.java
new file mode 100644
index 0000000..154fdcd
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritIsReplica.java
@@ -0,0 +1,25 @@
+// 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 java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/* Marker on {@link Boolean} indicating whether Gerrit is run as a read-only replica. */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface GerritIsReplica {}
diff --git a/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
new file mode 100644
index 0000000..bd07f7d
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritIsReplicaProvider.java
@@ -0,0 +1,46 @@
+// 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 com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Provides {@link Boolean} annotated with {@link GerritIsReplica}.
+ *
+ * <p>The returned boolean indicates whether Gerrit is run as a read-only replica.
+ */
+@Singleton
+public final class GerritIsReplicaProvider implements Provider<Boolean> {
+  public static final String CONFIG_SECTION = "container";
+  public static final String REPLICA_KEY = "replica";
+  public static final String DEPRECATED_REPLICA_KEY = "slave";
+
+  public final boolean isReplica;
+
+  @Inject
+  public GerritIsReplicaProvider(@GerritServerConfig Config config) {
+    this.isReplica =
+        config.getBoolean(CONFIG_SECTION, REPLICA_KEY, false)
+            || config.getBoolean(CONFIG_SECTION, DEPRECATED_REPLICA_KEY, false);
+  }
+
+  @Override
+  public Boolean get() {
+    return isReplica;
+  }
+}
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index 25ee759..3777a55 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -78,5 +78,8 @@
         .annotatedWith(GerritServerConfig.class)
         .toProvider(GerritServerConfigProvider.class);
     bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
+    bind(Boolean.class)
+        .annotatedWith(GerritIsReplica.class)
+        .toProvider(GerritIsReplicaProvider.class);
   }
 }
diff --git a/java/com/google/gerrit/server/config/GroupSetProvider.java b/java/com/google/gerrit/server/config/GroupSetProvider.java
index 7f487e1..025946d 100644
--- a/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.util.RequestContext;
diff --git a/java/com/google/gerrit/server/config/PluginConfig.java b/java/com/google/gerrit/server/config/PluginConfig.java
index f41d5c2..0eebd98 100644
--- a/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/java/com/google/gerrit/server/config/PluginConfig.java
@@ -17,7 +17,7 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
 import java.util.Arrays;
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 329530c..0f46199 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -309,12 +309,12 @@
     public void postUpdate(Context ctx) throws Exception {
       changeReverted.fire(change, ins.getChange(), ctx.getWhen());
       try {
-        RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
-        cm.setFrom(ctx.getAccountId());
-        cm.setNotify(ctx.getNotify(change.getId()));
-        cm.setMessageId(
+        RevertedSender emailSender = revertedSenderFactory.create(ctx.getProject(), change.getId());
+        emailSender.setFrom(ctx.getAccountId());
+        emailSender.setNotify(ctx.getNotify(change.getId()));
+        emailSender.setMessageId(
             messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-        cm.send();
+        emailSender.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
             "Cannot send email for revert change %s", change.getId());
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index 89d6bd3..40e2730 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -189,13 +189,13 @@
                   @Override
                   public void run() {
                     try {
-                      MergedSender cm =
+                      MergedSender emailSender =
                           mergedSenderFactory.create(ctx.getProject(), psId.changeId());
-                      cm.setFrom(ctx.getAccountId());
-                      cm.setPatchSet(patchSet, info);
-                      cm.setMessageId(
+                      emailSender.setFrom(ctx.getAccountId());
+                      emailSender.setPatchSet(patchSet, info);
+                      emailSender.setMessageId(
                           messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
-                      cm.send();
+                      emailSender.send();
                     } catch (Exception e) {
                       logger.atSevere().withCause(e).log(
                           "Cannot send email for submitted patch set %s", psId);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 3dc8961..5d36e70 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -1005,7 +1005,15 @@
   private String buildError(String error, List<String> branches) {
     StringBuilder sb = new StringBuilder();
     if (branches.size() == 1) {
-      sb.append("branch ").append(branches.get(0)).append(":\n");
+      String branch = branches.get(0);
+      sb.append("branch ").append(branch).append(":\n");
+      // As of 2020, there are still many git-review <1.27 installations in the wild.
+      // These users will see failures as their old git-review assumes that
+      // `refs/publish/...` is still magic, which it isn't. As Gerrit's default error messages are
+      // misleading for these users, we hint them at upgrading their git-review.
+      if (branch.startsWith("refs/publish/")) {
+        sb.append("If you are using git-review, update to at least git-review 1.27. Otherwise:\n");
+      }
       sb.append(error);
       return sb.toString();
     }
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index f5c69e0..b1cb2f9 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -520,26 +520,27 @@
     @Override
     public void run() {
       try {
-        ReplacePatchSetSender cm =
+        ReplacePatchSetSender emailSender =
             replacePatchSetFactory.create(projectState.getNameKey(), notes.getChangeId());
-        cm.setFrom(ctx.getAccount().account().id());
-        cm.setPatchSet(newPatchSet, info);
-        cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
-        cm.setNotify(ctx.getNotify(notes.getChangeId()));
-        cm.addReviewers(
+        emailSender.setFrom(ctx.getAccount().account().id());
+        emailSender.setPatchSet(newPatchSet, info);
+        emailSender.setChangeMessage(msg.getMessage(), ctx.getWhen());
+        emailSender.setNotify(ctx.getNotify(notes.getChangeId()));
+        emailSender.addReviewers(
             Streams.concat(
                     oldRecipients.getReviewers().stream(),
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
                         .map(PatchSetApproval::accountId))
                 .collect(toImmutableSet()));
-        cm.addExtraCC(
+        emailSender.addExtraCC(
             Streams.concat(
                     oldRecipients.getCcOnly().stream(),
                     reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs).stream())
                 .collect(toImmutableSet()));
-        cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSetId));
+        emailSender.setMessageId(
+            messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSetId));
         // TODO(dborowitz): Support byEmail
-        cm.send();
+        emailSender.send();
       } catch (Exception e) {
         logger.atSevere().withCause(e).log(
             "Cannot send email for new patch set %s", newPatchSet.id());
diff --git a/java/com/google/gerrit/server/group/GroupResolver.java b/java/com/google/gerrit/server/group/GroupResolver.java
index 1aa265b..50ec893 100644
--- a/java/com/google/gerrit/server/group/GroupResolver.java
+++ b/java/com/google/gerrit/server/group/GroupResolver.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
diff --git a/java/com/google/gerrit/server/group/GroupResource.java b/java/com/google/gerrit/server/group/GroupResource.java
index 1050314..b0e81ec 100644
--- a/java/com/google/gerrit/server/group/GroupResource.java
+++ b/java/com/google/gerrit/server/group/GroupResource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.account.GroupControl;
diff --git a/java/com/google/gerrit/server/group/InternalGroupDescription.java b/java/com/google/gerrit/server/group/InternalGroupDescription.java
index c70c8bf..740557a 100644
--- a/java/com/google/gerrit/server/group/InternalGroupDescription.java
+++ b/java/com/google/gerrit/server/group/InternalGroupDescription.java
@@ -19,9 +19,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import java.sql.Timestamp;
 
 public class InternalGroupDescription implements GroupDescription.Internal {
diff --git a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
index b2d9632..cae213f 100644
--- a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -19,8 +19,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.AllUsersName;
diff --git a/java/com/google/gerrit/server/group/SubgroupResource.java b/java/com/google/gerrit/server/group/SubgroupResource.java
index ceea2dc..21356be 100644
--- a/java/com/google/gerrit/server/group/SubgroupResource.java
+++ b/java/com/google/gerrit/server/group/SubgroupResource.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
 
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index a446718..b5ccb18 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -21,9 +21,9 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.StartupCheck;
 import com.google.gerrit.server.StartupException;
diff --git a/java/com/google/gerrit/server/group/db/AuditLogFormatter.java b/java/com/google/gerrit/server/group/db/AuditLogFormatter.java
index ec4c0fc..235ca4f 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogFormatter.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogFormatter.java
@@ -19,9 +19,9 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index b75670d..cdba81f 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -28,8 +28,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index 163b9c6..90a5a1f 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -17,10 +17,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.AccountGroupByIdAudit;
 import com.google.gerrit.entities.AccountGroupMemberAudit;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.InternalGroup;
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 0414304..35f5dea 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -23,8 +23,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
diff --git a/java/com/google/gerrit/server/group/db/RenameGroupOp.java b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
index b7564e2..45dcdfc 100644
--- a/java/com/google/gerrit/server/group/db/RenameGroupOp.java
+++ b/java/com/google/gerrit/server/group/db/RenameGroupOp.java
@@ -17,8 +17,8 @@
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.DefaultQueueOp;
 import com.google.gerrit.server.git.WorkQueue;
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
index 51c7ca3..8b7055e 100644
--- a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -18,9 +18,9 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index a7c4016..160ac14 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
@@ -59,7 +60,6 @@
 import com.google.gerrit.index.RefState;
 import com.google.gerrit.index.SchemaUtil;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index 51c7730..2d77f61 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -22,8 +22,8 @@
 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.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.index.SiteIndexer;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.InternalGroup;
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index b38bef6..bdc933f 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -224,10 +224,10 @@
 
   private void sendRejectionEmail(MailMessage message, InboundEmailRejectionSender.Error reason) {
     try {
-      InboundEmailRejectionSender em =
+      InboundEmailRejectionSender emailSender =
           emailRejectionSender.create(message.from(), message.id(), reason);
-      em.setMessageId(messageIdGenerator.fromMailMessage(message));
-      em.send();
+      emailSender.setMessageId(messageIdGenerator.fromMailMessage(message));
+      emailSender.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot send email to warn for an error");
     }
diff --git a/java/com/google/gerrit/server/mail/send/AbandonedSender.java b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
index a6fb4de..3ac610d 100644
--- a/java/com/google/gerrit/server/mail/send/AbandonedSender.java
+++ b/java/com/google/gerrit/server/mail/send/AbandonedSender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -51,9 +51,4 @@
       appendHtml(soyHtmlTemplate("AbandonedHtml"));
     }
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index e02f02a..0d447ca 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
 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.account.AccountSshKey;
 import com.google.inject.assistedinject.Assisted;
@@ -99,11 +99,6 @@
     soyContextEmailData.put("userNameEmail", getUserNameEmailFor(user.getAccountId()));
   }
 
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
-
   private String getEmail() {
     return user.getAccount().preferredEmail();
   }
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 22d332a..1e984c1 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetInfo;
@@ -33,7 +34,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchList;
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 0ba5da51..7d5f3fa 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.KeyUtil;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RobotComment;
@@ -34,7 +35,6 @@
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.CommentsUtil;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.receive.Protocol;
 import com.google.gerrit.server.patch.PatchFile;
@@ -572,9 +572,4 @@
     return MailProcessingUtil.rfcDateformatter.format(
         ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")));
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index 1f58abb..b78dc62 100644
--- a/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -17,11 +17,11 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.RefPermission;
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index ce336ff..46e7fd8 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.common.base.Joiner;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
 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.account.AccountSshKey;
 import com.google.inject.assistedinject.Assisted;
@@ -97,11 +97,6 @@
     soyContextEmailData.put("userNameEmail", getUserNameEmailFor(user.getAccountId()));
   }
 
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
-
   private String getEmail() {
     return user.getAccount().preferredEmail();
   }
diff --git a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index 4f42679..d5863a6 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -93,9 +93,4 @@
     super.setupSoyContext();
     soyContextEmailData.put("reviewerNames", getReviewerNames());
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
index 76f9b81..77efbf8 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -51,9 +51,4 @@
       appendHtml(soyHtmlTemplate("DeleteVoteHtml"));
     }
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/EmailSender.java b/java/com/google/gerrit/server/mail/send/EmailSender.java
index 9b3a1f7..711ab1b 100644
--- a/java/com/google/gerrit/server/mail/send/EmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/EmailSender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import java.util.Collection;
 import java.util.Map;
 
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java b/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
index 61fa50d..a6d4f6d 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 
 /** Constructs an address to send email from. */
 public interface FromAddressGenerator {
diff --git a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
index dfaabbe..ecf808d 100644
--- a/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
+++ b/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -19,7 +19,7 @@
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index bca5338..c1c2f31 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail.send;
 
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
 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;
@@ -76,11 +76,6 @@
     soyContextEmailData.put("operation", operation);
   }
 
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
-
   private String getEmail() {
     return user.getAccount().preferredEmail();
   }
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
index 110f26a..709bf61 100644
--- a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -16,9 +16,9 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.MailHeader;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -90,9 +90,4 @@
     super.setupSoyContext();
     footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
index 92220eb..07ca254 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
@@ -69,6 +69,7 @@
     "NoReplyFooterHtml.soy",
     "Private.soy",
     "RegisterNewEmail.soy",
+    "RegisterNewEmailHtml.soy",
     "ReplacePatchSet.soy",
     "ReplacePatchSetHtml.soy",
     "Restored.soy",
diff --git a/java/com/google/gerrit/server/mail/send/MergedSender.java b/java/com/google/gerrit/server/mail/send/MergedSender.java
index b28a4dc..6ee6c68 100644
--- a/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -18,14 +18,14 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -131,9 +131,4 @@
     super.setupSoyContext();
     soyContextEmailData.put("approvals", getApprovals());
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index d81dca4..0e97f7e 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -102,9 +102,4 @@
     soyContext.put("ownerName", getNameFor(change.getOwner()));
     soyContextEmailData.put("reviewerNames", getReviewerNames());
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 0fb5c6f..5ffd928 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -18,13 +18,13 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.mail.MailHeader;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
 import java.util.HashMap;
 import java.util.Map;
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 8f63177..1eb274b 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -22,14 +22,14 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
+import com.google.gerrit.entities.EmailHeader.AddressList;
 import com.google.gerrit.entities.UserIdentity;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
-import com.google.gerrit.mail.EmailHeader.AddressList;
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.NotifyResolver;
@@ -631,11 +631,6 @@
   }
 
   protected final boolean useHtml() {
-    return args.settings.html && supportsHtml();
-  }
-
-  /** Override this method to enable HTML in a subclass. */
-  protected boolean supportsHtml() {
-    return false;
+    return args.settings.html;
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index e67ff01..0514337 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -17,20 +17,19 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.NotifyConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
-import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -61,13 +60,15 @@
   }
 
   /** Returns all watchers that are relevant */
-  public final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig) {
+  public final Watchers getWatchers(
+      NotifyConfig.NotifyType type, boolean includeWatchersFromNotifyConfig) {
     Watchers matching = new Watchers();
     Set<Account.Id> projectWatchers = new HashSet<>();
 
     for (AccountState a : args.accountQueryProvider.get().byWatchedProject(project)) {
       Account.Id accountId = a.account().id();
-      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e : a.projectWatches().entrySet()) {
+      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> e :
+          a.projectWatches().entrySet()) {
         if (project.equals(e.getKey().project())
             && add(matching, accountId, e.getKey(), e.getValue(), type)) {
           // We only want to prevent matching All-Projects if this filter hits
@@ -77,7 +78,8 @@
     }
 
     for (AccountState a : args.accountQueryProvider.get().byWatchedProject(args.allProjectsName)) {
-      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyType>> e : a.projectWatches().entrySet()) {
+      for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> e :
+          a.projectWatches().entrySet()) {
         if (args.allProjectsName.equals(e.getKey().project())) {
           Account.Id accountId = a.account().id();
           if (!projectWatchers.contains(accountId)) {
@@ -212,8 +214,8 @@
       Watchers matching,
       Account.Id accountId,
       ProjectWatchKey key,
-      Set<NotifyType> watchedTypes,
-      NotifyType type) {
+      Set<NotifyConfig.NotifyType> watchedTypes,
+      NotifyConfig.NotifyType type) {
     logger.atFine().log("Checking project watch %s of account %s", key, accountId);
 
     IdentifiedUser user = args.identifiedUserFactory.create(accountId);
diff --git a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
index bb2efe6..a54a652 100644
--- a/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -16,9 +16,9 @@
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.exceptions.EmailException;
 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.mail.EmailTokenVerifier;
 import com.google.inject.Inject;
@@ -60,6 +60,9 @@
   @Override
   protected void format() throws EmailException {
     appendText(textTemplate("RegisterNewEmail"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("RegisterNewEmailHtml"));
+    }
   }
 
   public boolean isAllowed() {
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 909c52a..274e664 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -16,11 +16,11 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.util.ArrayList;
@@ -99,9 +99,4 @@
     super.setupSoyContext();
     soyContextEmailData.put("reviewerNames", getReviewerNames());
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/RestoredSender.java b/java/com/google/gerrit/server/mail/send/RestoredSender.java
index 2a4c556..ffe70cf 100644
--- a/java/com/google/gerrit/server/mail/send/RestoredSender.java
+++ b/java/com/google/gerrit/server/mail/send/RestoredSender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -51,9 +51,4 @@
       appendHtml(soyHtmlTemplate("RestoredHtml"));
     }
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/RevertedSender.java b/java/com/google/gerrit/server/mail/send/RevertedSender.java
index dadd0d2..c11529b 100644
--- a/java/com/google/gerrit/server/mail/send/RevertedSender.java
+++ b/java/com/google/gerrit/server/mail/send/RevertedSender.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.mail.send;
 
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -50,9 +50,4 @@
       appendHtml(soyHtmlTemplate("RevertedHtml"));
     }
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
index 2b1e362..29f4c69 100644
--- a/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
+++ b/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
@@ -66,9 +66,4 @@
     super.setupSoyContext();
     soyContextEmailData.put("assigneeName", getNameFor(assignee));
   }
-
-  @Override
-  protected boolean supportsHtml() {
-    return true;
-  }
 }
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 8e53558..af00b20 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -21,9 +21,9 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.Version;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.Encryption;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index f639f49..10a8d8b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
@@ -66,7 +67,6 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index a2ca066..0b03a07 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
@@ -48,7 +49,6 @@
 import com.google.gerrit.entities.converter.PatchSetProtoConverter;
 import com.google.gerrit.entities.converter.ProtoConverter;
 import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 1c81694..1956154 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -55,6 +55,7 @@
 import com.google.common.collect.TreeBasedTable;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
@@ -63,7 +64,6 @@
 import com.google.gerrit.entities.RobotComment;
 import com.google.gerrit.entities.SubmissionId;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.project.ProjectCache;
diff --git a/java/com/google/gerrit/server/permissions/LabelPermission.java b/java/com/google/gerrit/server/permissions/LabelPermission.java
index 7cce9c4..64eecfe 100644
--- a/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -19,7 +19,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.server.util.LabelVote;
 
 /** Permission representing a label. */
diff --git a/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index 090d257..9b74341 100644
--- a/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -17,6 +17,8 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritIsReplica;
+import com.google.gerrit.server.config.GerritIsReplicaProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
@@ -101,6 +103,14 @@
     return secureStore;
   }
 
+  @Inject private GerritIsReplicaProvider isReplicaProvider;
+
+  @Provides
+  @GerritIsReplica
+  boolean getIsReplica() {
+    return isReplicaProvider.get();
+  }
+
   @Inject
   CopyConfigModule() {}
 
diff --git a/java/com/google/gerrit/server/plugins/ListPlugins.java b/java/com/google/gerrit/server/plugins/ListPlugins.java
index 465d041..0408efc 100644
--- a/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -146,12 +146,14 @@
   public static PluginInfo toPluginInfo(Plugin p) {
     String id;
     String version;
+    String apiVersion;
     String indexUrl;
     String filename;
     Boolean disabled;
 
     id = Url.encode(p.getName());
     version = p.getVersion();
+    apiVersion = p.getApiVersion();
     disabled = p.isDisabled() ? true : null;
     if (p.getSrcFile() != null) {
       indexUrl = String.format("plugins/%s/", p.getName());
@@ -161,6 +163,6 @@
       filename = null;
     }
 
-    return new PluginInfo(id, version, indexUrl, filename, disabled);
+    return new PluginInfo(id, version, apiVersion, indexUrl, filename, disabled);
   }
 }
diff --git a/java/com/google/gerrit/server/plugins/Plugin.java b/java/com/google/gerrit/server/plugins/Plugin.java
index 5759705..238066b 100644
--- a/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/java/com/google/gerrit/server/plugins/Plugin.java
@@ -116,6 +116,11 @@
     return apiType;
   }
 
+  @Nullable
+  public String getApiVersion() {
+    return null;
+  }
+
   public Plugin.CacheKey getCacheKey() {
     return cacheKey;
   }
diff --git a/java/com/google/gerrit/server/plugins/PluginUtil.java b/java/com/google/gerrit/server/plugins/PluginUtil.java
index 932a01d..4f00cd0 100644
--- a/java/com/google/gerrit/server/plugins/PluginUtil.java
+++ b/java/com/google/gerrit/server/plugins/PluginUtil.java
@@ -53,7 +53,9 @@
   }
 
   static Path asTemp(InputStream in, String prefix, String suffix, Path dir) throws IOException {
-    Files.createDirectories(dir);
+    if (!Files.exists(dir)) {
+      Files.createDirectories(dir);
+    }
     Path tmp = Files.createTempFile(dir, prefix, suffix);
     boolean keep = false;
     try (OutputStream out = Files.newOutputStream(tmp)) {
diff --git a/java/com/google/gerrit/server/plugins/ServerPlugin.java b/java/com/google/gerrit/server/plugins/ServerPlugin.java
index f236202..320b618 100644
--- a/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -154,6 +154,13 @@
   }
 
   @Override
+  @Nullable
+  public String getApiVersion() {
+    Attributes main = manifest.getMainAttributes();
+    return main.getValue("Gerrit-ApiVersion");
+  }
+
+  @Override
   protected boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
diff --git a/java/com/google/gerrit/server/project/AccessControlModule.java b/java/com/google/gerrit/server/project/AccessControlModule.java
index 89ab8ee..ecad4e1 100644
--- a/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -17,8 +17,8 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.config.AdministrateServerGroups;
 import com.google.gerrit.server.config.AdministrateServerGroupsProvider;
diff --git a/java/com/google/gerrit/server/project/CachedProjectConfig.java b/java/com/google/gerrit/server/project/CachedProjectConfig.java
index 241a146..8af2f80 100644
--- a/java/com/google/gerrit/server/project/CachedProjectConfig.java
+++ b/java/com/google/gerrit/server/project/CachedProjectConfig.java
@@ -20,14 +20,16 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.BranchOrderSection;
+import com.google.gerrit.entities.ConfiguredMimeTypes;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.NotifyConfig;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.git.BranchOrderSection;
-import com.google.gerrit.server.git.NotifyConfig;
+import com.google.gerrit.entities.StoredCommentLinkInfo;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -51,8 +53,8 @@
   }
 
   /**
-   * Returns the group reference for a {@link AccountGroup.UUID}, if the group is used by at least
-   * one rule.
+   * Returns the group reference for a {@link com.google.gerrit.entities.AccountGroup.UUID}, if the
+   * group is used by at least one rule.
    */
   public Optional<GroupReference> getGroup(AccountGroup.UUID uuid) {
     return Optional.ofNullable(getGroups().get(uuid));
@@ -91,7 +93,10 @@
   /** Returns configured {@link ConfiguredMimeTypes}s. */
   public abstract ConfiguredMimeTypes getMimeTypes();
 
-  /** Returns {@link SubscribeSection} keyed by the {@link Project.NameKey} they reference. */
+  /**
+   * Returns {@link SubscribeSection} keyed by the {@link
+   * com.google.gerrit.entities.Project.NameKey} they reference.
+   */
   public abstract ImmutableMap<Project.NameKey, SubscribeSection> getSubscribeSections();
 
   /** Returns {@link StoredCommentLinkInfo} keyed by their name. */
diff --git a/java/com/google/gerrit/server/project/CommentLinkProvider.java b/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 500e163..1b9dc37 100644
--- a/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.StoredCommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.server.config.ConfigUpdatedEvent;
 import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
diff --git a/java/com/google/gerrit/server/project/GroupList.java b/java/com/google/gerrit/server/project/GroupList.java
index 33c73e8..98dc44a 100644
--- a/java/com/google/gerrit/server/project/GroupList.java
+++ b/java/com/google/gerrit/server/project/GroupList.java
@@ -16,8 +16,8 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.TabFile;
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
index 9ff079f..569cb54 100644
--- a/java/com/google/gerrit/server/project/LabelDefinitionJson.java
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -17,7 +17,7 @@
 import static java.util.stream.Collectors.toMap;
 
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 727bf44..0c69722 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -30,37 +30,40 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.BranchOrderSection;
+import com.google.gerrit.entities.ConfiguredMimeTypes;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.entities.NotifyConfig;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.StoredCommentLinkInfo;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.BranchOrderSection;
-import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.git.meta.VersionedMetaData;
@@ -408,7 +411,7 @@
       String name = section.getName();
       if (sectionsWithUnknownPermissions.contains(name)) {
         AccessSection.Builder a = accessSections.get(name).toBuilder();
-        a.modifyPermissions(p -> p.clear());
+        a.modifyPermissions(List::clear);
         accessSections.put(name, a.build());
       } else {
         accessSections.remove(name);
@@ -1337,7 +1340,7 @@
         rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
       }
 
-      if (nc.getNotify().equals(EnumSet.of(NotifyType.ALL))) {
+      if (nc.getNotify().equals(Sets.immutableEnumSet(NotifyType.ALL))) {
         rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
       } else {
         List<String> types = new ArrayList<>(4);
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index fa0a262..19c3afb 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -20,12 +20,12 @@
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index f00df53..de55a12 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -20,7 +20,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.common.LabelTypeInfo;
 import com.google.gerrit.extensions.common.ProjectInfo;
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 616809b..42e09d3 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -22,7 +22,6 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
@@ -31,7 +30,10 @@
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.BranchOrderSection;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.StoredCommentLinkInfo;
 import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -39,7 +41,6 @@
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
diff --git a/java/com/google/gerrit/server/project/testing/BUILD b/java/com/google/gerrit/server/project/testing/BUILD
index 988a89f..3112b5a 100644
--- a/java/com/google/gerrit/server/project/testing/BUILD
+++ b/java/com/google/gerrit/server/project/testing/BUILD
@@ -5,5 +5,8 @@
     testonly = True,
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
-    deps = ["//java/com/google/gerrit/common:server"],
+    deps = [
+        "//java/com/google/gerrit/common:server",
+        "//java/com/google/gerrit/entities",
+    ],
 )
diff --git a/java/com/google/gerrit/server/project/testing/TestLabels.java b/java/com/google/gerrit/server/project/testing/TestLabels.java
index 2c0b23c..8629757 100644
--- a/java/com/google/gerrit/server/project/testing/TestLabels.java
+++ b/java/com/google/gerrit/server/project/testing/TestLabels.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import java.util.Arrays;
 
 public class TestLabels {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 886d0ee..2681b6d 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -28,13 +28,14 @@
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.NotSignedInException;
@@ -48,7 +49,6 @@
 import com.google.gerrit.index.query.QueryBuilder;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryRequiresAuthException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
index 070f800..62fe9e8 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 4e60db5..fbc8d0e 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -19,9 +19,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.index.query.LimitPredicate;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 907dd18..015b235 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -21,9 +21,9 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.InvalidSshKeyException;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 89b931f..6ee4539 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -161,12 +161,12 @@
       }
     } else {
       try {
-        RegisterNewEmailSender sender = registerNewEmailFactory.create(email);
-        if (!sender.isAllowed()) {
+        RegisterNewEmailSender emailSender = registerNewEmailFactory.create(email);
+        if (!emailSender.isAllowed()) {
           throw new MethodNotAllowedException("Not allowed to add email address " + email);
         }
-        sender.setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
-        sender.send();
+        emailSender.setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
+        emailSender.send();
         info.pendingConfirmation = true;
       } catch (EmailException | RuntimeException e) {
         logger.atSevere().withCause(e).log("Cannot send email verification message to %s", email);
diff --git a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
index beb5e8f..8d65aac 100644
--- a/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/GetWatchedProjects.java
@@ -20,6 +20,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.Accounts;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
index b2859e6..c80bf57 100644
--- a/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
+++ b/java/com/google/gerrit/server/restapi/account/PostWatchedProjects.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.account;
 
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.extensions.client.ProjectWatchInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.ProjectWatches;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index 4bb6327..20c4b48 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -147,6 +147,7 @@
       if (loader != null) {
         r.author = loader.get(c.author.getId());
       }
+      r.commitId = c.getCommitId().getName();
     }
 
     protected Range toRange(Comment.Range commentRange) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 9e792d0..52887e0 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -176,16 +176,28 @@
   public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input)
       throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
           PermissionBackendException, ConfigInvalidException {
+    if (Strings.isNullOrEmpty(input.project)) {
+      throw new BadRequestException("project must be non-empty");
+    }
+
+    return execute(updateFactory, input, projectsCollection.parse(input.project));
+  }
+
+  /** Creates the changes in the given project. This is public for reuse in the project API. */
+  public Response<ChangeInfo> execute(
+      BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource)
+      throws IOException, RestApiException, UpdateException, PermissionBackendException,
+          ConfigInvalidException {
     if (!user.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-    IdentifiedUser me = user.get().asIdentifiedUser();
-    checkAndSanitizeChangeInput(input, me);
 
-    ProjectResource projectResource = projectsCollection.parse(input.project);
     ProjectState projectState = projectResource.getProjectState();
     projectState.checkStatePermitsWrite();
 
+    IdentifiedUser me = user.get().asIdentifiedUser();
+    checkAndSanitizeChangeInput(input, me);
+
     Project.NameKey project = projectResource.getNameKey();
     contributorAgreements.check(project, user.get());
 
@@ -207,10 +219,6 @@
    */
   private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
       throws RestApiException, PermissionBackendException, IOException {
-    if (Strings.isNullOrEmpty(input.project)) {
-      throw new BadRequestException("project must be non-empty");
-    }
-
     if (Strings.isNullOrEmpty(input.branch)) {
       throw new BadRequestException("branch must be non-empty");
     }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index bfe7177..f88be81 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -229,13 +229,14 @@
       try {
         NotifyResolver.Result notify = ctx.getNotify(change.getId());
         if (notify.shouldNotify()) {
-          ReplyToChangeSender cm = deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
-          cm.setFrom(user.getAccountId());
-          cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
-          cm.setNotify(notify);
-          cm.setMessageId(
+          ReplyToChangeSender emailSender =
+              deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
+          emailSender.setFrom(user.getAccountId());
+          emailSender.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
+          emailSender.setNotify(notify);
+          emailSender.setMessageId(
               messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-          cm.send();
+          emailSender.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/ListReviewers.java b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
index 25ef480..3d07d43 100644
--- a/java/com/google/gerrit/server/restapi/change/ListReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListReviewers.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ReviewerJson;
diff --git a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
index 73b1f59..b44f637 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRevisionReviewers.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerJson;
 import com.google.gerrit.server.change.ReviewerResource;
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index 3338543..5d65663 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 
 import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.entities.BranchOrderSection;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -29,7 +30,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.change.ChangeIndexer;
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 85079e2..902986c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.entities.Comment;
@@ -81,7 +82,6 @@
 import com.google.gerrit.extensions.validators.CommentValidationContext;
 import com.google.gerrit.extensions.validators.CommentValidationFailure;
 import com.google.gerrit.extensions.validators.CommentValidator;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index d1506b7..7faf8e0 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -150,12 +150,13 @@
     @Override
     public void postUpdate(Context ctx) {
       try {
-        ReplyToChangeSender cm = restoredSenderFactory.create(ctx.getProject(), change.getId());
-        cm.setFrom(ctx.getAccountId());
-        cm.setChangeMessage(message.getMessage(), ctx.getWhen());
-        cm.setMessageId(
+        ReplyToChangeSender emailSender =
+            restoredSenderFactory.create(ctx.getProject(), change.getId());
+        emailSender.setFrom(ctx.getAccountId());
+        emailSender.setChangeMessage(message.getMessage(), ctx.getWhen());
+        emailSender.setMessageId(
             messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-        cm.send();
+        emailSender.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 e2b898f..cb3a375 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -602,12 +602,12 @@
       changeReverted.fire(
           change, changeNotesFactory.createChecked(revertChangeId).getChange(), ctx.getWhen());
       try {
-        RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
-        cm.setFrom(ctx.getAccountId());
-        cm.setNotify(ctx.getNotify(change.getId()));
-        cm.setMessageId(
+        RevertedSender emailSender = revertedSenderFactory.create(ctx.getProject(), change.getId());
+        emailSender.setFrom(ctx.getAccountId());
+        emailSender.setNotify(ctx.getNotify(change.getId()));
+        emailSender.setMessageId(
             messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
-        cm.send();
+        emailSender.send();
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
             "Cannot send email for revert change %s", change.getId());
diff --git a/java/com/google/gerrit/server/restapi/change/Reviewers.java b/java/com/google/gerrit/server/restapi/change/Reviewers.java
index d702142..4bfcf14 100644
--- a/java/com/google/gerrit/server/restapi/change/Reviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/Reviewers.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.change.ChangeResource;
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 676cc07..2a55e41 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -23,8 +23,8 @@
 import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ReviewerState;
diff --git a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
index ac0945d..2651ab5 100644
--- a/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/RevisionReviewers.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
@@ -23,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ReviewerResource;
 import com.google.gerrit.server.change.RevisionResource;
diff --git a/java/com/google/gerrit/server/restapi/config/AgreementJson.java b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
index d5c085b..e1e8e96 100644
--- a/java/com/google/gerrit/server/restapi/config/AgreementJson.java
+++ b/java/com/google/gerrit/server/restapi/config/AgreementJson.java
@@ -16,7 +16,7 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.common.AgreementInfo;
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index 1f4b468..beb7dfd 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -348,7 +348,7 @@
       // Return non-null theme path without checking for file existence. Even if the file doesn't
       // exist under the site path, it may be served from a CDN (in which case it's up to the admin
       // to also pass a proper asset path to the index Soy template).
-      return DEFAULT_THEME;
+      return DEFAULT_THEME_JS;
     }
     return null;
   }
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index 93d095d..700a2ab 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -17,9 +17,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.AccountInfo;
diff --git a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
index 3fd3f29..23fa73d 100644
--- a/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/AddSubgroups.java
@@ -18,8 +18,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index e5a1478..74ca721 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -18,9 +18,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.groups.GroupInput;
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index a7b2e2d..fa52a79 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -16,9 +16,9 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
index b9d6ca8..fe67635 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteSubgroups.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index 508547d..8a469f1 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -16,10 +16,10 @@
 
 import static java.util.Comparator.comparing;
 
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.AccountGroupByIdAudit;
 import com.google.gerrit.entities.AccountGroupMemberAudit;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupAuditEventInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
diff --git a/java/com/google/gerrit/server/restapi/group/GetDescription.java b/java/com/google/gerrit/server/restapi/group/GetDescription.java
index b770281..f65b5e0 100644
--- a/java/com/google/gerrit/server/restapi/group/GetDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/GetDescription.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.group.GroupResource;
diff --git a/java/com/google/gerrit/server/restapi/group/GetOwner.java b/java/com/google/gerrit/server/restapi/group/GetOwner.java
index e8bdfaa..2ab9a69c 100644
--- a/java/com/google/gerrit/server/restapi/group/GetOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/GetOwner.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.group;
 
-import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index 99c9df7..e1459c3 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -19,8 +19,8 @@
 
 import com.google.common.base.Strings;
 import com.google.common.base.Suppliers;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
index 65a7f4f..e0cfb1e 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.common.collect.ListMultimap;
-import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index bcb199f..3e2a577 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -21,10 +21,10 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Streams;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.client.ListGroupsOption;
 import com.google.gerrit.extensions.client.ListOption;
diff --git a/java/com/google/gerrit/server/restapi/group/ListMembers.java b/java/com/google/gerrit/server/restapi/group/ListMembers.java
index 23f0aa7..5b3e8dc 100644
--- a/java/com/google/gerrit/server/restapi/group/ListMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/ListMembers.java
@@ -20,9 +20,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
diff --git a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
index 540718f..776c17c 100644
--- a/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListSubgroups.java
@@ -18,8 +18,8 @@
 import static java.util.Comparator.comparing;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.Response;
diff --git a/java/com/google/gerrit/server/restapi/group/MembersCollection.java b/java/com/google/gerrit/server/restapi/group/MembersCollection.java
index 6dfb2b6..79f3d6a 100644
--- a/java/com/google/gerrit/server/restapi/group/MembersCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/MembersCollection.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.group;
 
-import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
diff --git a/java/com/google/gerrit/server/restapi/group/PutDescription.java b/java/com/google/gerrit/server/restapi/group/PutDescription.java
index 8fe4b20..942e680 100644
--- a/java/com/google/gerrit/server/restapi/group/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/group/PutDescription.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.DescriptionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
diff --git a/java/com/google/gerrit/server/restapi/group/PutName.java b/java/com/google/gerrit/server/restapi/group/PutName.java
index 9a3c87d..acdae33 100644
--- a/java/com/google/gerrit/server/restapi/group/PutName.java
+++ b/java/com/google/gerrit/server/restapi/group/PutName.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.NameInput;
diff --git a/java/com/google/gerrit/server/restapi/group/PutOptions.java b/java/com/google/gerrit/server/restapi/group/PutOptions.java
index 53bf571..748861e 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOptions.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOptions.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.restapi.group;
 
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
diff --git a/java/com/google/gerrit/server/restapi/group/PutOwner.java b/java/com/google/gerrit/server/restapi/group/PutOwner.java
index 04129af..96ce9e4 100644
--- a/java/com/google/gerrit/server/restapi/group/PutOwner.java
+++ b/java/com/google/gerrit/server/restapi/group/PutOwner.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.restapi.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.extensions.api.groups.OwnerInput;
 import com.google.gerrit.extensions.common.GroupInfo;
diff --git a/java/com/google/gerrit/server/restapi/group/SubgroupsCollection.java b/java/com/google/gerrit/server/restapi/group/SubgroupsCollection.java
index cebc27a..c7f6473 100644
--- a/java/com/google/gerrit/server/restapi/group/SubgroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/SubgroupsCollection.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.restapi.group;
 
-import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
new file mode 100644
index 0000000..dbcd8c9
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -0,0 +1,69 @@
+// 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.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.exceptions.InvalidNameException;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class CreateChange implements RestModifyView<ProjectResource, ChangeInput> {
+  private final com.google.gerrit.server.restapi.change.CreateChange changeCreateChange;
+  private final Provider<CurrentUser> user;
+  private final BatchUpdate.Factory updateFactory;
+
+  @Inject
+  public CreateChange(
+      Provider<CurrentUser> user,
+      BatchUpdate.Factory updateFactory,
+      com.google.gerrit.server.restapi.change.CreateChange changeCreateChange) {
+    this.updateFactory = updateFactory;
+    this.changeCreateChange = changeCreateChange;
+    this.user = user;
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ProjectResource rsrc, ChangeInput input)
+      throws PermissionBackendException, IOException, ConfigInvalidException,
+          InvalidChangeOperationException, InvalidNameException, UpdateException, RestApiException {
+    if (!user.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    if (!Strings.isNullOrEmpty(input.project)) {
+      throw new BadRequestException("may not specify project");
+    }
+
+    input.project = rsrc.getName();
+    return changeCreateChange.execute(updateFactory, input, rsrc);
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index 1c19eb0..416eeb3 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -17,7 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 2d1191f..f60601e 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -27,10 +27,10 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
index ccc216d..82e34eb 100644
--- a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
+++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
@@ -18,8 +18,8 @@
 import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index c56e8c6..0c16822 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -26,8 +26,8 @@
 import com.google.common.collect.Iterables;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.NoSuchGroupException;
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 5b3ea30..ee3914d 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -86,6 +86,7 @@
 
     child(PROJECT_KIND, "branches").to(BranchesCollection.class);
     create(BRANCH_KIND).to(CreateBranch.class);
+    post(PROJECT_KIND, "create.change").to(CreateChange.class);
     put(BRANCH_KIND).to(PutBranch.class);
     get(BRANCH_KIND).to(GetBranch.class);
     delete(BRANCH_KIND).to(DeleteBranch.class);
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index d1511c1..572b798 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -17,11 +17,11 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
diff --git a/java/com/google/gerrit/server/schema/AclUtil.java b/java/com/google/gerrit/server/schema/AclUtil.java
index e65568f..6db93397 100644
--- a/java/com/google/gerrit/server/schema/AclUtil.java
+++ b/java/com/google/gerrit/server/schema/AclUtil.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.server.project.ProjectConfig;
 
 /**
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 0fb282e..cd3c945 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -25,10 +25,10 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
diff --git a/java/com/google/gerrit/server/schema/AllProjectsInput.java b/java/com/google/gerrit/server/schema/AllProjectsInput.java
index c91695f..bd405f7 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsInput.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsInput.java
@@ -18,10 +18,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.server.notedb.Sequences;
 import java.util.Optional;
diff --git a/java/com/google/gerrit/server/schema/AllUsersCreator.java b/java/com/google/gerrit/server/schema/AllUsersCreator.java
index 1ac8e69..89fd3654d 100644
--- a/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -22,9 +22,9 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.Version;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
diff --git a/java/com/google/gerrit/server/schema/GrantRevertPermission.java b/java/com/google/gerrit/server/schema/GrantRevertPermission.java
index d4ba29b..77513d3 100644
--- a/java/com/google/gerrit/server/schema/GrantRevertPermission.java
+++ b/java/com/google/gerrit/server/schema/GrantRevertPermission.java
@@ -19,8 +19,8 @@
 import static com.google.gerrit.server.schema.AclUtil.remove;
 
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
diff --git a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
index 1279218..f53f9a6 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.metrics.MetricMaker;
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index c433ee6..4efa4c8 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -91,13 +91,14 @@
   public void run() {
     RequestContext old = requestContext.setContext(this);
     try {
-      MergedSender cm = mergedSenderFactory.create(project, change.getId());
+      MergedSender emailSender = mergedSenderFactory.create(project, change.getId());
       if (submitter != null) {
-        cm.setFrom(submitter);
+        emailSender.setFrom(submitter);
       }
-      cm.setNotify(notify);
-      cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
-      cm.send();
+      emailSender.setNotify(notify);
+      emailSender.setMessageId(
+          messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
+      emailSender.send();
     } catch (Exception e) {
       logger.atSevere().withCause(e).log("Cannot email merged notification for %s", change.getId());
     } finally {
diff --git a/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
index 996ad87..76034ce 100644
--- a/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
+++ b/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.server.validators;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import java.util.Map;
 import java.util.Set;
 
diff --git a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index e5dad7e..3a952f0 100644
--- a/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -49,15 +49,17 @@
           .toJson(output, new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
       stdout.print('\n');
     } else {
-      stdout.format("%-30s %-10s %-8s %s\n", "Name", "Version", "Status", "File");
+      String template = "%-30s %-10s %-16s %-8s %s\n";
+      stdout.format(template, "Name", "Version", "Api-Version", "Status", "File");
       stdout.print(
           "-------------------------------------------------------------------------------\n");
       for (Map.Entry<String, PluginInfo> p : output.entrySet()) {
         PluginInfo info = p.getValue();
         stdout.format(
-            "%-30s %-10s %-8s %s\n",
+            template,
             p.getKey(),
             Strings.nullToEmpty(info.version),
+            Strings.nullToEmpty(info.apiVersion),
             status(info.disabled),
             Strings.nullToEmpty(info.filename));
       }
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 81964be..42d781f 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -23,7 +23,7 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.io.CharStreams;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.GerritApi;
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index a60995b..fec9b27 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -21,9 +21,9 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.exceptions.EmailException;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.mail.MailHeader;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.send.EmailSender;
diff --git a/java/gerrit/PRED_get_legacy_label_types_1.java b/java/gerrit/PRED_get_legacy_label_types_1.java
index 2f0c1ea..956e821 100644
--- a/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -15,7 +15,7 @@
 package gerrit;
 
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.server.rules.StoredValues;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
index 8e08b1c..30f1dcb 100644
--- a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -18,13 +18,13 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.EmailHeader;
 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;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index aaa1eaa..60c2543 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -78,13 +78,14 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
@@ -122,7 +123,6 @@
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.httpd.CacheBasedWebSession;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ExceptionHook;
 import com.google.gerrit.server.ServerInitiated;
 import com.google.gerrit.server.account.AccountProperties;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 62a1ad2..c4bb47a 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -29,10 +29,10 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
index 673379d..3c605e1 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -6,7 +6,6 @@
     group = "api_account",
     labels = [
         "api",
-        "noci",
         "no_windows",
     ],
     deps = [
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 746e6fe..f66bc8d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.Theme;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.config.DownloadScheme;
@@ -68,6 +69,7 @@
 
     // change all default values
     i.changesPerPage *= -1;
+    i.theme = Theme.DARK;
     i.dateFormat = DateFormat.US;
     i.timeFormat = TimeFormat.HHMM_24;
     i.emailStrategy = EmailStrategy.DISABLED;
@@ -90,6 +92,7 @@
     assertPrefs(o, i, "my");
     assertThat(o.my).containsExactlyElementsIn(i.my);
     assertThat(o.changeTable).containsExactlyElementsIn(i.changeTable);
+    assertThat(o.theme).isEqualTo(i.theme);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
index a8fd834..0309646 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -18,9 +18,9 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.Address;
 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;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/BUILD b/javatests/com/google/gerrit/acceptance/api/change/BUILD
index f5993a4..94fb0dc 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/change/BUILD
@@ -5,7 +5,6 @@
     group = f[:f.index(".")],
     labels = [
         "api",
-        "noci",
     ],
     deps = ["//java/com/google/gerrit/server/util/time"],
 ) for f in glob(["*IT.java"])]
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 42c09c7..e7c0f89 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -94,6 +94,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -156,7 +157,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.ChangeResource;
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 8bc9cd1..acebe67 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -56,10 +56,10 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
index 6fcca8c..c977d43 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsUpdateIT.java
@@ -18,9 +18,9 @@
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.exceptions.NoSuchGroupException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.ServerInitiated;
diff --git a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index ff873dd..6838f8d 100644
--- a/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -37,7 +37,12 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.plugins.MandatoryPluginsCollection;
 import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
 import org.junit.Test;
 
 @NoHttpd
@@ -51,7 +56,14 @@
 
   private static final ImmutableList<String> PLUGINS =
       ImmutableList.of(
-          "plugin-a.js", "plugin-b.html", "plugin-c.js", "plugin-d.html", "plugin_e.js");
+          "plugin-a.js",
+          "plugin-b.html",
+          "plugin-c.js",
+          "plugin-d.html",
+          "plugin-normal.jar",
+          "plugin-empty.jar",
+          "plugin-unset.jar",
+          "plugin_e.js");
 
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private MandatoryPluginsCollection mandatoryPluginsCollection;
@@ -67,13 +79,14 @@
     // Install all the plugins
     InstallPluginInput input = new InstallPluginInput();
     for (String plugin : PLUGINS) {
-      input.raw = plugin.endsWith(".js") ? JS_PLUGIN_CONTENT : HTML_PLUGIN_CONTENT;
+      input.raw = pluginContent(plugin);
       api = gApi.plugins().install(plugin, input);
       assertThat(api).isNotNull();
       PluginInfo info = api.get();
       String name = pluginName(plugin);
       assertThat(info.id).isEqualTo(name);
       assertThat(info.version).isEqualTo(pluginVersion(plugin));
+      assertThat(info.apiVersion).isEqualTo(pluginApiVersion(plugin));
       assertThat(info.indexUrl).isEqualTo(String.format("plugins/%s/", name));
       assertThat(info.filename).isEqualTo(plugin);
       assertThat(info.disabled).isNull();
@@ -168,12 +181,52 @@
     return plugin.substring(0, dot);
   }
 
+  private RawInput pluginJarContent(String plugin) throws IOException {
+    ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
+    Manifest manifest = new Manifest();
+    Attributes attributes = manifest.getMainAttributes();
+    attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+    if (!plugin.endsWith("-unset.jar")) {
+      attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, pluginVersion(plugin));
+      attributes.put(new Attributes.Name("Gerrit-ApiVersion"), pluginApiVersion(plugin));
+    }
+    try (JarOutputStream jarStream = new JarOutputStream(arrayStream, manifest)) {}
+    return RawInputUtil.create(arrayStream.toByteArray());
+  }
+
+  private RawInput pluginContent(String plugin) throws IOException {
+    if (plugin.endsWith(".js")) {
+      return JS_PLUGIN_CONTENT;
+    }
+    if (plugin.endsWith(".html")) {
+      return HTML_PLUGIN_CONTENT;
+    }
+    assertThat(plugin).endsWith(".jar");
+    return pluginJarContent(plugin);
+  }
+
   private String pluginVersion(String plugin) {
     String name = pluginName(plugin);
+    if (name.endsWith("empty")) {
+      return "";
+    }
+    if (name.endsWith("unset")) {
+      return null;
+    }
     int dash = name.lastIndexOf("-");
     return dash > 0 ? name.substring(dash + 1) : "";
   }
 
+  private String pluginApiVersion(String plugin) {
+    if (plugin.endsWith("normal.jar")) {
+      return "2.16.19-SNAPSHOT";
+    }
+    if (plugin.endsWith("empty.jar")) {
+      return "";
+    }
+    return null;
+  }
+
   private void assertBadRequest(ListRequest req) throws Exception {
     assertThrows(BadRequestException.class, () -> req.get());
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index da92381..d9d2f65 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.BranchOrderSection;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.RefNames;
@@ -93,7 +94,6 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.restapi.change.GetRevisionActions;
 import com.google.inject.Inject;
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index b01b195..4c3c9d3 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
@@ -93,7 +94,6 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.git.ObjectIds;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.receive.NoteDbPushOption;
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
index dcee118..f2accd4 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmitOnPush.java
@@ -27,15 +27,15 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.notedb.ChangeNotes;
diff --git a/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java b/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
index 6f7a4c3..86fce9c 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushAccountIT.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -43,7 +44,6 @@
 import com.google.gerrit.server.account.AccountProperties;
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.ProjectWatches;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.testing.ConfigSuite;
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 1027938..e9f5143 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -70,6 +70,7 @@
           RestCall.get("/projects/%s/statistics.git"),
           RestCall.post("/projects/%s/index"),
           RestCall.post("/projects/%s/gc"),
+          RestCall.post("/projects/%s/create.change"),
           RestCall.get("/projects/%s/children"),
           RestCall.get("/projects/%s/branches"),
           RestCall.post("/projects/%s/branches:delete"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 843ecc6..012e98d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -33,7 +34,6 @@
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 94357b9..40b6da4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -53,7 +54,6 @@
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.gson.stream.JsonReader;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 46054ec..5f17e87 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -33,8 +33,8 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.access.AccessSectionInfo;
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
new file mode 100644
index 0000000..0c221aa
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
@@ -0,0 +1,49 @@
+// 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.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.entities.RefNames.REFS_HEADS;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.common.ChangeInput;
+import org.junit.Test;
+
+public class CreateChangeIT extends AbstractDaemonTest {
+
+  /**
+   * Just a basic test. The real functionality is tested by {@link
+   * com.google.gerrit.acceptance.rest.change.CreateChangeIT}.
+   */
+  @Test
+  public void basic() throws Exception {
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = "foo";
+    assertThat(gApi.projects().name(project.get()).branches().get().stream().map(i -> i.ref))
+        .doesNotContain(REFS_HEADS + branchInput.ref);
+    RestResponse r =
+        adminRestSession.put(
+            "/projects/" + project.get() + "/branches/" + branchInput.ref, branchInput);
+    r.assertCreated();
+
+    ChangeInput input = new ChangeInput();
+    input.branch = "foo";
+    input.subject = "subject";
+    RestResponse cr = adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+    cr.assertCreated();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/config/BUILD b/javatests/com/google/gerrit/acceptance/server/config/BUILD
new file mode 100644
index 0000000..17802bd
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/config/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "server_config",
+    labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
new file mode 100644
index 0000000..d01a81d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/config/GerritIsReplicaIT.java
@@ -0,0 +1,46 @@
+// 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.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.server.config.GerritIsReplicaProvider;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class GerritIsReplicaIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return new Config();
+  }
+
+  @Inject GerritIsReplicaProvider isReplicaProvider;
+
+  @Test
+  public void isNotReplica() {
+    assertThat(isReplicaProvider.get()).isFalse();
+  }
+
+  @Test
+  @Sandboxed
+  public void isReplica() throws Exception {
+    restartAsSlave();
+    assertThat(isReplicaProvider.get()).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 76514ec..70d8335 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -17,17 +17,17 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.entities.NotifyConfig.NotifyType.ABANDONED_CHANGES;
+import static com.google.gerrit.entities.NotifyConfig.NotifyType.ALL_COMMENTS;
+import static com.google.gerrit.entities.NotifyConfig.NotifyType.NEW_CHANGES;
+import static com.google.gerrit.entities.NotifyConfig.NotifyType.NEW_PATCHSETS;
+import static com.google.gerrit.entities.NotifyConfig.NotifyType.SUBMITTED_CHANGES;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.ALL;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.NONE;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.OWNER;
 import static com.google.gerrit.extensions.api.changes.NotifyHandling.OWNER_REVIEWERS;
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS;
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.ENABLED;
-import static com.google.gerrit.server.account.ProjectWatches.NotifyType.ABANDONED_CHANGES;
-import static com.google.gerrit.server.account.ProjectWatches.NotifyType.ALL_COMMENTS;
-import static com.google.gerrit.server.account.ProjectWatches.NotifyType.NEW_CHANGES;
-import static com.google.gerrit.server.account.ProjectWatches.NotifyType.NEW_PATCHSETS;
-import static com.google.gerrit.server.account.ProjectWatches.NotifyType.SUBMITTED_CHANGES;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.common.collect.ImmutableList;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index 0826c166..6dd2f32 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -22,9 +22,9 @@
 import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.mail.EmailHeader;
 import com.google.gerrit.mail.MailProcessingUtil;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index d7d67b8..4f79e09 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.Streams;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
@@ -40,7 +41,6 @@
 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;
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 0ae9ad2..1c916a3 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -17,7 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.mail.EmailHeader;
+import com.google.gerrit.entities.EmailHeader;
 import java.net.URI;
 import java.util.Map;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index ff26fec..b04ae33 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -27,13 +27,13 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.NotifyConfig;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.mail.Address;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
-import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import java.util.EnumSet;
diff --git a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
index 113bd77..593b635 100644
--- a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
+++ b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
@@ -19,6 +19,8 @@
 
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.AccountGroup.UUID;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
 import org.junit.Test;
 
 public class GroupReferenceTest {
diff --git a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
index 8fea072..298ce1e 100644
--- a/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelFunctionTest.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import java.time.Instant;
diff --git a/javatests/com/google/gerrit/common/data/LabelTypeTest.java b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
index 2f81fa9..4810f58 100644
--- a/javatests/com/google/gerrit/common/data/LabelTypeTest.java
+++ b/javatests/com/google/gerrit/common/data/LabelTypeTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.LabelValue;
 import org.junit.Test;
 
 public class LabelTypeTest {
diff --git a/javatests/com/google/gerrit/common/data/PermissionRuleTest.java b/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
index d193b09..ee6590a 100644
--- a/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionRuleTest.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import org.junit.Before;
 import org.junit.Test;
 
diff --git a/javatests/com/google/gerrit/common/data/PermissionTest.java b/javatests/com/google/gerrit/common/data/PermissionTest.java
index 9dd71ca..ac3e2c5 100644
--- a/javatests/com/google/gerrit/common/data/PermissionTest.java
+++ b/javatests/com/google/gerrit/common/data/PermissionTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import org.junit.Before;
 import org.junit.Test;
 
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index 2306449..b22b8ad 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
 import java.sql.Timestamp;
diff --git a/javatests/com/google/gerrit/mail/AddressTest.java b/javatests/com/google/gerrit/mail/AddressTest.java
index 8addcf8..232b8d1 100644
--- a/javatests/com/google/gerrit/mail/AddressTest.java
+++ b/javatests/com/google/gerrit/mail/AddressTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
+import com.google.gerrit.entities.Address;
 import org.junit.Test;
 
 public class AddressTest {
diff --git a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
index 296d1a1..7e3edab 100644
--- a/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
+++ b/javatests/com/google/gerrit/mail/MailHeaderParserTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.gerrit.entities.Address;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/mail/data/AttachmentMessage.java b/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
index aea59ba..b39e3be 100644
--- a/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
+++ b/javatests/com/google/gerrit/mail/data/AttachmentMessage.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.mail.data;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java b/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
index 957ee6e..92ba97c 100644
--- a/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
+++ b/javatests/com/google/gerrit/mail/data/Base64HeaderMessage.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.mail.data;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java b/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
index e5e2ed8..7cbf9c0 100644
--- a/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
+++ b/javatests/com/google/gerrit/mail/data/HtmlMimeMessage.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.mail.data;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/mail/data/NonUTF8Message.java b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
index e183a37..60368eb 100644
--- a/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
+++ b/javatests/com/google/gerrit/mail/data/NonUTF8Message.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.mail.data;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java b/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
index ac739c8..94c9d42 100644
--- a/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
+++ b/javatests/com/google/gerrit/mail/data/QuotedPrintableHeaderMessage.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.mail.data;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
index 3f8e62f..20d8076 100644
--- a/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
+++ b/javatests/com/google/gerrit/mail/data/SimpleTextMessage.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.mail.data;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.LocalDateTime;
 import java.time.Month;
diff --git a/javatests/com/google/gerrit/server/account/AccountCacheTest.java b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
index 5e75fe5..a2aa40b 100644
--- a/javatests/com/google/gerrit/server/account/AccountCacheTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
@@ -19,6 +19,7 @@
 import com.google.common.truth.Truth;
 import com.google.common.truth.extensions.proto.ProtoTruth;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.proto.Cache;
 import com.google.gerrit.server.config.CachedPreferences;
@@ -103,7 +104,7 @@
     CachedAccountDetails original =
         CachedAccountDetails.create(
             ACCOUNT,
-            ImmutableMap.of(key, ImmutableSet.of(ProjectWatches.NotifyType.ALL_COMMENTS)),
+            ImmutableMap.of(key, ImmutableSet.of(NotifyConfig.NotifyType.ALL_COMMENTS)),
             CachedPreferences.fromString(""));
 
     byte[] serialized = SERIALIZER.serialize(original);
@@ -127,7 +128,7 @@
     CachedAccountDetails original =
         CachedAccountDetails.create(
             ACCOUNT,
-            ImmutableMap.of(key, ImmutableSet.of(ProjectWatches.NotifyType.ALL_COMMENTS)),
+            ImmutableMap.of(key, ImmutableSet.of(NotifyConfig.NotifyType.ALL_COMMENTS)),
             CachedPreferences.fromString(""));
 
     byte[] serialized = SERIALIZER.serialize(original);
diff --git a/javatests/com/google/gerrit/server/account/WatchConfigTest.java b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
index 95dbbde..7d36b94 100644
--- a/javatests/com/google/gerrit/server/account/WatchConfigTest.java
+++ b/javatests/com/google/gerrit/server/account/WatchConfigTest.java
@@ -19,8 +19,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.NotifyConfig.NotifyType;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.account.ProjectWatches.NotifyType;
 import com.google.gerrit.server.account.ProjectWatches.NotifyValue;
 import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
 import com.google.gerrit.server.git.ValidationError;
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/AccessSectionSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/AccessSectionSerializerTest.java
new file mode 100644
index 0000000..660b9e6
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/AccessSectionSerializerTest.java
@@ -0,0 +1,39 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.AccessSectionSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.AccessSectionSerializer.serialize;
+
+import com.google.gerrit.common.data.AccessSection;
+import org.junit.Test;
+
+public class AccessSectionSerializerTest {
+  @Test
+  public void roundTrip() {
+    AccessSection autoValue =
+        AccessSection.builder("refs/test")
+            .addPermission(PermissionSerializerTest.ALL_VALUES_SET.toBuilder())
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    AccessSection autoValue = AccessSection.builder("refs/test").build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/AddressSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/AddressSerializerTest.java
new file mode 100644
index 0000000..4f080a7
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/AddressSerializerTest.java
@@ -0,0 +1,36 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.AddressSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.AddressSerializer.serialize;
+
+import com.google.gerrit.entities.Address;
+import org.junit.Test;
+
+public class AddressSerializerTest {
+  @Test
+  public void roundTrip() {
+    Address autoValue = Address.create("Jane Doe", "jdoe@example.com");
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    Address autoValue = Address.create("jdoe@example.com");
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD b/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
index c781d86..5470553 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
@@ -4,19 +4,15 @@
     name = "tests",
     srcs = glob(["*.java"]),
     deps = [
+        "//java/com/google/gerrit/common:server",
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
-        "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/cache/serialize",
         "//java/com/google/gerrit/server/cache/serialize/entities",
         "//java/com/google/gerrit/server/cache/testing",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:guava",
-        "//lib:jgit",
-        "//lib:junit",
         "//lib:protobuf",
-        "//lib/auto:auto-value",
-        "//lib/auto:auto-value-annotations",
         "//lib/truth",
         "//lib/truth:truth-proto-extension",
         "//proto:cache_java_proto",
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/BranchOrderSectionSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/BranchOrderSectionSerializerTest.java
new file mode 100644
index 0000000..f3a0445
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/BranchOrderSectionSerializerTest.java
@@ -0,0 +1,31 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.BranchOrderSectionSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.BranchOrderSectionSerializer.serialize;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.BranchOrderSection;
+import org.junit.Test;
+
+public class BranchOrderSectionSerializerTest {
+  @Test
+  public void roundTrip() {
+    BranchOrderSection autoValue = BranchOrderSection.create(ImmutableList.of("master", "stable"));
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/ConfiguredMimeTypeSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/ConfiguredMimeTypeSerializerTest.java
new file mode 100644
index 0000000..f0e4932
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/ConfiguredMimeTypeSerializerTest.java
@@ -0,0 +1,36 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.ConfiguredMimeTypeSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.ConfiguredMimeTypeSerializer.serialize;
+
+import com.google.gerrit.entities.ConfiguredMimeTypes;
+import org.junit.Test;
+
+public class ConfiguredMimeTypeSerializerTest {
+  @Test
+  public void reType_roundTrip() {
+    ConfiguredMimeTypes.ReType value = new ConfiguredMimeTypes.ReType("type", "pattern");
+    assertThat(deserialize(serialize(value))).isEqualTo(value);
+  }
+
+  @Test
+  public void fnType_roundTrip() throws Exception {
+    ConfiguredMimeTypes.FnType value = new ConfiguredMimeTypes.FnType("type", "pattern");
+    assertThat(deserialize(serialize(value))).isEqualTo(value);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/ContributorAgreementSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/ContributorAgreementSerializerTest.java
new file mode 100644
index 0000000..81372d5
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/ContributorAgreementSerializerTest.java
@@ -0,0 +1,56 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.ContributorAgreementSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.ContributorAgreementSerializer.serialize;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupReference;
+import org.junit.Test;
+
+public class ContributorAgreementSerializerTest {
+  @Test
+  public void roundTrip() {
+    ContributorAgreement autoValue =
+        ContributorAgreement.builder("name")
+            .setDescription("desc")
+            .setAgreementUrl("url")
+            .setAutoVerify(GroupReference.create("auto-verify"))
+            .setAccepted(
+                ImmutableList.of(
+                    PermissionRule.create(GroupReference.create("accepted1")),
+                    PermissionRule.create(GroupReference.create("accepted2"))))
+            .setExcludeProjectsRegexes(ImmutableList.of("refs/*"))
+            .setMatchProjectsRegexes(ImmutableList.of("refs/heads/*"))
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    ContributorAgreement autoValue =
+        ContributorAgreement.builder("name")
+            .setAccepted(
+                ImmutableList.of(
+                    PermissionRule.create(GroupReference.create("accepted1")),
+                    PermissionRule.create(GroupReference.create("accepted2"))))
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/GroupReferenceSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/GroupReferenceSerializerTest.java
new file mode 100644
index 0000000..a5092e0
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/GroupReferenceSerializerTest.java
@@ -0,0 +1,38 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.GroupReferenceSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.GroupReferenceSerializer.serialize;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
+import org.junit.Test;
+
+public class GroupReferenceSerializerTest {
+  @Test
+  public void roundTrip() {
+    GroupReference groupReferenceAutoValue =
+        GroupReference.create(AccountGroup.uuid("uuid"), "name");
+    assertThat(deserialize(serialize(groupReferenceAutoValue))).isEqualTo(groupReferenceAutoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    GroupReference groupReferenceAutoValue = GroupReference.create("name");
+    assertThat(deserialize(serialize(groupReferenceAutoValue))).isEqualTo(groupReferenceAutoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
new file mode 100644
index 0000000..fac662d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
@@ -0,0 +1,61 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.LabelTypeSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.LabelTypeSerializer.serialize;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.entities.LabelValue;
+import org.junit.Test;
+
+public class LabelTypeSerializerTest {
+  static final LabelType ALL_VALUES_SET =
+      LabelType.builder(
+              "name",
+              ImmutableList.of(
+                  LabelValue.create((short) 0, "no vote"),
+                  LabelValue.create((short) 1, "approved")))
+          .setCanOverride(true)
+          .setAllowPostSubmit(true)
+          .setIgnoreSelfApproval(true)
+          .setRefPatterns(ImmutableList.of("refs/heads/*", "refs/tags/*"))
+          .setDefaultValue((short) 1)
+          .setCopyAnyScore(true)
+          .setCopyMaxScore(true)
+          .setCopyMinScore(true)
+          .setCopyAllScoresOnMergeFirstParentUpdate(true)
+          .setCopyAllScoresOnTrivialRebase(true)
+          .setCopyAllScoresIfNoCodeChange(true)
+          .setCopyAllScoresIfNoChange(true)
+          .setCopyValues(ImmutableList.of((short) 0, (short) 1))
+          .setMaxNegative((short) -1)
+          .setMaxPositive((short) 1)
+          .setCanOverride(true)
+          .build();
+
+  @Test
+  public void roundTrip() {
+    assertThat(deserialize(serialize(ALL_VALUES_SET))).isEqualTo(ALL_VALUES_SET);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    LabelType autoValue = ALL_VALUES_SET.toBuilder().setRefPatterns(null).build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/LabelValueSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelValueSerializerTest.java
new file mode 100644
index 0000000..7e3abbd
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelValueSerializerTest.java
@@ -0,0 +1,30 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.LabelValueSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.LabelValueSerializer.serialize;
+
+import com.google.gerrit.entities.LabelValue;
+import org.junit.Test;
+
+public class LabelValueSerializerTest {
+  @Test
+  public void roundTrip() {
+    LabelValue autoValue = LabelValue.create((short) 123, "Approved!");
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/NotifyConfigSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/NotifyConfigSerializerTest.java
new file mode 100644
index 0000000..3447ae3
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/NotifyConfigSerializerTest.java
@@ -0,0 +1,47 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.NotifyConfigSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.NotifyConfigSerializer.serialize;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Address;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.NotifyConfig;
+import org.junit.Test;
+
+public class NotifyConfigSerializerTest {
+  @Test
+  public void roundTrip() {
+    NotifyConfig autoValue =
+        NotifyConfig.builder()
+            .setName("foo-bar")
+            .addAddress(Address.create("address@example.com"))
+            .addGroup(GroupReference.create("group-uuid"))
+            .setHeader(NotifyConfig.Header.CC)
+            .setFilter("filter")
+            .setNotify(ImmutableSet.of(NotifyConfig.NotifyType.ALL_COMMENTS))
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    NotifyConfig autoValue = NotifyConfig.builder().setName("foo-bar").build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/PermissionRuleSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/PermissionRuleSerializerTest.java
new file mode 100644
index 0000000..3ce3549
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/PermissionRuleSerializerTest.java
@@ -0,0 +1,43 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.PermissionRuleSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.PermissionRuleSerializer.serialize;
+
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupReference;
+import org.junit.Test;
+
+public class PermissionRuleSerializerTest {
+  @Test
+  public void roundTrip() {
+    PermissionRule permissionRuleAutoValue =
+        PermissionRule.builder(GroupReference.create("name"))
+            .setAction(PermissionRule.Action.BATCH)
+            .setForce(true)
+            .setMax(321)
+            .setMin(123)
+            .build();
+    assertThat(deserialize(serialize(permissionRuleAutoValue))).isEqualTo(permissionRuleAutoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    PermissionRule permissionRuleAutoValue = PermissionRule.create(GroupReference.create("name"));
+    assertThat(deserialize(serialize(permissionRuleAutoValue))).isEqualTo(permissionRuleAutoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/PermissionSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/PermissionSerializerTest.java
new file mode 100644
index 0000000..ae399eb
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/PermissionSerializerTest.java
@@ -0,0 +1,43 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.PermissionSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.PermissionSerializer.serialize;
+
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.GroupReference;
+import org.junit.Test;
+
+public class PermissionSerializerTest {
+  static final Permission ALL_VALUES_SET =
+      Permission.builder(Permission.ABANDON)
+          .setExclusiveGroup(true)
+          .add(PermissionRule.builder(GroupReference.create("group")))
+          .build();
+
+  @Test
+  public void roundTrip() {
+    assertThat(deserialize(serialize(ALL_VALUES_SET))).isEqualTo(ALL_VALUES_SET);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    Permission permission = Permission.builder(Permission.ABANDON).build();
+    assertThat(deserialize(serialize(permission))).isEqualTo(permission);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/StoredCommentLinkInfoSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/StoredCommentLinkInfoSerializerTest.java
new file mode 100644
index 0000000..ccd2378
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/StoredCommentLinkInfoSerializerTest.java
@@ -0,0 +1,58 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.StoredCommentLinkInfoSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.StoredCommentLinkInfoSerializer.serialize;
+
+import com.google.gerrit.entities.StoredCommentLinkInfo;
+import org.junit.Test;
+
+public class StoredCommentLinkInfoSerializerTest {
+  @Test
+  public void htmlOnly_roundTrip() {
+    StoredCommentLinkInfo autoValue =
+        StoredCommentLinkInfo.builder("name")
+            .setEnabled(true)
+            .setHtml("<p>html")
+            .setMatch("*")
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+
+  @Test
+  public void linkOnly_roundTrip() {
+    StoredCommentLinkInfo autoValue =
+        StoredCommentLinkInfo.builder("name")
+            .setEnabled(true)
+            .setLink("<p>html")
+            .setMatch("*")
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+
+  @Test
+  public void overrideOnly_roundTrip() {
+    StoredCommentLinkInfo autoValue =
+        StoredCommentLinkInfo.builder("name")
+            .setEnabled(true)
+            .setOverrideOnly(true)
+            .setLink("<p>html")
+            .setMatch("*")
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/SubscribeSectionSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/SubscribeSectionSerializerTest.java
new file mode 100644
index 0000000..fc96932
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubscribeSectionSerializerTest.java
@@ -0,0 +1,37 @@
+// 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.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.SubscribeSectionSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.SubscribeSectionSerializer.serialize;
+
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.Project;
+import org.junit.Test;
+
+public class SubscribeSectionSerializerTest {
+  @Test
+  public void roundTrip() {
+    SubscribeSection autoValue =
+        SubscribeSection.builder(Project.nameKey("project"))
+            .addMultiMatchRefSpec("multi")
+            .addMultiMatchRefSpec("multi2")
+            .addMatchingRefSpec("matching1")
+            .addMatchingRefSpec("matching2")
+            .build();
+    assertThat(deserialize(serialize(autoValue))).isEqualTo(autoValue);
+  }
+}
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index c749b77..20fe387 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -17,9 +17,9 @@
 import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.server.config.AllUsersName;
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index b7fe23d..c1f3615 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -25,9 +25,9 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 278f617..3b7beb9 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -25,9 +25,9 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.testing.GroupReferenceSubject;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.DuplicateKeyException;
diff --git a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
index 805c542..d8e29f9 100644
--- a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
+++ b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
@@ -16,7 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.mail.MailMessage;
 import java.time.Instant;
 import org.junit.Test;
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 03129ae..f10a281 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -22,7 +22,7 @@
 import static org.mockito.Mockito.when;
 
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.mail.Address;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.util.time.TimeUtil;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index a5e7dce..f1b7198 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitRequirement;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
@@ -39,7 +40,6 @@
 import com.google.gerrit.entities.converter.ChangeMessageProtoConverter;
 import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
 import com.google.gerrit.entities.converter.PatchSetProtoConverter;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index df5903f..b0b1c1e 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -38,6 +38,7 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.entities.AttentionSetUpdate.Operation;
 import com.google.gerrit.entities.BranchNameKey;
@@ -49,7 +50,6 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.SubmissionId;
 import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.AssigneeStatusUpdate;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 8818d81..6cfd9f2d 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -20,9 +20,9 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.SubmissionId;
-import com.google.gerrit.mail.Address;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestChanges;
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index 5e57551..5af8a1e 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -28,11 +28,11 @@
 import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
diff --git a/javatests/com/google/gerrit/server/project/GroupListTest.java b/javatests/com/google/gerrit/server/project/GroupListTest.java
index 18e1631..f3295f8 100644
--- a/javatests/com/google/gerrit/server/project/GroupListTest.java
+++ b/javatests/com/google/gerrit/server/project/GroupListTest.java
@@ -23,8 +23,8 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.git.ValidationError;
 import java.io.IOException;
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 2d31acf..6ea6a33 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -22,19 +22,20 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.BranchOrderSection;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.StoredCommentLinkInfo;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.BranchOrderSection;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.project.testing.TestLabels;
@@ -597,6 +598,27 @@
   }
 
   @Test
+  public void readCommentLinksNoHtmlOrLinkAndMissingEnabled() throws Exception {
+    RevCommit rev =
+        tr.commit()
+            .add(
+                "project.config",
+                "[commentlink \"bugzilla\"]\n \tlink = http://bugs.example.com/show_bug.cgi?id=$2"
+                    + "\n \tmatch = \"(bug\\\\s+#?)(\\\\d+)\"\n")
+            .create();
+    ProjectConfig cfg = read(rev);
+    assertThat(cfg.getCommentLinkSections())
+        .containsExactly(
+            StoredCommentLinkInfo.builder("bugzilla")
+                .setMatch("(bug\\s+#?)(\\d+)")
+                .setLink("http://bugs.example.com/show_bug.cgi?id=$2")
+                .build());
+    StoredCommentLinkInfo stored = Iterables.getOnlyElement(cfg.getCommentLinkSections());
+    assertThat(StoredCommentLinkInfo.fromInfo(stored.toInfo(), stored.getEnabled()))
+        .isEqualTo(stored);
+  }
+
+  @Test
   public void readCommentLinkInvalidPattern() throws Exception {
     RevCommit rev =
         tr.commit()
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 2eb25da..a013145 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -44,7 +44,6 @@
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
@@ -52,6 +51,7 @@
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.GroupReference;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
index 85a3207..858f6a2 100644
--- a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -18,10 +18,10 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import java.time.Instant;
diff --git a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
index 9cf4896..18d279f 100644
--- a/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
+++ b/javatests/com/google/gerrit/server/schema/AllProjectsCreatorTest.java
@@ -23,11 +23,11 @@
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupUuid;
diff --git a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
index c92a8e0..d58713a 100644
--- a/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
+++ b/javatests/com/google/gerrit/server/schema/SchemaCreatorImplTest.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.common.data.LabelFunction;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.LabelValue;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectConfig;
diff --git a/package.json b/package.json
index 329e3cb..a6641b9 100644
--- a/package.json
+++ b/package.json
@@ -4,16 +4,15 @@
   "description": "Gerrit Code Review",
   "dependencies": {},
   "devDependencies": {
-    "@bazel/rollup": "^1.6.1",
-    "@bazel/terser": "^1.7.0",
-    "@bazel/typescript": "^1.6.1",
+    "@bazel/rollup": "^2.0.0",
+    "@bazel/terser": "^2.0.0",
+    "@bazel/typescript": "^2.0.0",
     "eslint": "^6.6.0",
     "eslint-config-google": "^0.13.0",
     "eslint-plugin-html": "^6.0.0",
     "eslint-plugin-import": "^2.20.1",
     "eslint-plugin-jsdoc": "^19.2.0",
     "eslint-plugin-prettier": "^3.1.3",
-    "fried-twinkie": "^0.2.2",
     "gts": "^2.0.2",
     "polymer-cli": "^1.9.11",
     "prettier": "2.0.5",
@@ -29,7 +28,6 @@
     "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
-    "test-template": "./polygerrit-ui/app/run_template_test.sh",
     "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
     "test:debug": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
     "test:single": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles"
diff --git a/plugins/delete-project b/plugins/delete-project
index 7cb59ec..64db8df 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 7cb59ecacbbe7bc995873ae112e48cf0ff521d2a
+Subproject commit 64db8df08e855d8367c146bc5071f68f70df5171
diff --git a/plugins/download-commands b/plugins/download-commands
index c4ef993..fd650ca 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit c4ef993fa5e8578641d6447c831ace13743dd5de
+Subproject commit fd650ca386c382b42d30e7ad72279bfeb311aee4
diff --git a/plugins/replication b/plugins/replication
index ced7fc3..9d4d19a 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit ced7fc318feb76e2fc6d549669c5f5d8d905add5
+Subproject commit 9d4d19a579fc4962ab7f85f6b5cb12501ed048ad
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index e952b92..05f0ddd 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit e952b920ecbee5225f1098a02d4a39b19aa7e234
+Subproject commit 05f0ddd30928d0d050696f3d269dea0899334513
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 9eb6334..58ee52a 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 9eb63345a129533aa88235af3ba9308c53cee1d2
+Subproject commit 58ee52a8670e38f30785bfbb648ba27c61c3a202
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 3a66e97..3e95e42 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -279,47 +279,6 @@
 npm run polylint
 ```
 
-## Template Type Safety
-
-> **Warning**: This feature is temporary disabled, because it doesn't work with Polymer 2 and Polymer 3. Some of the checks are made by polymer linter.
-
-Polymer elements are not type checked against the element definition, making it
-trivial to break the display when refactoring or moving code. We now run
-additional tests to help ensure that template types are checked.
-
-A few notes to ensure that these tests pass
-- Any functions with optional parameters will need closure annotations.
-- Any Polymer parameters that are nullable or can be multiple types (other than
-  the one explicitly delared) will need type annotations.
-
-These tests require the `typescript` and `fried-twinkie` npm packages.
-
-To run on all files, execute the following command:
-
-```sh
-./polygerrit-ui/app/run_template_test.sh
-```
-
-or
-
-```sh
-npm run test-template
-```
-
-To run on a specific top level directory (ex: change-list)
-```sh
-TEMPLATE_NO_DEFAULT=true ./polygerrit-ui/app/run_template_test.sh //polygerrit-ui/app:template_test_change-list
-```
-
-To run on a specific file (ex: gr-change-list-view), execute the following command:
-```sh
-TEMPLATE_NO_DEFAULT=true ./polygerrit-ui/app/run_template_test.sh //polygerrit-ui/app:template_test_<TOP_LEVEL_DIRECTORY> --test_arg=<VIEW_NAME>
-```
-
-```sh
-TEMPLATE_NO_DEFAULT=true ./polygerrit-ui/app/run_template_test.sh //polygerrit-ui/app:template_test_change-list --test_arg=gr-change-list-view
-```
-
 ## Contributing
 
 Our users report bugs / feature requests related to the UI through [Monorail Issues - PolyGerrit](https://bugs.chromium.org/p/gerrit/issues/list?q=component%3APolyGerrit).
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 932997e..c402297 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -67,13 +67,14 @@
       }
     ],
     "new-cap": ["error", {
-      "capIsNewExceptions": ["Polymer", "LegacyElementMixin",
-        "GestureEventListeners", "LegacyDataMixin"]
+      "capIsNewExceptions": ["Polymer", "GestureEventListeners"],
+      "capIsNewExceptionPattern": "^.*Mixin$"
     }],
-    "no-console": "off",
+    "no-console": ["error", { allow: ["warn", "error", "info", "assert", "group", "groupEnd"] }],
     "no-multiple-empty-lines": ["error", {"max": 1}],
     "no-prototype-builtins": "off",
     "no-redeclare": "off",
+    'array-callback-return': ['error', { allowImplicit: true }],
     "no-restricted-syntax": [
       "error",
       {
@@ -109,6 +110,7 @@
     "prefer-const": "error",
     "prefer-promise-reject-errors": "error",
     "prefer-spread": "error",
+    "prefer-object-spread": "error",
     "quote-props": ["error", "consistent-as-needed"],
     "semi": ["error", "always"],
     "template-curly-spacing": "error",
@@ -216,7 +218,7 @@
       }
     },
     {
-      "files": ["*.html", "test.js", "test-infra.js", "template_test.js"],
+      "files": ["*.html", "test.js", "test-infra.js"],
       "rules": {
         "jsdoc/require-file-overview": "off"
       },
@@ -270,7 +272,7 @@
       }
     },
     {
-      "files": ["test/functional/**/*.js", "template_test.js"],
+      "files": ["test/functional/**/*.js"],
       // Settings for functional tests. These scripts are node scripts.
       // Turn off "no-undef" to allow any global variable
       "env": {
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 054033c..41c3f17 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -5,11 +5,11 @@
 
 # This list must be in sync with the "include" list in the tsconfig.json file
 src_dirs = [
-    "behaviors",
     "constants",
     "elements",
     "embed",
     "gr-diff",
+    "mixins",
     "samples",
     "scripts",
     "services",
@@ -163,40 +163,3 @@
         "manual",
     ],
 )
-
-DIRECTORIES = [
-    "admin",
-    "change",
-    "change-list",
-    "core",
-    "diff",
-    "edit",
-    "plugins",
-    "settings",
-    "shared",
-    "gr-app",
-]
-
-[sh_test(
-    name = "template_test_" + directory,
-    size = "enormous",
-    srcs = ["template_test.sh"],
-    args = [directory],
-    data = [
-        ":pg_code",
-        ":template_test_srcs",
-    ],
-    tags = [
-        # Should not run sandboxed.
-        "local",
-        "template",
-    ],
-) for directory in DIRECTORIES]
-
-filegroup(
-    name = "template_test_srcs",
-    srcs = [
-        "template_test_srcs/convert_for_template_tests.py",
-        "template_test_srcs/template_test.js",
-    ],
-)
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
deleted file mode 100644
index 46dfaf2..0000000
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.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 {descendedFromClass} from '../../utils/dom-util.js';
-
-export const DomUtilBehavior = {
-  /**
-   * 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}
-   */
-  descendedFromClass(element, className, opt_stopElement) {
-    console.warn('DomUtilBehavior is deprecated.' +
-     'Use descendedFromClass from utils directly.');
-    return descendedFromClass(element, className, opt_stopElement);
-  },
-};
-
-// 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.DomUtilBehavior = DomUtilBehavior;
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.js b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.js
deleted file mode 100644
index c110b34..0000000
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior_test.js
+++ /dev/null
@@ -1,62 +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 '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {DomUtilBehavior} from './dom-util-behavior.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-const nestedStructureFixture = fixtureFromTemplate(html`
-  <dom-util-behavior-test-element></dom-util-behavior-test-element>
-  <div>
-    <div class="a">
-      <div class="b">
-        <div class="c"></div>
-      </div>
-    </div>
-  </div>
-`);
-
-suite('dom-util-behavior tests', () => {
-  let element;
-  let divs;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'dom-util-behavior-test-element',
-      behaviors: [DomUtilBehavior],
-    });
-  });
-
-  setup(() => {
-    const testDom = nestedStructureFixture.instantiate();
-    element = testDom[0];
-    divs = testDom[1];
-  });
-
-  test('descendedFromClass', () => {
-    // .c is a child of .a and not vice versa.
-    assert.isTrue(element.descendedFromClass(divs.querySelector('.c'), 'a'));
-    assert.isFalse(element.descendedFromClass(divs.querySelector('.a'), 'c'));
-
-    // Stops at stop element.
-    assert.isFalse(element.descendedFromClass(divs.querySelector('.c'), 'a',
-        divs.querySelector('.b')));
-  });
-});
-
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js b/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
deleted file mode 100644
index 7b48ecc..0000000
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior.js
+++ /dev/null
@@ -1,159 +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.
- */
-
-/** @polymerBehavior Gerrit.AccessBehavior */
-export const AccessBehavior = {
-  properties: {
-    permissionValues: {
-      type: Object,
-      readOnly: true,
-      value: {
-        abandon: {
-          id: 'abandon',
-          name: 'Abandon',
-        },
-        addPatchSet: {
-          id: 'addPatchSet',
-          name: 'Add Patch Set',
-        },
-        create: {
-          id: 'create',
-          name: 'Create Reference',
-        },
-        createTag: {
-          id: 'createTag',
-          name: 'Create Annotated Tag',
-        },
-        createSignedTag: {
-          id: 'createSignedTag',
-          name: 'Create Signed Tag',
-        },
-        delete: {
-          id: 'delete',
-          name: 'Delete Reference',
-        },
-        deleteChanges: {
-          id: 'deleteChanges',
-          name: 'Delete Changes',
-        },
-        deleteOwnChanges: {
-          id: 'deleteOwnChanges',
-          name: 'Delete Own Changes',
-        },
-        editAssignee: {
-          id: 'editAssignee',
-          name: 'Edit Assignee',
-        },
-        editHashtags: {
-          id: 'editHashtags',
-          name: 'Edit Hashtags',
-        },
-        editTopicName: {
-          id: 'editTopicName',
-          name: 'Edit Topic Name',
-        },
-        forgeAuthor: {
-          id: 'forgeAuthor',
-          name: 'Forge Author Identity',
-        },
-        forgeCommitter: {
-          id: 'forgeCommitter',
-          name: 'Forge Committer Identity',
-        },
-        forgeServerAsCommitter: {
-          id: 'forgeServerAsCommitter',
-          name: 'Forge Server Identity',
-        },
-        owner: {
-          id: 'owner',
-          name: 'Owner',
-        },
-        publishDrafts: {
-          id: 'publishDrafts',
-          name: 'Publish Drafts',
-        },
-        push: {
-          id: 'push',
-          name: 'Push',
-        },
-        pushMerge: {
-          id: 'pushMerge',
-          name: 'Push Merge Commit',
-        },
-        read: {
-          id: 'read',
-          name: 'Read',
-        },
-        rebase: {
-          id: 'rebase',
-          name: 'Rebase',
-        },
-        revert: {
-          id: 'revert',
-          name: 'Revert',
-        },
-        removeReviewer: {
-          id: 'removeReviewer',
-          name: 'Remove Reviewer',
-        },
-        submit: {
-          id: 'submit',
-          name: 'Submit',
-        },
-        submitAs: {
-          id: 'submitAs',
-          name: 'Submit (On Behalf Of)',
-        },
-        toggleWipState: {
-          id: 'toggleWipState',
-          name: 'Toggle Work In Progress State',
-        },
-        viewPrivateChanges: {
-          id: 'viewPrivateChanges',
-          name: 'View Private Changes',
-        },
-      },
-    },
-  },
-
-  /**
-   * @param {!Object} obj
-   * @return {!Array} returns a sorted array sorted by the id of the original
-   *    object.
-   */
-  toSortedArray(obj) {
-    if (!obj) { return []; }
-    return Object.keys(obj)
-        .map(key => {
-          return {
-            id: key,
-            value: obj[key],
-          };
-        })
-        .sort((a, b) =>
-          // Since IDs are strings, use localeCompare.
-          a.id.localeCompare(b.id)
-        );
-  },
-};
-
-// 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.AccessBehavior = AccessBehavior;
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
deleted file mode 100644
index 51c52f1..0000000
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import {GerritNav} from '../../elements/core/gr-navigation/gr-navigation.js';
-
-/**
- * @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.
- */
-
-const ADMIN_LINKS = [{
-  name: 'Repositories',
-  noBaseUrl: true,
-  url: '/admin/repos',
-  view: 'gr-repo-list',
-  viewableToAll: true,
-}, {
-  name: 'Groups',
-  section: 'Groups',
-  noBaseUrl: true,
-  url: '/admin/groups',
-  view: 'gr-admin-group-list',
-}, {
-  name: 'Plugins',
-  capability: 'viewPlugins',
-  section: 'Plugins',
-  noBaseUrl: true,
-  url: '/admin/plugins',
-  view: 'gr-plugin-list',
-}];
-
-window.Gerrit = window.Gerrit || {};
-
-/** @polymerBehavior Gerrit.AdminNavBehavior */
-export const AdminNavBehavior = {
-  /**
-   * @param {!Object} account
-   * @param {!Function} getAccountCapabilities
-   * @param {!Function} getAdminMenuLinks
-   *  Possible aguments in options:
-   *    repoName?: string
-   *    groupId?: string,
-   *    groupName?: string,
-   *    groupIsInternal?: boolean,
-   *    isAdmin?: boolean,
-   *    groupOwner?: boolean,
-   * @param {!Object=} opt_options
-   * @return {Promise<!Object>}
-   */
-  getAdminLinks(account, getAccountCapabilities, getAdminMenuLinks,
-      opt_options) {
-    if (!account) {
-      return Promise.resolve(this._filterLinks(link => link.viewableToAll,
-          getAdminMenuLinks, opt_options));
-    }
-    return getAccountCapabilities()
-        .then(capabilities => this._filterLinks(
-            link => !link.capability
-            || capabilities.hasOwnProperty(link.capability),
-            getAdminMenuLinks,
-            opt_options));
-  },
-
-  /**
-   * @param {!Function} filterFn
-   * @param {!Function} getAdminMenuLinks
-   *  Possible aguments in options:
-   *    repoName?: string
-   *    groupId?: string,
-   *    groupName?: string,
-   *    groupIsInternal?: boolean,
-   *    isAdmin?: boolean,
-   *    groupOwner?: boolean,
-   * @param {!Object|undefined} opt_options
-   * @return {Promise<!Object>}
-   */
-  _filterLinks(filterFn, getAdminMenuLinks, opt_options) {
-    let links = ADMIN_LINKS.slice(0);
-    let expandedSection;
-
-    const isExternalLink = link => link.url[0] !== '/';
-
-    // Append top-level links that are defined by plugins.
-    links.push(...getAdminMenuLinks().map(link => {
-      return {
-        url: link.url,
-        name: link.text,
-        capability: link.capability || null,
-        noBaseUrl: !isExternalLink(link),
-        view: null,
-        viewableToAll: !link.capability,
-        target: isExternalLink(link) ? '_blank' : null,
-      };
-    }));
-
-    links = links.filter(filterFn);
-
-    const filteredLinks = [];
-    const repoName = opt_options && opt_options.repoName;
-    const groupId = opt_options && opt_options.groupId;
-    const groupName = opt_options && opt_options.groupName;
-    const groupIsInternal = opt_options && opt_options.groupIsInternal;
-    const isAdmin = opt_options && opt_options.isAdmin;
-    const groupOwner = opt_options && opt_options.groupOwner;
-
-    // Don't bother to get sub-navigation items if only the top level links
-    // are needed. This is used by the main header dropdown.
-    if (!repoName && !groupId) { return {links, expandedSection}; }
-
-    // Otherwise determine the full set of links and return both the full
-    // set in addition to the subsection that should be displayed if it
-    // exists.
-    for (const link of links) {
-      const linkCopy = Object.assign({}, link);
-      if (linkCopy.name === 'Repositories' && repoName) {
-        linkCopy.subsection = this.getRepoSubsections(repoName);
-        expandedSection = linkCopy.subsection;
-      } else if (linkCopy.name === 'Groups' && groupId && groupName) {
-        linkCopy.subsection = this.getGroupSubsections(groupId, groupName,
-            groupIsInternal, isAdmin, groupOwner);
-        expandedSection = linkCopy.subsection;
-      }
-      filteredLinks.push(linkCopy);
-    }
-    return {links: filteredLinks, expandedSection};
-  },
-
-  getGroupSubsections(groupId, groupName, groupIsInternal, isAdmin,
-      groupOwner) {
-    const subsection = {
-      name: groupName,
-      view: GerritNav.View.GROUP,
-      url: GerritNav.getUrlForGroup(groupId),
-      children: [],
-    };
-    if (groupIsInternal) {
-      subsection.children.push({
-        name: 'Members',
-        detailType: GerritNav.GroupDetailView.MEMBERS,
-        view: GerritNav.View.GROUP,
-        url: GerritNav.getUrlForGroupMembers(groupId),
-      });
-    }
-    if (groupIsInternal && (isAdmin || groupOwner)) {
-      subsection.children.push(
-          {
-            name: 'Audit Log',
-            detailType: GerritNav.GroupDetailView.LOG,
-            view: GerritNav.View.GROUP,
-            url: GerritNav.getUrlForGroupLog(groupId),
-          }
-      );
-    }
-    return subsection;
-  },
-
-  getRepoSubsections(repoName) {
-    return {
-      name: repoName,
-      view: GerritNav.View.REPO,
-      url: GerritNav.getUrlForRepo(repoName),
-      children: [{
-        name: 'Access',
-        view: GerritNav.View.REPO,
-        detailType: GerritNav.RepoDetailView.ACCESS,
-        url: GerritNav.getUrlForRepoAccess(repoName),
-      },
-      {
-        name: 'Commands',
-        view: GerritNav.View.REPO,
-        detailType: GerritNav.RepoDetailView.COMMANDS,
-        url: GerritNav.getUrlForRepoCommands(repoName),
-      },
-      {
-        name: 'Branches',
-        view: GerritNav.View.REPO,
-        detailType: GerritNav.RepoDetailView.BRANCHES,
-        url: GerritNav.getUrlForRepoBranches(repoName),
-      },
-      {
-        name: 'Tags',
-        view: GerritNav.View.REPO,
-        detailType: GerritNav.RepoDetailView.TAGS,
-        url: GerritNav.getUrlForRepoTags(repoName),
-      },
-      {
-        name: 'Dashboards',
-        view: GerritNav.View.REPO,
-        detailType: GerritNav.RepoDetailView.DASHBOARDS,
-        url: GerritNav.getUrlForRepoDashboards(repoName),
-      }],
-    };
-  },
-};
-
-// 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.AdminNavBehavior = AdminNavBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
deleted file mode 100644
index 7feaf79..0000000
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/** @polymerBehavior Gerrit.ChangeTableBehavior */
-export const ChangeTableBehavior = {
-  properties: {
-    columnNames: {
-      type: Array,
-      value: [
-        'Subject',
-        'Status',
-        'Owner',
-        'Assignee',
-        'Reviewers',
-        'Comments',
-        'Repo',
-        'Branch',
-        'Updated',
-        'Size',
-      ],
-      readOnly: true,
-    },
-  },
-
-  /**
-   * Returns the complement to the given column array
-   *
-   * @param {Array} columns
-   * @return {!Array}
-   */
-  getComplementColumns(columns) {
-    return this.columnNames.filter(column => !columns.includes(column));
-  },
-
-  /**
-   * @param {string} columnToCheck
-   * @param {!Array} columnsToDisplay
-   * @return {boolean}
-   */
-  isColumnHidden(columnToCheck, columnsToDisplay) {
-    if ([columnsToDisplay, columnToCheck].includes(undefined)) {
-      return false;
-    }
-    return !columnsToDisplay.includes(columnToCheck);
-  },
-
-  /**
-   * Is the column disabled by a server config or experiment? For example the
-   * assignee feature might be disabled and thus the corresponding column is
-   * also disabled.
-   *
-   * @param {string} column
-   * @param {Object} config
-   * @param {!Array<string>} experiments
-   * @return {boolean}
-   */
-  isColumnEnabled(column, config, experiments) {
-    if (!config || !config.change) return true;
-    if (column === 'Assignee') return !!config.change.enable_assignee;
-    if (column === 'Comments') return experiments.includes('comments-column');
-    if (column === 'Reviewers') return !!config.change.enable_attention_set;
-    return true;
-  },
-
-  /**
-   * @param {!Array<string>} columns
-   * @param {Object} config
-   * @param {!Array<string>} experiments
-   * @return {!Array<string>} enabled columns, see isColumnEnabled().
-   */
-  getEnabledColumns(columns, config, experiments) {
-    return columns.filter(
-        col => this.isColumnEnabled(col, config, experiments));
-  },
-
-  /**
-   * The Project column was renamed to Repo, but some users may have
-   * preferences that use its old name. If that column is found, rename it
-   * before use.
-   *
-   * @param {!Array<string>} columns
-   * @return {!Array<string>} If the column was renamed, returns a new array
-   *     with the corrected name. Otherwise, it returns the original param.
-   */
-  getVisibleColumns(columns) {
-    const projectIndex = columns.indexOf('Project');
-    if (projectIndex === -1) { return columns; }
-    const newColumns = columns.slice(0);
-    newColumns[projectIndex] = 'Repo';
-    return newColumns;
-  },
-};
-
-// 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.ChangeTableBehavior = ChangeTableBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
deleted file mode 100644
index 607499b..0000000
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior.js
+++ /dev/null
@@ -1,41 +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 {GrDisplayNameUtils} from '../../scripts/gr-display-name-utils/gr-display-name-utils.js';
-
-/** @polymerBehavior Gerrit.DisplayNameBehavior */
-export const DisplayNameBehavior = {
-  // TODO(dmfilippov) replace DisplayNameBehavior with GrDisplayNameUtils
-
-  getUserName(config, account) {
-    return GrDisplayNameUtils.getUserName(config, account);
-  },
-
-  getDisplayName(config, account) {
-    return GrDisplayNameUtils.getDisplayName(config, account);
-  },
-
-  getGroupDisplayName(group) {
-    return GrDisplayNameUtils.getGroupDisplayName(group);
-  },
-};
-
-// 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.DisplayNameBehavior = DisplayNameBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.js b/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.js
deleted file mode 100644
index 82108429..0000000
--- a/polygerrit-ui/app/behaviors/gr-display-name-behavior/gr-display-name-behavior_test.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {DisplayNameBehavior} from './gr-display-name-behavior.js';
-
-const basicFixture = fixtureFromElement('test-element-anon');
-
-suite('gr-display-name-behavior tests', () => {
-  let element;
-  // eslint-disable-next-line no-unused-vars
-  const config = {
-    user: {
-      anonymous_coward_name: 'Anonymous Coward',
-    },
-  };
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'test-element-anon',
-      behaviors: [
-        DisplayNameBehavior,
-      ],
-    });
-  });
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('getUserName name only', () => {
-    const account = {
-      name: 'test-name',
-    };
-    assert.equal(element.getUserName(config, account), 'test-name');
-  });
-
-  test('getUserName username only', () => {
-    const account = {
-      username: 'test-user',
-    };
-    assert.equal(element.getUserName(config, account), 'test-user');
-  });
-
-  test('getUserName email only', () => {
-    const account = {
-      email: 'test-user@test-url.com',
-    };
-    assert.equal(element.getUserName(config, account),
-        'test-user@test-url.com');
-  });
-
-  test('getUserName returns not Anonymous Coward as the anon name', () => {
-    assert.equal(element.getUserName(config, null), 'Anonymous');
-  });
-
-  test('getUserName for the config returning the anon name', () => {
-    const config = {
-      user: {
-        anonymous_coward_name: 'Test Anon',
-      },
-    };
-    assert.equal(element.getUserName(config, null), 'Test Anon');
-  });
-
-  test('getGroupDisplayName', () => {
-    assert.equal(element.getGroupDisplayName({name: 'Some user name'}),
-        'Some user name (group)');
-  });
-});
-
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
deleted file mode 100644
index 180fcc8..0000000
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.js
+++ /dev/null
@@ -1,83 +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 {getBaseUrl} from '../../utils/url-util.js';
-import {URLEncodingBehavior} from '../gr-url-encoding-behavior/gr-url-encoding-behavior.js';
-
-/** @polymerBehavior ListViewBehavior */
-export const ListViewBehavior = [{
-  computeLoadingClass(loading) {
-    return loading ? 'loading' : '';
-  },
-
-  computeShownItems(items) {
-    return items.slice(0, 25);
-  },
-
-  getUrl(path, item) {
-    return getBaseUrl() + path + this.encodeURL(item, true);
-  },
-
-  /**
-   * @param {Object} params
-   * @return {string}
-   */
-  getFilterValue(params) {
-    if (!params) { return ''; }
-    return params.filter || '';
-  },
-
-  /**
-   * @param {Object} params
-   * @return {number}
-   */
-  getOffsetValue(params) {
-    if (params && params.offset) {
-      return params.offset;
-    }
-    return 0;
-  },
-},
-URLEncodingBehavior,
-];
-
-// eslint-disable-next-line no-unused-vars
-function defineEmptyMixin() {
-  // This is a temporary function.
-  // Polymer linter doesn't process correctly the following code:
-  // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
-  // To workaround this issue, the mock mixin is declared in this method.
-  // In the following changes, legacy behaviors will be converted to mixins.
-
-  /**
-   * @polymer
-   * @mixinFunction
-   */
-  const ListViewMixin = base => // eslint-disable-line no-unused-vars
-    class extends base {
-      computeLoadingClass(loading) {}
-
-      computeShownItems(items) {}
-    };
-}
-
-// 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.ListViewBehavior = ListViewBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
deleted file mode 100644
index fafec9d..0000000
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js
+++ /dev/null
@@ -1,301 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Tags identifying ChangeMessages that move change into WIP state.
-const WIP_TAGS = [
-  'autogenerated:gerrit:newWipPatchSet',
-  'autogenerated:gerrit:setWorkInProgress',
-];
-
-// Tags identifying ChangeMessages that move change out of WIP state.
-const READY_TAGS = [
-  'autogenerated:gerrit:setReadyForReview',
-];
-
-/** @polymerBehavior Gerrit.PatchSetBehavior*/
-export const PatchSetBehavior = {
-  EDIT_NAME: 'edit',
-  PARENT_NAME: 'PARENT',
-
-  /**
-   * As patchNum can be either a string (e.g. 'edit', 'PARENT') OR a number,
-   * this function checks for patchNum equality.
-   *
-   * @param {string|number} a
-   * @param {string|number|undefined} b Undefined sometimes because
-   *    computeLatestPatchNum can return undefined.
-   * @return {boolean}
-   */
-  patchNumEquals(a, b) {
-    return a + '' === b + '';
-  },
-
-  /**
-   * Whether the given patch is a numbered parent of a merge (i.e. a negative
-   * number).
-   *
-   * @param  {string|number} n
-   * @return {boolean}
-   */
-  isMergeParent(n) {
-    return (n + '')[0] === '-';
-  },
-
-  /**
-   * Given an object of revisions, get a particular revision based on patch
-   * num.
-   *
-   * @param {Object} revisions The object of revisions given by the API
-   * @param {number|string} patchNum The number index of the revision
-   * @return {Object} The correspondent revision obj from {revisions}
-   */
-  getRevisionByPatchNum(revisions, patchNum) {
-    for (const rev of Object.values(revisions || {})) {
-      if (PatchSetBehavior.patchNumEquals(rev._number, patchNum)) {
-        return rev;
-      }
-    }
-  },
-
-  /**
-   * Find change edit base revision if change edit exists.
-   *
-   * @param {!Array<!Object>} revisions The revisions array.
-   * @return {Object} change edit parent revision or null if change edit
-   *     doesn't exist.
-   */
-  findEditParentRevision(revisions) {
-    const editInfo =
-        revisions.find(info => info._number ===
-            PatchSetBehavior.EDIT_NAME);
-
-    if (!editInfo) { return null; }
-
-    return revisions.find(info => info._number === editInfo.basePatchNum) ||
-        null;
-  },
-
-  /**
-   * Find change edit base patch set number if change edit exists.
-   *
-   * @param {!Array<!Object>} revisions The revisions array.
-   * @return {number} Change edit patch set number or -1.
-   */
-  findEditParentPatchNum(revisions) {
-    const revisionInfo =
-        PatchSetBehavior.findEditParentRevision(revisions);
-    return revisionInfo ? revisionInfo._number : -1;
-  },
-
-  /**
-   * Sort given revisions array according to the patch set number, in
-   * descending order.
-   * The sort algorithm is change edit aware. Change edit has patch set number
-   * equals 'edit', but must appear after the patch set it was based on.
-   * Example: change edit is based on patch set 2, and another patch set was
-   * uploaded after change edit creation, the sorted order should be:
-   * 3, edit, 2, 1.
-   *
-   * @param {!Array<!Object>} revisions The revisions array
-   * @return {!Array<!Object>} The sorted {revisions} array
-   */
-  sortRevisions(revisions) {
-    const editParent =
-        PatchSetBehavior.findEditParentPatchNum(revisions);
-    // Map a normal patchNum to 2 * (patchNum - 1) + 1... I.e. 1 -> 1,
-    // 2 -> 3, 3 -> 5, etc.
-    // Map an edit to the patchNum of parent*2... I.e. edit on 2 -> 4.
-    const num = r => (r._number === PatchSetBehavior.EDIT_NAME ?
-      2 * editParent :
-      2 * (r._number - 1) + 1);
-    return revisions.sort((a, b) => num(b) - num(a));
-  },
-
-  /**
-   * Construct a chronological list of patch sets derived from change details.
-   * Each element of this list is an object with the following properties:
-   *
-   *   * num {number} The number identifying the patch set
-   *   * desc {!string} Optional patch set description
-   *   * wip {boolean} If true, this patch set was never subject to review.
-   *   * sha {string} hash of the commit
-   *
-   * The wip property is determined by the change's current work_in_progress
-   * property and its log of change messages.
-   *
-   * @param {!Object} change The change details
-   * @return {!Array<!Object>} Sorted list of patch set objects, as described
-   *     above
-   */
-  computeAllPatchSets(change) {
-    if (!change) { return []; }
-    let patchNums = [];
-    if (change.revisions && Object.keys(change.revisions).length) {
-      const revisions = Object.keys(change.revisions)
-          .map(sha => Object.assign({sha}, change.revisions[sha]));
-      patchNums =
-        PatchSetBehavior.sortRevisions(revisions)
-            .map(e => {
-              // TODO(kaspern): Mark which patchset an edit was made on, if an
-              // edit exists -- perhaps with a temporary description.
-              return {
-                num: e._number,
-                desc: e.description,
-                sha: e.sha,
-              };
-            });
-    }
-    return PatchSetBehavior._computeWipForPatchSets(change, patchNums);
-  },
-
-  /**
-   * Populate the wip properties of the given list of patch sets.
-   *
-   * @param {!Object} change The change details
-   * @param {!Array<!Object>} patchNums Sorted list of patch set objects, as
-   *     generated by computeAllPatchSets
-   * @return {!Array<!Object>} The given list of patch set objects, with the
-   *     wip property set on each of them
-   */
-  _computeWipForPatchSets(change, patchNums) {
-    if (!change.messages || !change.messages.length) {
-      return patchNums;
-    }
-    const psWip = {};
-    let wip = change.work_in_progress;
-    for (let i = 0; i < change.messages.length; i++) {
-      const msg = change.messages[i];
-      if (WIP_TAGS.includes(msg.tag)) {
-        wip = true;
-      } else if (READY_TAGS.includes(msg.tag)) {
-        wip = false;
-      }
-      if (psWip[msg._revision_number] !== false) {
-        psWip[msg._revision_number] = wip;
-      }
-    }
-
-    for (let i = 0; i < patchNums.length; i++) {
-      patchNums[i].wip = psWip[patchNums[i].num];
-    }
-    return patchNums;
-  },
-
-  /** @return {number|undefined} */
-  computeLatestPatchNum(allPatchSets) {
-    if (!allPatchSets || !allPatchSets.length) { return undefined; }
-    if (allPatchSets[0].num === PatchSetBehavior.EDIT_NAME) {
-      return allPatchSets[1].num;
-    }
-    return allPatchSets[0].num;
-  },
-
-  /** @return {boolean} */
-  hasEditBasedOnCurrentPatchSet(allPatchSets) {
-    if (!allPatchSets || allPatchSets.length < 2) { return false; }
-    return allPatchSets[0].num === PatchSetBehavior.EDIT_NAME;
-  },
-
-  /** @return {boolean} */
-  hasEditPatchsetLoaded(patchRangeRecord) {
-    const patchRange = patchRangeRecord.base;
-    if (!patchRange) { return false; }
-    return patchRange.patchNum === PatchSetBehavior.EDIT_NAME ||
-        patchRange.basePatchNum === PatchSetBehavior.EDIT_NAME;
-  },
-
-  /**
-   * Check whether there is no newer patch than the latest patch that was
-   * available when this change was loaded.
-   *
-   * @return {Promise<!Object>} A promise that yields true if the latest patch
-   *     has been loaded, and false if a newer patch has been uploaded in the
-   *     meantime. The promise is rejected on network error.
-   */
-  fetchChangeUpdates(change, restAPI) {
-    const knownLatest = PatchSetBehavior.computeLatestPatchNum(
-        PatchSetBehavior.computeAllPatchSets(change));
-    return restAPI.getChangeDetail(change._number)
-        .then(detail => {
-          if (!detail) {
-            const error = new Error('Unable to check for latest patchset.');
-            return Promise.reject(error);
-          }
-          const actualLatest = PatchSetBehavior.computeLatestPatchNum(
-              PatchSetBehavior.computeAllPatchSets(detail));
-          return {
-            isLatest: actualLatest <= knownLatest,
-            newStatus: change.status !== detail.status ? detail.status : null,
-            newMessages: change.messages.length < detail.messages.length,
-          };
-        });
-  },
-
-  /**
-   * @param {number|string} patchNum
-   * @param {!Array<!Object>} revisions A sorted array of revisions.
-   *
-   * @return {number} The index of the revision with the given patchNum.
-   */
-  findSortedIndex(patchNum, revisions) {
-    revisions = revisions || [];
-    const findNum = rev => rev._number + '' === patchNum + '';
-    return revisions.findIndex(findNum);
-  },
-
-  /**
-   * Convert parent indexes from patch range expressions to numbers.
-   * For example, in a patch range expression `"-3"` becomes `3`.
-   *
-   * @param {number|string} rangeBase
-   * @return {number}
-   */
-  getParentIndex(rangeBase) {
-    return -parseInt(rangeBase + '', 10);
-  },
-};
-
-// eslint-disable-next-line no-unused-vars
-function defineEmptyMixin() {
-  // This is a temporary function.
-  // Polymer linter doesn't process correctly the following code:
-  // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
-  // To workaround this issue, the mock mixin is declared in this method.
-  // In the following changes, legacy behaviors will be converted to mixins.
-
-  /**
-   * @polymer
-   * @mixinFunction
-   */
-  const PatchSetMixin = base => // eslint-disable-line no-unused-vars
-    class extends base {
-      computeLatestPatchNum(allPatchSets) {}
-
-      hasEditPatchsetLoaded(patchRangeRecord) {}
-
-      hasEditBasedOnCurrentPatchSet(allPatchSets) {}
-
-      computeAllPatchSets(change) {}
-    };
-}
-
-// 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.PatchSetBehavior = PatchSetBehavior;
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
deleted file mode 100644
index be8c811..0000000
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {SpecialFilePath} from '../../constants/constants.js';
-
-/** @polymerBehavior Gerrit.PathListBehavior */
-export const PathListBehavior = {
-
-  /**
-   * @param {string} a
-   * @param {string} b
-   * @return {number}
-   */
-  specialFilePathCompare(a, b) {
-    // The commit message always goes first.
-    if (a === SpecialFilePath.COMMIT_MESSAGE) {
-      return -1;
-    }
-    if (b === SpecialFilePath.COMMIT_MESSAGE) {
-      return 1;
-    }
-
-    // The merge list always comes next.
-    if (a === SpecialFilePath.MERGE_LIST) {
-      return -1;
-    }
-    if (b === SpecialFilePath.MERGE_LIST) {
-      return 1;
-    }
-
-    const aLastDotIndex = a.lastIndexOf('.');
-    const aExt = a.substr(aLastDotIndex + 1);
-    const aFile = a.substr(0, aLastDotIndex) || a;
-
-    const bLastDotIndex = b.lastIndexOf('.');
-    const bExt = b.substr(bLastDotIndex + 1);
-    const bFile = b.substr(0, bLastDotIndex) || b;
-
-    // Sort header files above others with the same base name.
-    const headerExts = ['h', 'hxx', 'hpp'];
-    if (aFile.length > 0 && aFile === bFile) {
-      if (headerExts.includes(aExt) && headerExts.includes(bExt)) {
-        return a.localeCompare(b);
-      }
-      if (headerExts.includes(aExt)) {
-        return -1;
-      }
-      if (headerExts.includes(bExt)) {
-        return 1;
-      }
-    }
-    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 === SpecialFilePath.COMMIT_MESSAGE) {
-      return 'Commit message';
-    } else if (path === SpecialFilePath.MERGE_LIST) {
-      return 'Merge list';
-    }
-    return path;
-  },
-
-  isMagicPath(path) {
-    return !!path &&
-        (path === SpecialFilePath.COMMIT_MESSAGE || path ===
-            SpecialFilePath.MERGE_LIST);
-  },
-
-  computeTruncatedPath(path) {
-    return PathListBehavior.truncatePath(
-        PathListBehavior.computeDisplayPath(path));
-  },
-
-  /**
-   * Truncates URLs to display filename only
-   * Example
-   * // returns '.../text.html'
-   * util.truncatePath.('dir/text.html');
-   * Example
-   * // returns 'text.html'
-   * util.truncatePath.('text.html');
-   *
-   * @param {string} path
-   * @param {number=} opt_threshold
-   * @return {string} Returns the truncated value of a URL.
-   */
-  truncatePath(path, opt_threshold) {
-    const threshold = opt_threshold || 1;
-    const pathPieces = path.split('/');
-
-    if (pathPieces.length <= threshold) { return path; }
-
-    const index = pathPieces.length - threshold;
-    // Character is an ellipsis.
-    return `\u2026/${pathPieces.slice(index).join('/')}`;
-  },
-};
-
-// 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.PathListBehavior = PathListBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.js b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.js
deleted file mode 100644
index 94e82e2..0000000
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma.js';
-import {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;
-    const testFiles = [
-      '/a.h',
-      '/MERGE_LIST',
-      '/a.cpp',
-      '/COMMIT_MSG',
-      '/asdasd',
-      '/mrPeanutbutter.py',
-    ];
-    assert.deepEqual(
-        testFiles.sort(sort),
-        [
-          '/COMMIT_MSG',
-          '/MERGE_LIST',
-          '/a.h',
-          '/a.cpp',
-          '/asdasd',
-          '/mrPeanutbutter.py',
-        ]);
-  });
-
-  test('file display name', () => {
-    const name = PathListBehavior.computeDisplayPath;
-    assert.equal(name('/foo/bar/baz'), '/foo/bar/baz');
-    assert.equal(name('/foobarbaz'), '/foobarbaz');
-    assert.equal(name('/COMMIT_MSG'), 'Commit message');
-    assert.equal(name('/MERGE_LIST'), 'Merge list');
-  });
-
-  test('isMagicPath', () => {
-    const isMagic = PathListBehavior.isMagicPath;
-    assert.isFalse(isMagic(undefined));
-    assert.isFalse(isMagic('/foo.cc'));
-    assert.isTrue(isMagic('/COMMIT_MSG'));
-    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';
-    let shortenedPath = truncatePath(path);
-    // The expected path is truncated with an ellipsis.
-    const expectedPath = '\u2026/file.js';
-    assert.equal(shortenedPath, expectedPath);
-
-    path = 'level2/file.js';
-    shortenedPath = truncatePath(path);
-    assert.equal(shortenedPath, expectedPath);
-  });
-
-  test('truncatePath with opt_threshold', () => {
-    const truncatePath = PathListBehavior.truncatePath;
-    let path = 'level1/level2/level3/level4/file.js';
-    let shortenedPath = truncatePath(path, 2);
-    // The expected path is truncated with an ellipsis.
-    const expectedPath = '\u2026/level4/file.js';
-    assert.equal(shortenedPath, expectedPath);
-
-    path = 'level2/file.js';
-    shortenedPath = truncatePath(path, 2);
-    assert.equal(shortenedPath, path);
-  });
-
-  test('truncatePath with short path should not add ellipsis', () => {
-    const truncatePath = PathListBehavior.truncatePath;
-    const path = 'file.js';
-    const expectedPath = 'file.js';
-    const shortenedPath = truncatePath(path);
-    assert.equal(shortenedPath, expectedPath);
-  });
-});
-
diff --git a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js b/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
deleted file mode 100644
index 3ba2e60..0000000
--- a/polygerrit-ui/app/behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js
+++ /dev/null
@@ -1,38 +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.RepoPluginConfig*/
-export const RepoPluginConfig = {
-  // Should be kept in sync with
-  // gerrit/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.java.
-  ENTRY_TYPES: {
-    ARRAY: 'ARRAY',
-    BOOLEAN: 'BOOLEAN',
-    INT: 'INT',
-    LIST: 'LIST',
-    LONG: 'LONG',
-    STRING: 'STRING',
-  },
-  PLUGIN_CONFIG_CHANGED: 'plugin-config-changed',
-};
-
-// 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.RepoPluginConfig = RepoPluginConfig;
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
deleted file mode 100644
index cc8b55a..0000000
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../elements/shared/gr-tooltip/gr-tooltip.js';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {getRootElement} from '../../scripts/rootElement.js';
-
-const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
-
-/** @polymerBehavior Gerrit.TooltipBehavior */
-export const TooltipBehavior = {
-
-  properties: {
-    hasTooltip: {
-      type: Boolean,
-      observer: '_setupTooltipListeners',
-    },
-    positionBelow: {
-      type: Boolean,
-      value: false,
-      reflectToAttribute: true,
-    },
-
-    _isTouchDevice: {
-      type: Boolean,
-      value() {
-        return 'ontouchstart' in document.documentElement;
-      },
-    },
-    _tooltip: Object,
-    _titleText: String,
-    _hasSetupTooltipListeners: {
-      type: Boolean,
-      value: false,
-    },
-  },
-
-  /** @override */
-  detached() {
-    // NOTE: if you define your own `detached` in your component
-    // then this won't take affect (as its not a class yet)
-    this._handleHideTooltip();
-    this.removeEventListener('mouseenter', this._mouseenterHandler);
-  },
-
-  _setupTooltipListeners() {
-    if (!this._mouseenterHandler) {
-      this._mouseenterHandler = this._handleShowTooltip.bind(this);
-    }
-
-    if (!this.hasTooltip) {
-      // if attribute set to false, remove the listener
-      this.removeEventListener('mouseenter', this._mouseenterHandler);
-      this._hasSetupTooltipListeners = false;
-      return;
-    }
-
-    if (this._hasSetupTooltipListeners) {
-      return;
-    }
-    this._hasSetupTooltipListeners = true;
-
-    this.addEventListener('mouseenter', this._mouseenterHandler);
-  },
-
-  _handleShowTooltip(e) {
-    if (this._isTouchDevice) { return; }
-
-    if (!this.hasAttribute('title') ||
-        this.getAttribute('title') === '' ||
-        this._tooltip) {
-      return;
-    }
-
-    // Store the title attribute text then set it to an empty string to
-    // prevent it from showing natively.
-    this._titleText = this.getAttribute('title');
-    this.setAttribute('title', '');
-
-    const tooltip = document.createElement('gr-tooltip');
-    tooltip.text = this._titleText;
-    tooltip.maxWidth = this.getAttribute('max-width');
-    tooltip.positionBelow = this.getAttribute('position-below');
-
-    // Set visibility to hidden before appending to the DOM so that
-    // calculations can be made based on the element’s size.
-    tooltip.style.visibility = 'hidden';
-    getRootElement().appendChild(tooltip);
-    this._positionTooltip(tooltip);
-    tooltip.style.visibility = null;
-
-    this._tooltip = tooltip;
-    this.listen(window, 'scroll', '_handleWindowScroll');
-    this.listen(this, 'mouseleave', '_handleHideTooltip');
-    this.listen(this, 'click', '_handleHideTooltip');
-  },
-
-  _handleHideTooltip(e) {
-    if (this._isTouchDevice) { return; }
-    if (!this.hasAttribute('title') ||
-        this._titleText == null) {
-      return;
-    }
-
-    this.unlisten(window, 'scroll', '_handleWindowScroll');
-    this.unlisten(this, 'mouseleave', '_handleHideTooltip');
-    this.unlisten(this, 'click', '_handleHideTooltip');
-    this.setAttribute('title', this._titleText);
-    if (this._tooltip && this._tooltip.parentNode) {
-      this._tooltip.parentNode.removeChild(this._tooltip);
-    }
-    this._tooltip = null;
-  },
-
-  _handleWindowScroll(e) {
-    if (!this._tooltip) { return; }
-
-    this._positionTooltip(this._tooltip);
-  },
-
-  _positionTooltip(tooltip) {
-    // This flush is needed for tooltips to be positioned correctly in Firefox
-    // and Safari.
-    flush();
-    const rect = this.getBoundingClientRect();
-    const boxRect = tooltip.getBoundingClientRect();
-    const parentRect = tooltip.parentElement.getBoundingClientRect();
-    const top = rect.top - parentRect.top;
-    const left =
-        rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
-    const right = parentRect.width - left - boxRect.width;
-    if (left < 0) {
-      tooltip.updateStyles({
-        '--gr-tooltip-arrow-center-offset': left + 'px',
-      });
-    } else if (right < 0) {
-      tooltip.updateStyles({
-        '--gr-tooltip-arrow-center-offset': (-0.5 * right) + 'px',
-      });
-    }
-    tooltip.style.left = Math.max(0, left) + 'px';
-
-    if (!this.positionBelow) {
-      tooltip.style.top = Math.max(0, top) + 'px';
-      tooltip.style.transform = 'translateY(calc(-100% - ' + BOTTOM_OFFSET +
-          'px))';
-    } else {
-      tooltip.style.top = top + rect.height + BOTTOM_OFFSET + 'px';
-    }
-  },
-};
-
-// 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.TooltipBehavior = TooltipBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
deleted file mode 100644
index 5c9e911..0000000
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/** @polymerBehavior Gerrit.URLEncodingBehavior */
-export const URLEncodingBehavior = {
-  /**
-   * Pretty-encodes a URL. Double-encodes the string, and then replaces
-   *   benevolent characters for legibility.
-   *
-   * @param {string} url
-   * @param {boolean=} replaceSlashes
-   * @return {string}
-   */
-  encodeURL(url, replaceSlashes) {
-    // @see Issue 4255 regarding double-encoding.
-    let output = encodeURIComponent(encodeURIComponent(url));
-    // @see Issue 4577 regarding more readable URLs.
-    output = output.replace(/%253A/g, ':');
-    output = output.replace(/%2520/g, '+');
-    if (replaceSlashes) {
-      output = output.replace(/%252F/g, '/');
-    }
-    return output;
-  },
-
-  /**
-   * Single decode for URL components. Will decode plus signs ('+') to spaces.
-   * Note: because this function decodes once, it is not the inverse of
-   * encodeURL.
-   *
-   * @param {string} url
-   * @return {string}
-   */
-  singleDecodeURL(url) {
-    const withoutPlus = url.replace(/\+/g, '%20');
-    return decodeURIComponent(withoutPlus);
-  },
-};
-
-// 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.URLEncodingBehavior = URLEncodingBehavior;
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.js b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.js
deleted file mode 100644
index 1fe2874..0000000
--- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior_test.js
+++ /dev/null
@@ -1,75 +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 '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {URLEncodingBehavior} from './gr-url-encoding-behavior.js';
-
-const basicFixture =
-    fixtureFromElement('gr-url-encoding-behavior-test-element');
-
-suite('gr-url-encoding-behavior tests', () => {
-  let element;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'gr-url-encoding-behavior-test-element',
-      behaviors: [URLEncodingBehavior],
-    });
-  });
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  suite('encodeURL', () => {
-    test('double encodes', () => {
-      assert.equal(element.encodeURL('abc?123'), 'abc%253F123');
-      assert.equal(element.encodeURL('def/ghi'), 'def%252Fghi');
-      assert.equal(element.encodeURL('jkl'), 'jkl');
-      assert.equal(element.encodeURL(''), '');
-    });
-
-    test('does not convert colons', () => {
-      assert.equal(element.encodeURL('mno:pqr'), 'mno:pqr');
-    });
-
-    test('converts spaces to +', () => {
-      assert.equal(element.encodeURL('words with spaces'), 'words+with+spaces');
-    });
-
-    test('does not convert slashes when configured', () => {
-      assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
-    });
-
-    test('does not convert slashes when configured', () => {
-      assert.equal(element.encodeURL('stu/vwx', true), 'stu/vwx');
-    });
-  });
-
-  suite('singleDecodeUrl', () => {
-    test('single decodes', () => {
-      assert.equal(element.singleDecodeURL('abc%3Fdef'), 'abc?def');
-    });
-
-    test('converts + to space', () => {
-      assert.equal(element.singleDecodeURL('ghi+jkl'), 'ghi jkl');
-    });
-  });
-});
-
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts
deleted file mode 100644
index 5671387..0000000
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.ts
+++ /dev/null
@@ -1,245 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {getBaseUrl} from '../../utils/url-util';
-import {ChangeStatus} from '../../constants/constants';
-
-// WARNING: The types below can be completely wrong!
-// The types was added to avoid eslinter and typescript errors.
-// Correct typing requires more analysis and (probably) code changes.
-// This will be done later.
-type ChangeNum = string; // This can be wrong! See WARNING above
-type PatchNum = string; // This can be wrong! See WARNING above
-
-// This can be wrong! See WARNING above
-interface Change {
-  status: string; // This can be wrong! See WARNING above
-  mergeable: boolean; // This can be wrong! See WARNING above
-  work_in_progress: boolean; // This can be wrong! See WARNING above
-  is_private: boolean; // This can be wrong! See WARNING above
-  submittable: boolean; // This can be wrong! See WARNING above
-}
-
-// This can be wrong! See WARNING above
-interface ChangeStatusesOptions {
-  mergeable: boolean; // This can be wrong! See WARNING above
-  submitEnabled: boolean; // This can be wrong! See WARNING above
-}
-
-/** @polymerBehavior Gerrit.RESTClientBehavior */
-export const RESTClientBehavior = [
-  {
-    ChangeDiffType: {
-      ADDED: 'ADDED',
-      COPIED: 'COPIED',
-      DELETED: 'DELETED',
-      MODIFIED: 'MODIFIED',
-      RENAMED: 'RENAMED',
-      REWRITE: 'REWRITE',
-    },
-
-    // Must be kept in sync with the ListChangesOption enum and protobuf.
-    ListChangesOption: {
-      LABELS: 0,
-      DETAILED_LABELS: 8,
-
-      // Return information on the current patch set of the change.
-      CURRENT_REVISION: 1,
-      ALL_REVISIONS: 2,
-
-      // If revisions are included, parse the commit object.
-      CURRENT_COMMIT: 3,
-      ALL_COMMITS: 4,
-
-      // If a patch set is included, include the files of the patch set.
-      CURRENT_FILES: 5,
-      ALL_FILES: 6,
-
-      // If accounts are included, include detailed account info.
-      DETAILED_ACCOUNTS: 7,
-
-      // Include messages associated with the change.
-      MESSAGES: 9,
-
-      // Include allowed actions client could perform.
-      CURRENT_ACTIONS: 10,
-
-      // Set the reviewed boolean for the caller.
-      REVIEWED: 11,
-
-      // Include download commands for the caller.
-      DOWNLOAD_COMMANDS: 13,
-
-      // Include patch set weblinks.
-      WEB_LINKS: 14,
-
-      // Include consistency check results.
-      CHECK: 15,
-
-      // Include allowed change actions client could perform.
-      CHANGE_ACTIONS: 16,
-
-      // Include a copy of commit messages including review footers.
-      COMMIT_FOOTERS: 17,
-
-      // Include push certificate information along with any patch sets.
-      PUSH_CERTIFICATES: 18,
-
-      // Include change's reviewer updates.
-      REVIEWER_UPDATES: 19,
-
-      // Set the submittable boolean.
-      SUBMITTABLE: 20,
-
-      // If tracking ids are included, include detailed tracking ids info.
-      TRACKING_IDS: 21,
-
-      // Skip mergeability data.
-      SKIP_MERGEABLE: 22,
-
-      /**
-       * Skip diffstat computation that compute the insertions field (number of lines inserted) and
-       * deletions field (number of lines deleted)
-       */
-      SKIP_DIFFSTAT: 23,
-    },
-
-    listChangesOptionsToHex(...args: number[]) {
-      let v = 0;
-      for (let i = 0; i < args.length; i++) {
-        v |= 1 << args[i];
-      }
-      return v.toString(16);
-    },
-
-    /**
-     *  @return {string}
-     */
-    changeBaseURL(project: string, changeNum: ChangeNum, patchNum: PatchNum) {
-      let v =
-        getBaseUrl() +
-        '/changes/' +
-        encodeURIComponent(project) +
-        '~' +
-        changeNum;
-      if (patchNum) {
-        v += '/revisions/' + patchNum;
-      }
-      return v;
-    },
-
-    changePath(changeNum: ChangeNum) {
-      return getBaseUrl() + '/c/' + changeNum;
-    },
-
-    changeIsOpen(change?: Change) {
-      return change && change.status === ChangeStatus.NEW;
-    },
-
-    /**
-     * @param {!Object} change
-     * @param {!Object=} opt_options
-     *
-     * @return {!Array}
-     */
-    changeStatuses(change: Change, opt_options?: ChangeStatusesOptions) {
-      const states = [];
-      if (change.status === ChangeStatus.MERGED) {
-        states.push('Merged');
-      } else if (change.status === ChangeStatus.ABANDONED) {
-        states.push('Abandoned');
-      } else if (
-        change.mergeable === false ||
-        (opt_options && opt_options.mergeable === false)
-      ) {
-        // 'mergeable' prop may not always exist (@see Issue 6819)
-        states.push('Merge Conflict');
-      }
-      if (change.work_in_progress) {
-        states.push('WIP');
-      }
-      if (change.is_private) {
-        states.push('Private');
-      }
-
-      // If there are any pre-defined statuses, only return those. Otherwise,
-      // will determine the derived status.
-      if (states.length || !opt_options) {
-        return states;
-      }
-
-      // If no missing requirements, either active or ready to submit.
-      if (change.submittable && opt_options.submitEnabled) {
-        states.push('Ready to submit');
-      } else {
-        // Otherwise it is active.
-        states.push('Active');
-      }
-      return states;
-    },
-
-    /**
-     * @param {!Object} change
-     * @return {string}
-     */
-    changeStatusString(change: Change) {
-      return this.changeStatuses(change).join(', ');
-    },
-  },
-];
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
-// @ts-ignore
-function defineEmptyMixin() {
-  // This is a temporary function.
-  // Polymer linter doesn't process correctly the following code:
-  // class MyElement extends Polymer.mixinBehaviors([legacyBehaviors], ...) {...}
-  // To workaround this issue, the mock mixin is declared in this method.
-  // In the following changes, legacy behaviors will be converted to mixins.
-
-  /**
-   * @polymer
-   * @mixinFunction
-   */
-  const RESTClientMixin = (
-    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
-    // @ts-ignore
-    base
-  ) =>
-    class extends base {
-      // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
-      // @ts-ignore
-      changeStatusString(change) {}
-
-      // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
-      // @ts-ignore
-      changeStatuses(change, opt_options) {}
-    };
-  // We can't apply @ts-ignore directly to RESTClientMixin - it breaks polylint
-  // tests (polylinter expects that @polymer and @mixinFunction appear right
-  // before the mixin definition). To workaround it and suppress error about
-  // unused variable use a temporary variable.
-  // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
-  // @ts-ignore
-  const tmp = RESTClientMixin;
-}
-
-// 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.RESTClientBehavior = RESTClientBehavior;
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 632387e..d1255d2 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
@@ -23,11 +23,10 @@
 import '../gr-permission/gr-permission.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.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 {htmlTemplate} from './gr-access-section_html.js';
-import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import {AccessPermissions, toSortedPermissionsArray} from '../../../utils/access-util.js';
 
 /**
  * Fired when the section has been modified or removed.
@@ -52,11 +51,9 @@
 /**
  * @extends PolymerElement
  */
-class GrAccessSection extends mixinBehaviors( [
-  AccessBehavior,
-], GestureEventListeners(
+class GrAccessSection extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-access-section'; }
@@ -100,7 +97,7 @@
   }
 
   _updateSection(section) {
-    this._permissions = this.toSortedArray(section.value.permissions);
+    this._permissions = toSortedPermissionsArray(section.value.permissions);
     this._originalId = section.id;
   }
 
@@ -149,11 +146,11 @@
       return [];
     }
     if (name === GLOBAL_NAME) {
-      allPermissions = this.toSortedArray(capabilities);
+      allPermissions = toSortedPermissionsArray(capabilities);
     } else {
       const labelOptions = this._computeLabelOptions(labels);
       allPermissions = labelOptions.concat(
-          this.toSortedArray(this.permissionValues));
+          toSortedPermissionsArray(AccessPermissions));
     }
     return allPermissions
         .filter(permission => !this.section.value.permissions[permission.id]);
@@ -191,11 +188,11 @@
     return labelOptions;
   }
 
-  _computePermissionName(name, permission, permissionValues, capabilities) {
+  _computePermissionName(name, permission, capabilities) {
     if (name === GLOBAL_NAME) {
       return capabilities[permission.id].name;
-    } else if (permissionValues[permission.id]) {
-      return permissionValues[permission.id].name;
+    } else if (AccessPermissions[permission.id]) {
+      return AccessPermissions[permission.id].name;
     } else if (permission.value.label) {
       let behalfOf = '';
       if (permission.id.startsWith('labelAs-')) {
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
rename to polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts
index e9c16241..7c9f28b 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -114,7 +114,7 @@
       <div class="sectionContent">
         <template is="dom-repeat" items="{{_permissions}}" as="permission">
           <gr-permission
-            name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]"
+            name="[[_computePermissionName(section.id, permission, capabilities)]]"
             permission="{{permission}}"
             labels="[[labels]]"
             section="[[section.id]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
index 2d7096f..8749158 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-access-section.js';
+import {AccessPermissions, toSortedPermissionsArray} from '../../../utils/access-util.js';
 
 const fixture = fixtureFromElement('gr-access-section');
 
@@ -117,20 +118,14 @@
     });
 
     test('_computePermissions', () => {
-      sinon.stub(element, 'toSortedArray').returns(
-          [{
-            id: 'push',
-            value: {
-              rules: {},
-            },
-          },
-          {
-            id: 'read',
-            value: {
-              rules: {},
-            },
-          },
-          ]);
+      const capabilities = {
+        push: {
+          rules: {},
+        },
+        read: {
+          rules: {},
+        },
+      };
 
       const expectedPermissions = [{
         id: 'push',
@@ -159,22 +154,16 @@
       // For global capabilities, just return the sorted array filtered by
       // existing permissions.
       let name = 'GLOBAL_CAPABILITIES';
-      assert.deepEqual(element._computePermissions(name, element.capabilities,
+      assert.deepEqual(element._computePermissions(name, capabilities,
           element.labels), expectedPermissions);
 
-      // Uses the capabilities array to come up with possible values.
-      assert.isTrue(element.toSortedArray.lastCall.
-          calledWithExactly(element.capabilities));
-
       // For everything else, include possible label values before filtering.
       name = 'refs/for/*';
-      assert.deepEqual(element._computePermissions(name, element.capabilities,
-          element.labels), labelOptions.concat(expectedPermissions));
-
-      // Uses permissionValues (defined in gr-access-behavior) to come up with
-      // possible values.
-      assert.isTrue(element.toSortedArray.lastCall.
-          calledWithExactly(element.permissionValues));
+      assert.deepEqual(
+          element._computePermissions(name, capabilities, element.labels),
+          labelOptions
+              .concat(toSortedPermissionsArray(AccessPermissions))
+              .filter(permission => permission.id !== 'read'));
     });
 
     test('_computePermissionName', () => {
@@ -184,7 +173,7 @@
         value: {},
       };
       assert.equal(element._computePermissionName(name, permission,
-          element.permissionValues, element.capabilities),
+          element.capabilities),
       element.capabilities[permission.id].name);
 
       name = 'refs/for/*';
@@ -194,8 +183,8 @@
       };
 
       assert.equal(element._computePermissionName(
-          name, permission, element.permissionValues, element.capabilities),
-      element.permissionValues[permission.id].name);
+          name, permission, element.capabilities),
+      AccessPermissions[permission.id].name);
 
       name = 'refs/for/*';
       permission = {
@@ -206,7 +195,7 @@
       };
 
       assert.equal(element._computePermissionName(name, permission,
-          element.permissionValues, element.capabilities),
+          element.capabilities),
       'Label Code-Review');
 
       permission = {
@@ -217,7 +206,7 @@
       };
 
       assert.equal(element._computePermissionName(name, permission,
-          element.permissionValues, element.capabilities),
+          element.capabilities),
       'Label Code-Review(On Behalf Of)');
     });
 
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 865fd33..fd1f492 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
@@ -22,21 +22,18 @@
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-create-group-dialog/gr-create-group-dialog.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 {htmlTemplate} from './gr-admin-group-list_html.js';
-import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
  * @appliesMixin ListViewMixin
  * @extends PolymerElement
  */
-class GrAdminGroupList extends mixinBehaviors( [
-  ListViewBehavior,
-], GestureEventListeners(
+class GrAdminGroupList extends ListViewMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
rename to polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
index 4548a45..e22dc66 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 5b8896e..e555583 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
@@ -33,28 +33,23 @@
 import '../gr-repo-dashboards/gr-repo-dashboards.js';
 import '../gr-repo-detail-list/gr-repo-detail-list.js';
 import '../gr-repo-list/gr-repo-list.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 {htmlTemplate} from './gr-admin-view_html.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
-import {AdminNavBehavior} from '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getAdminLinks} from '../../../utils/admin-nav-util.js';
 
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
 /**
  * @extends PolymerElement
  */
-class GrAdminView extends mixinBehaviors( [
-  AdminNavBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrAdminView extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-admin-view'; }
@@ -134,7 +129,7 @@
         };
       }
 
-      return this.getAdminLinks(this._account,
+      return getAdminLinks(this._account,
           this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
           this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
           options)
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
rename to polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
index b62a41b..5e85a93 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
rename to polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.ts
index 3810d32..ce9ac9c 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 91573fc..c0b71c0 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
@@ -21,12 +21,10 @@
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-select/gr-select.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 {htmlTemplate} from './gr-create-change-dialog_html.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 const SUGGESTIONS_LIMIT = 15;
@@ -35,11 +33,9 @@
 /**
  * @extends PolymerElement
  */
-class GrCreateChangeDialog extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrCreateChangeDialog extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-create-change-dialog'; }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
rename to polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts
index f18da81..77e2c3b 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 ea77dd7..85a76f1 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
@@ -18,23 +18,19 @@
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-group-dialog_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
 import page from 'page/page.mjs';
 
 /**
  * @extends PolymerElement
  */
-class GrCreateGroupDialog extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrCreateGroupDialog extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-create-group-dialog'; }
@@ -62,8 +58,7 @@
   }
 
   _computeGroupUrl(groupId) {
-    return getBaseUrl() + '/admin/groups/' +
-        this.encodeURL(groupId, true);
+    return getBaseUrl() + '/admin/groups/' + encodeURL(groupId, true);
   }
 
   _updateGroupName(name) {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
rename to polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
index bc1f24c..d4ecc5d 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 635e3b1..3b9176c 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
@@ -20,13 +20,11 @@
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-select/gr-select.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 {htmlTemplate} from './gr-create-pointer-dialog_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
 import page from 'page/page.mjs';
 
 const DETAIL_TYPES = {
@@ -37,11 +35,9 @@
 /**
  * @extends PolymerElement
  */
-class GrCreatePointerDialog extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrCreatePointerDialog extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-create-pointer-dialog'; }
@@ -75,10 +71,10 @@
   _computeItemUrl(project) {
     if (this.itemDetail === DETAIL_TYPES.branches) {
       return getBaseUrl() + '/admin/repos/' +
-          this.encodeURL(this.repoName, true) + ',branches';
+          encodeURL(this.repoName, true) + ',branches';
     } else if (this.itemDetail === DETAIL_TYPES.tags) {
       return getBaseUrl() + '/admin/repos/' +
-          this.encodeURL(this.repoName, true) + ',tags';
+          encodeURL(this.repoName, true) + ',tags';
     }
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
rename to polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
index 62a2e0f..0b3d81ae 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 4872a39..9855523 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
@@ -21,23 +21,19 @@
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-select/gr-select.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 {htmlTemplate} from './gr-create-repo-dialog_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
 import page from 'page/page.mjs';
 
 /**
  * @extends PolymerElement
  */
-class GrCreateRepoDialog extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrCreateRepoDialog extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-create-repo-dialog'; }
@@ -95,7 +91,7 @@
 
   _computeRepoUrl(repoName) {
     return getBaseUrl() + '/admin/repos/' +
-        this.encodeURL(repoName, true);
+        encodeURL(repoName, true);
   }
 
   _updateRepoName(name) {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
rename to polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
index 680986c..02aabfe 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 2c0c1302..259a302 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
@@ -20,12 +20,11 @@
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-account-link/gr-account-link.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 {htmlTemplate} from './gr-group-audit-log_html.js';
-import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
@@ -33,9 +32,7 @@
 /**
  * @extends PolymerElement
  */
-class GrGroupAuditLog extends mixinBehaviors( [
-  ListViewBehavior,
-], GestureEventListeners(
+class GrGroupAuditLog extends ListViewMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
rename to polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
index 130efbb..1212685 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 98985c1..ced9c69 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
@@ -25,13 +25,11 @@
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.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 {htmlTemplate} from './gr-group-members_html.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 
 const SUGGESTIONS_LIMIT = 15;
 const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
@@ -42,11 +40,9 @@
 /**
  * @extends PolymerElement
  */
-class GrGroupMembers extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrGroupMembers extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-group-members'; }
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
rename to polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts
index 47657ac..2d3f8fc 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-form-styles">
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
rename to polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
index 7a843dc..aed73bf 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
index c6054b2..479f2b1 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
@@ -68,7 +68,7 @@
   });
 
   test('default values with external group', done => {
-    const groupExternal = Object.assign({}, group);
+    const groupExternal = {...group};
     groupExternal.id = 'external-group-id';
     groupStub.restore();
     groupStub = sinon.stub(
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 1aa3e4b..9675c92 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -24,12 +24,11 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-rule-editor/gr-rule-editor.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-permission_html.js';
-import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
+import {toSortedPermissionsArray} from '../../../utils/access-util.js';
 
 const MAX_AUTOCOMPLETE_RESULTS = 20;
 
@@ -49,11 +48,9 @@
  * @event added-permission-removed
  * @extends PolymerElement
  */
-class GrPermission extends mixinBehaviors( [
-  AccessBehavior,
-], GestureEventListeners(
+class GrPermission extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-permission'; }
@@ -182,7 +179,7 @@
   }
 
   _sortPermission(permission) {
-    this._rules = this.toSortedArray(permission.value.rules);
+    this._rules = toSortedPermissionsArray(permission.value.rules);
   }
 
   _computeSectionClass(editing, deleted) {
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
rename to polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
index ed4f64a..9795c92 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
rename to polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts
index be35035..c96b86c 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 868a7cd..3a54ad4 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
@@ -18,20 +18,17 @@
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-list-view/gr-list-view.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-plugin-list_html.js';
-import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
 
 /**
  * @appliesMixin ListViewMixin
  * @extends PolymerElement
  */
-class GrPluginList extends mixinBehaviors( [
-  ListViewBehavior,
-], GestureEventListeners(
+class GrPluginList extends ListViewMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.ts
similarity index 71%
rename from polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
rename to polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.ts
index 042364f..d5318b5 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.ts
@@ -14,14 +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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
     /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
   </style>
   <style include="gr-table-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+    .placeholder {
+      color: var(--deemphasized-text-color);
+    }
   </style>
   <gr-list-view
     filter="[[_filter]]"
@@ -36,6 +38,7 @@
         <tr class="headerRow">
           <th class="name topHeader">Plugin Name</th>
           <th class="version topHeader">Version</th>
+          <th class="apiVersion topHeader">API Version</th>
           <th class="status topHeader">Status</th>
         </tr>
         <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
@@ -53,7 +56,22 @@
                 [[item.id]]
               </template>
             </td>
-            <td class="version">[[item.version]]</td>
+            <td class="version">
+              <template is="dom-if" if="[[item.version]]">
+                [[item.version]]
+              </template>
+              <template is="dom-if" if="[[!item.version]]">
+                <span class="placeholder">--</span>
+              </template>
+            </td>
+            <td class="apiVersion">
+              <template is="dom-if" if="[[item.api_version]]">
+                [[item.api_version]]
+              </template>
+              <template is="dom-if" if="[[!item.api_version]]">
+                <span class="placeholder">--</span>
+              </template>
+            </td>
             <td class="status">[[_status(item)]]</td>
           </tr>
         </template>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
index a73c7cf..d60483e 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
@@ -25,13 +25,18 @@
 const pluginGenerator = () => {
   const plugin = {
     id: `test${++counter}`,
-    version: '3.0-SNAPSHOT',
     disabled: false,
   };
 
   if (counter !== 2) {
     plugin.index_url = `plugins/test${counter}/`;
   }
+  if (counter !== 3) {
+    plugin.version = `version-${counter}`;
+  }
+  if (counter !== 4) {
+    plugin.api_version = `api-version-${counter}`;
+  }
   return plugin;
 };
 
@@ -61,10 +66,11 @@
 
     test('plugin in the list is formatted correctly', done => {
       flush(() => {
-        assert.equal(element._plugins[2].id, 'test3');
-        assert.equal(element._plugins[2].index_url, 'plugins/test3/');
-        assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
-        assert.equal(element._plugins[2].disabled, false);
+        assert.equal(element._plugins[4].id, 'test5');
+        assert.equal(element._plugins[4].index_url, 'plugins/test5/');
+        assert.equal(element._plugins[4].version, 'version-5');
+        assert.equal(element._plugins[4].api_version, 'api-version-5');
+        assert.equal(element._plugins[4].disabled, false);
         done();
       });
     });
@@ -80,6 +86,25 @@
       });
     });
 
+    test('versions', done => {
+      flush(() => {
+        const versions = element.root.querySelectorAll('.version');
+        assert.equal(versions[2].innerText, 'version-2');
+        assert.equal(versions[3].innerText, '--');
+        done();
+      });
+    });
+
+    test('api versions', done => {
+      flush(() => {
+        const apiVersions = element.root.querySelectorAll(
+            '.apiVersion');
+        assert.equal(apiVersions[3].innerText, 'api-version-3');
+        assert.equal(apiVersions[4].innerText, '--');
+        done();
+      });
+    });
+
     test('_shownPlugins', () => {
       assert.equal(element._shownPlugins.length, 25);
     });
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 8308504..8f48382 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
@@ -20,15 +20,17 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-access-section/gr-access-section.js';
 import {flush, dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-access_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {
+  encodeURL,
+  getBaseUrl,
+  singleDecodeURL,
+} from '../../../utils/url-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {toSortedPermissionsArray} from '../../../utils/access-util.js';
 
 const Defs = {};
 
@@ -83,12 +85,9 @@
 /**
  * @extends PolymerElement
  */
-class GrRepoAccess extends mixinBehaviors( [
-  AccessBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrRepoAccess extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-repo-access'; }
@@ -183,10 +182,10 @@
           // Keep a copy of the original inherit from values separate from
           // the ones data bound to gr-autocomplete, so the original value
           // can be restored if the user cancels.
-          this._inheritsFrom = res.inherits_from ? Object.assign({},
-              res.inherits_from) : null;
-          this._originalInheritsFrom = res.inherits_from ? Object.assign({},
-              res.inherits_from) : null;
+          this._inheritsFrom = res.inherits_from ? ({
+            ...res.inherits_from}) : null;
+          this._originalInheritsFrom = res.inherits_from ? ({
+            ...res.inherits_from}) : null;
           // Initialize the filter value so when the user clicks edit, the
           // current value appears. If there is no parent repo, it is
           // initialized as an empty string.
@@ -197,7 +196,7 @@
           this._weblinks = res.config_web_links || [];
           this._canUpload = res.can_upload;
           this._ownerOf = res.owner_of || [];
-          return this.toSortedArray(this._local);
+          return toSortedPermissionsArray(this._local);
         }));
 
     promises.push(this.$.restAPI.getCapabilities(errFn)
@@ -283,7 +282,7 @@
     }
     // Restore inheritFrom.
     if (this._inheritsFrom) {
-      this._inheritsFrom = Object.assign({}, this._originalInheritsFrom);
+      this._inheritsFrom = {...this._originalInheritsFrom};
       this._inheritFromFilter = this._inheritsFrom.name;
     }
     for (const key of Object.keys(this._local)) {
@@ -395,11 +394,9 @@
     };
 
     const originalInheritsFromId = this._originalInheritsFrom ?
-      this.singleDecodeURL(this._originalInheritsFrom.id) :
-      null;
+      singleDecodeURL(this._originalInheritsFrom.id) : null;
     const inheritsFromId = this._inheritsFrom ?
-      this.singleDecodeURL(this._inheritsFrom.id) :
-      null;
+      singleDecodeURL(this._inheritsFrom.id) : null;
 
     const inheritFromChanged =
         // Inherit from changed
@@ -514,7 +511,7 @@
 
   _computeParentHref(repoName) {
     return getBaseUrl() +
-        `/admin/repos/${this.encodeURL(repoName, true)},access`;
+        `/admin/repos/${encodeURL(repoName, true)},access`;
   }
 }
 
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts
index 8148884..4e76360 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
index 0248dfc..c60a1fe 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
@@ -19,6 +19,7 @@
 import './gr-repo-access.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {toSortedPermissionsArray} from '../../../utils/access-util.js';
 
 const basicFixture = fixtureFromElement('gr-repo-access');
 
@@ -135,7 +136,7 @@
       assert.isNotOk(element._inheritsFrom);
       assert.deepEqual(element._local, accessRes.local);
       assert.deepEqual(element._sections,
-          element.toSortedArray(accessRes.local));
+          toSortedPermissionsArray(accessRes.local));
       assert.deepEqual(element._labels, repoRes.labels);
       assert.equal(getComputedStyle(element.shadowRoot
           .querySelector('.weblinks')).display,
@@ -144,7 +145,7 @@
     })
         .then(() => {
           assert.deepEqual(element._sections,
-              element.toSortedArray(accessRes2.local));
+              toSortedPermissionsArray(accessRes2.local));
           assert.equal(getComputedStyle(element.shadowRoot
               .querySelector('.weblinks')).display,
           'none');
@@ -307,7 +308,7 @@
       // by any tests.
       element._local = JSON.parse(JSON.stringify(accessRes.local));
       element._ownerOf = [];
-      element._sections = element.toSortedArray(element._local);
+      element._sections = toSortedPermissionsArray(element._local);
       element._groups = JSON.parse(JSON.stringify(accessRes.groups));
       element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
       element._labels = JSON.parse(JSON.stringify(repoRes.labels));
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
index 3ae5b29..3880e4a 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts
index 8ce69df..7cdd10e 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 e3a9958..4989365 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
@@ -29,13 +29,12 @@
 import '../gr-create-pointer-dialog/gr-create-pointer-dialog.js';
 import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-detail-list_html.js';
-import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
+import {encodeURL} from '../../../utils/url-util.js';
 
 const DETAIL_TYPES = {
   BRANCHES: 'branches',
@@ -48,10 +47,7 @@
  * @appliesMixin ListViewMixin
  * @extends PolymerElement
  */
-class GrRepoDetailList extends mixinBehaviors( [
-  ListViewBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrRepoDetailList extends ListViewMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
@@ -170,7 +166,7 @@
   }
 
   _getPath(repo) {
-    return `/admin/repos/${this.encodeURL(repo, false)},` +
+    return `/admin/repos/${encodeURL(repo, false)},` +
         `${this.detailType}`;
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
index 21971e7..8955092 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-form-styles">
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 a8119e6..249a75d 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
@@ -21,21 +21,18 @@
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-create-repo-dialog/gr-create-repo-dialog.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 {htmlTemplate} from './gr-repo-list_html.js';
-import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
  * @appliesMixin ListViewMixin
  * @extends PolymerElement
  */
-class GrRepoList extends mixinBehaviors( [
-  ListViewBehavior,
-], GestureEventListeners(
+class GrRepoList extends ListViewMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
index 3681399..f61adce 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 fba5e4e..4933f41 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
@@ -24,21 +24,30 @@
 import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
 import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-plugin-config_html.js';
-import {RepoPluginConfig} from '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js';
+
+// Should be kept in sync with
+// gerrit/java/com/google/gerrit/extensions/api/projects/ProjectConfigEntryType.java.
+const CONFIG_ENTRY_TYPE = {
+  ARRAY: 'ARRAY',
+  BOOLEAN: 'BOOLEAN',
+  INT: 'INT',
+  LIST: 'LIST',
+  LONG: 'LONG',
+  STRING: 'STRING',
+};
+
+const PLUGIN_CONFIG_CHANGED_EVENT_NAME = 'plugin-config-changed';
 
 /**
  * @extends PolymerElement
  */
-class GrRepoPluginConfig extends mixinBehaviors( [
-  RepoPluginConfig,
-], GestureEventListeners(
+class GrRepoPluginConfig extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-repo-plugin-config'; }
@@ -70,22 +79,22 @@
   }
 
   _isArray(type) {
-    return type === this.ENTRY_TYPES.ARRAY;
+    return type === CONFIG_ENTRY_TYPE.ARRAY;
   }
 
   _isBoolean(type) {
-    return type === this.ENTRY_TYPES.BOOLEAN;
+    return type === CONFIG_ENTRY_TYPE.BOOLEAN;
   }
 
   _isList(type) {
-    return type === this.ENTRY_TYPES.LIST;
+    return type === CONFIG_ENTRY_TYPE.LIST;
   }
 
   _isString(type) {
     // Treat numbers like strings for simplicity.
-    return type === this.ENTRY_TYPES.STRING ||
-        type === this.ENTRY_TYPES.INT ||
-        type === this.ENTRY_TYPES.LONG;
+    return type === CONFIG_ENTRY_TYPE.STRING ||
+        type === CONFIG_ENTRY_TYPE.INT ||
+        type === CONFIG_ENTRY_TYPE.LONG;
   }
 
   _computeDisabled(editable) {
@@ -147,8 +156,8 @@
       notifyPath: `${name}.${notifyPath}`,
     };
 
-    this.dispatchEvent(new CustomEvent(
-        this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
+    this.dispatchEvent(new CustomEvent(PLUGIN_CONFIG_CHANGED_EVENT_NAME,
+        {detail, bubbles: true, composed: true}));
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts
index ee633463..3045108 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 99%
rename from polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
rename to polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
index ea8a8b9..26d05c3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 f50b9ed..a23614d 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
@@ -20,14 +20,12 @@
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-select/gr-select.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-rule-editor_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
+import {AccessPermissions} from '../../../utils/access-util.js';
 
 /**
  * Fired when the rule has been modified or removed.
@@ -79,12 +77,9 @@
 /**
  * @extends PolymerElement
  */
-class GrRuleEditor extends mixinBehaviors( [
-  AccessBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrRuleEditor extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-rule-editor'; }
@@ -158,12 +153,12 @@
   }
 
   _computeForce(permission, action) {
-    if (this.permissionValues.push.id === permission &&
+    if (AccessPermissions.push.id === permission &&
         action !== Action.DENY) {
       return true;
     }
 
-    return this.permissionValues.editTopicName.id === permission;
+    return AccessPermissions.editTopicName.id === permission;
   }
 
   _computeForceClass(permission, action) {
@@ -171,7 +166,7 @@
   }
 
   _computeGroupPath(group) {
-    return `${getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
+    return `${getBaseUrl()}/admin/groups/${encodeURL(group, true)}`;
   }
 
   _handleAccessSaved() {
@@ -201,7 +196,7 @@
   }
 
   _computeForceOptions(permission, action) {
-    if (permission === this.permissionValues.push.id) {
+    if (permission === AccessPermissions.push.id) {
       if (action === Action.ALLOW) {
         return ForcePushOptions.ALLOW;
       } else if (action === Action.BLOCK) {
@@ -209,7 +204,7 @@
       } else {
         return [];
       }
-    } else if (permission === this.permissionValues.editTopicName.id) {
+    } else if (permission === AccessPermissions.editTopicName.id) {
       return FORCE_EDIT_OPTIONS;
     }
     return [];
@@ -264,7 +259,7 @@
     // gr-permission will take care of removing rules that were added but
     // unsaved. We need to keep the added bit for the filter.
     if (this.rule.value.added) { return; }
-    this.set('rule.value', Object.assign({}, this._originalRuleValues));
+    this.set('rule.value', {...this._originalRuleValues});
     this._deleted = false;
     delete this.rule.value.deleted;
     delete this.rule.value.modified;
@@ -279,7 +274,7 @@
   }
 
   _setOriginalRuleValues(value) {
-    this._originalRuleValues = Object.assign({}, value);
+    this._originalRuleValues = {...value};
   }
 }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
rename to polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.ts
index 3e4f9d4..98403e0 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 1e06795..b2ce8fd 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
@@ -26,20 +26,18 @@
 import '../../../styles/shared-styles.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.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 {htmlTemplate} from './gr-change-list-item_html.js';
-import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
-import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
-import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {GrDisplayNameUtils} from '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import {getDisplayName} from '../../../utils/display-name-util.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 {truncatePath} from '../../../utils/path-list-util.js';
+import {changeStatuses} from '../../../utils/change-util.js';
 
 const CHANGE_SIZE = {
   XS: 10,
@@ -52,17 +50,10 @@
 const PRIMARY_REVIEWERS_COUNT = 2;
 
 /**
- * @appliesMixin RESTClientMixin
  * @extends PolymerElement
  */
-class GrChangeListItem extends mixinBehaviors( [
-  ChangeTableBehavior,
-  PathListBehavior,
-  RESTClientBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeListItem extends ChangeTableMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-list-item'; }
@@ -90,7 +81,7 @@
       },
       statuses: {
         type: Array,
-        computed: 'changeStatuses(change)',
+        computed: '_changeStatuses(change)',
       },
       showStar: {
         type: Boolean,
@@ -121,6 +112,10 @@
     });
   }
 
+  _changeStatuses(change) {
+    return changeStatuses(change);
+  }
+
   _computeChangeURL(change) {
     return GerritNav.getUrlForChange(change);
   }
@@ -210,7 +205,7 @@
     if (!change || !change.project) { return ''; }
     let str = '';
     if (change.internalHost) { str += change.internalHost + '/'; }
-    str += truncate ? this.truncatePath(change.project, 2) : change.project;
+    str += truncate ? truncatePath(change.project, 2) : change.project;
     return str;
   }
 
@@ -219,7 +214,7 @@
         isNaN(change.insertions + change.deletions)) {
       return 'Size unknown';
     } else {
-      return `+${change.insertions}, -${change.deletions}`;
+      return `added ${change.insertions}, removed ${change.deletions} lines`;
     }
   }
 
@@ -264,7 +259,7 @@
   _computeAdditionalReviewersTitle(change, config) {
     if (!change || !config) return '';
     return this._computeAdditionalReviewers(change)
-        .map(user => GrDisplayNameUtils.getDisplayName(config, user))
+        .map(user => getDisplayName(config, user))
         .join(', ');
   }
 
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
rename to polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index 48aab4c..7fa59d4 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -200,7 +200,7 @@
           change="[[change]]"
           account="[[reviewer]]"
         ></gr-account-link
-        ><span class="lastChildHidden">, </span>
+        ><span class="lastChildHidden" aria-hidden="true">, </span>
       </template>
       <template is="dom-if" if="[[_computeAdditionalReviewersCount(change)]]">
         <span title="[[_computeAdditionalReviewersTitle(change, config)]]">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
index e49da7e..6d51310 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
@@ -233,7 +233,7 @@
     assert.equal(element._computeSizeTooltip({
       insertions: 1,
       deletions: 2,
-    }), '+1, -2');
+    }), 'added 1, removed 2 lines');
   });
 
   test('TShirt sizing', () => {
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 baaf428..953c917 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
@@ -21,12 +21,10 @@
 import '../gr-repo-header/gr-repo-header.js';
 import '../gr-user-header/gr-user-header.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-change-list-view_html.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import page from 'page/page.mjs';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
@@ -45,11 +43,9 @@
 /**
  * @extends PolymerElement
  */
-class GrChangeListView extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrChangeListView extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-list-view'; }
@@ -190,7 +186,9 @@
             for (const query in LookupQueryPatterns) {
               if (LookupQueryPatterns.hasOwnProperty(query) &&
               this._query.match(LookupQueryPatterns[query])) {
-                GerritNav.navigateToChange(changes[0]);
+                // "Back"/"Forward" buttons work correctly only with
+                // opt_redirect options
+                GerritNav.navigateToChange(changes[0], null, null, null, true);
                 return;
               }
             }
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
rename to polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts
index b322f38..0e8f843 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
index f945476..db622d5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
@@ -192,10 +192,12 @@
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
-        assert.equal(url, change);
-        done();
-      });
+      sinon.stub(GerritNav, 'navigateToChange').callsFake(
+          (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
+            assert.equal(url, change);
+            assert.isTrue(opt_redirect);
+            done();
+          });
 
       element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
     });
@@ -204,10 +206,12 @@
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
-        assert.equal(url, change);
-        done();
-      });
+      sinon.stub(GerritNav, 'navigateToChange').callsFake(
+          (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
+            assert.equal(url, change);
+            assert.isTrue(opt_redirect);
+            done();
+          });
 
       element.params = {view: GerritNav.View.SEARCH, query: '1'};
     });
@@ -216,10 +220,12 @@
       const change = {_number: 1};
       sinon.stub(element, '_getChanges')
           .returns(Promise.resolve([change]));
-      sinon.stub(GerritNav, 'navigateToChange').callsFake( url => {
-        assert.equal(url, change);
-        done();
-      });
+      sinon.stub(GerritNav, 'navigateToChange').callsFake(
+          (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
+            assert.equal(url, change);
+            assert.isTrue(opt_redirect);
+            done();
+          });
 
       element.params = {view: GerritNav.View.SEARCH, query: COMMIT_HASH};
     });
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 f873802..648d70e 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
@@ -23,19 +23,17 @@
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.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 {htmlTemplate} from './gr-change-list_html.js';
 import {appContext} from '../../../services/app-context.js';
-import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin.js';
+import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.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 {changeIsOpen} from '../../../utils/change-util.js';
 
 const NUMBER_FIXED_COLUMNS = 3;
 const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
@@ -45,14 +43,9 @@
 /**
  * @extends PolymerElement
  */
-class GrChangeList extends mixinBehaviors( [
-  ChangeTableBehavior,
-  KeyboardShortcutBehavior,
-  RESTClientBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeList extends ChangeTableMixin(
+    KeyboardShortcutMixin(GestureEventListeners(
+        LegacyElementMixin(PolymerElement)))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-list'; }
@@ -140,14 +133,14 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
-      [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
-      [this.Shortcut.NEXT_PAGE]: '_nextPage',
-      [this.Shortcut.PREV_PAGE]: '_prevPage',
-      [this.Shortcut.OPEN_CHANGE]: '_openChange',
-      [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
-      [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
-      [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
+      [Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
+      [Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
+      [Shortcut.NEXT_PAGE]: '_nextPage',
+      [Shortcut.PREV_PAGE]: '_prevPage',
+      [Shortcut.OPEN_CHANGE]: '_openChange',
+      [Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
+      [Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
+      [Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
     };
   }
 
@@ -303,7 +296,7 @@
         !!config && !!config.change && config.change.enable_attention_set;
     return !isAttentionSetEnabled && showReviewedState && !change.reviewed &&
         !change.work_in_progress &&
-        this.changeIsOpen(change) &&
+        changeIsOpen(change) &&
         (!account || account._account_id != change.owner._account_id);
   }
 
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
rename to polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
index 623838b..404456c 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
index 78973df..9c8b04d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
@@ -21,6 +21,7 @@
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const basicFixture = fixtureFromElement('gr-change-list');
 
@@ -29,14 +30,14 @@
 
   suiteSetup(() => {
     const kb = TestKeyboardShortcutBinder.push();
-    kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
-    kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
-    kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
-    kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
-    kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
-    kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
+    kb.bindShortcut(Shortcut.CURSOR_NEXT_CHANGE, 'j');
+    kb.bindShortcut(Shortcut.CURSOR_PREV_CHANGE, 'k');
+    kb.bindShortcut(Shortcut.OPEN_CHANGE, 'o');
+    kb.bindShortcut(Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
+    kb.bindShortcut(Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
+    kb.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's');
+    kb.bindShortcut(Shortcut.NEXT_PAGE, 'n');
+    kb.bindShortcut(Shortcut.NEXT_PAGE, 'p');
   });
 
   suiteTeardown(() => {
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
rename to polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.ts
index cc6223d..c2f97a6 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
rename to polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.ts
index d2a1af9..9031738 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
rename to polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.ts
index c7cd647..9155d9a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles"></style>
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 c5d50c1..3b44381 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
@@ -24,25 +24,21 @@
 import '../gr-create-change-help/gr-create-change-help.js';
 import '../gr-create-destination-dialog/gr-create-destination-dialog.js';
 import '../gr-user-header/gr-user-header.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 {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';
+import {changeIsOpen} from '../../../utils/change-util.js';
 
 const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
 
 /**
  * @extends PolymerElement
  */
-class GrDashboardView extends mixinBehaviors( [
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrDashboardView extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-dashboard-view'; }
@@ -111,6 +107,10 @@
   attached() {
     super.attached();
     this._loadPreferences();
+    this.addEventListener('reload', e => {
+      e.stopPropagation();
+      this._reload();
+    });
   }
 
   _loadPreferences() {
@@ -300,7 +300,7 @@
     if (!draftSection || !draftSection.results.length) { return; }
 
     const closedChanges = draftSection.results
-        .filter(change => !this.changeIsOpen(change));
+        .filter(change => !changeIsOpen(change));
     if (!closedChanges.length) { return; }
 
     this._showDraftsBanner = true;
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
rename to polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index cf1036f..ea04c5a 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index bdd374a..f56ad75 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -19,6 +19,8 @@
 import './gr-dashboard-view.js';
 import {isHidden} from '../../../test/test-utils.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {changeIsOpen} from '../../../utils/change-util.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromElement('gr-dashboard-view');
 
@@ -66,16 +68,17 @@
 
       test('no drafts on open changes', () => {
         element.params = {user: 'self'};
-        element._results = [{query: 'has:draft', results: [{status: '_'}]}];
-        sinon.stub(element, 'changeIsOpen').returns(true);
+        const openChange = {status: ChangeStatus.NEW};
+        element._results = [{query: 'has:draft', results: [openChange]}];
         element._maybeShowDraftsBanner();
         assert.isFalse(element._showDraftsBanner);
       });
 
-      test('no drafts on open changes', () => {
+      test('no drafts on not open changes', () => {
         element.params = {user: 'self'};
-        element._results = [{query: 'has:draft', results: [{status: '_'}]}];
-        sinon.stub(element, 'changeIsOpen').returns(false);
+        const notOpenChange = {status: '_'};
+        element._results = [{query: 'has:draft', results: [notOpenChange]}];
+        assert.isFalse(changeIsOpen(element._results[0].results[0]));
         element._maybeShowDraftsBanner();
         assert.isTrue(element._showDraftsBanner);
       });
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.ts
similarity index 94%
rename from polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
rename to polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.ts
index 75af51e..9fd27c6 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
rename to polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.ts
index 8207284..72bdca6 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 21ccf82..259fea8 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
@@ -32,16 +32,22 @@
 import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog.js';
 import '../../../styles/shared-styles.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-actions_html.js';
-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 {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {appContext} from '../../../services/app-context.js';
+import {
+  fetchChangeUpdates,
+  patchNumEquals,
+} from '../../../utils/patch-set-util.js';
+import {
+  changeIsOpen,
+  ListChangesOption,
+  listChangesOptionsToHex,
+} from '../../../utils/change-util.js';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -241,12 +247,8 @@
 /**
  * @extends PolymerElement
  */
-class GrChangeActions extends mixinBehaviors( [
-  PatchSetBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeActions extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-actions'; }
@@ -732,7 +734,7 @@
     if (this.actions && editPatchsetLoaded) {
       // Only show actions that mutate an edit if an actual edit patch set
       // is loaded.
-      if (this.changeIsOpen(this.change)) {
+      if (changeIsOpen(this.change)) {
         if (editBasedOnCurrentPatchSet) {
           if (!this.actions.publishEdit) {
             this.set('actions.publishEdit', PUBLISH_EDIT);
@@ -754,7 +756,7 @@
       this._deleteAndNotify('deleteEdit');
     }
 
-    if (this.actions && this.changeIsOpen(this.change)) {
+    if (this.actions && changeIsOpen(this.change)) {
       // Only show edit button if there is no edit patchset loaded and the
       // file list is not in edit mode.
       if (editPatchsetLoaded || editMode) {
@@ -856,7 +858,7 @@
     if (!approval) {
       return null;
     }
-    const action = Object.assign({}, QUICK_APPROVE_ACTION);
+    const action = {...QUICK_APPROVE_ACTION};
     action.label = approval.label + approval.score;
     const review = {
       drafts: 'PUBLISH_ALL_REVISIONS',
@@ -897,7 +899,7 @@
       actions[a].label = this._getActionLabel(actions[a]);
 
       // Triggers a re-render by ensuring object inequality.
-      result.push(Object.assign({}, actions[a]));
+      result.push({...actions[a]});
     });
 
     let additionalActions = (additionalActionsChangeRecord &&
@@ -907,7 +909,7 @@
         .map(a => {
           a.__primary = primaryActionKeys.includes(a.__key);
           // Triggers a re-render by ensuring object inequality.
-          return Object.assign({}, a);
+          return {...a};
         });
     return result.concat(additionalActions).concat(pluginActions);
   }
@@ -958,7 +960,7 @@
 
   _getRevision(change, patchNum) {
     for (const rev of Object.values(change.revisions)) {
-      if (this.patchNumEquals(rev._number, patchNum)) {
+      if (patchNumEquals(rev._number, patchNum)) {
         return rev;
       }
     }
@@ -1422,7 +1424,7 @@
       cleanupFn.call(this);
       this._handleResponseError(action, response, payload);
     };
-    return this.fetchChangeUpdates(this.change, this.$.restAPI)
+    return fetchChangeUpdates(this.change, this.$.restAPI)
         .then(result => {
           if (!result.isLatest) {
             this.dispatchEvent(new CustomEvent('show-alert', {
@@ -1462,8 +1464,8 @@
     this.$.confirmCherrypick.branch = '';
     const query = `topic: "${this.change.topic}"`;
     const options =
-      this.listChangesOptionsToHex(this.ListChangesOption.MESSAGES,
-          this.ListChangesOption.ALL_REVISIONS);
+      listChangesOptionsToHex(ListChangesOption.MESSAGES,
+          ListChangesOption.ALL_REVISIONS);
     this.$.restAPI.getChanges('', query, undefined, options)
         .then(changes => {
           this.$.confirmCherrypick.updateChanges(changes);
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
rename to polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
index 200ffc9..4e315af 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index 2702af4..0655931 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -20,6 +20,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {generateChange} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-change-actions');
 
@@ -174,9 +175,10 @@
     });
 
     test('getActionDetails', () => {
-      element.revisionActions = Object.assign({
+      element.revisionActions = {
         'plugin~action': {},
-      }, element.revisionActions);
+        ...element.revisionActions,
+      };
       assert.isUndefined(element.getActionDetails('rubbish'));
       assert.strictEqual(element.revisionActions['plugin~action'],
           element.getActionDetails('plugin~action'));
@@ -271,8 +273,6 @@
       const showSpy = sinon.spy(element, '_showActionDialog');
       sinon.stub(element.$.restAPI, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sinon.stub(element, 'fetchChangeUpdates').callsFake(
-          () => Promise.resolve({isLatest: true}));
       sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
       element.change = {
         revisions: {
@@ -295,8 +295,6 @@
       sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake( done);
       sinon.stub(element.$.restAPI, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sinon.stub(element, 'fetchChangeUpdates').callsFake(
-          () => Promise.resolve({isLatest: true}));
       sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
       element.change = {
         revisions: {
@@ -1805,6 +1803,10 @@
         element.changeNum = 42;
         element.change._number = 42;
         element.latestPatchNum = 12;
+        element.change = generateChange({
+          revisionsCount: element.latestPatchNum,
+          messagesCount: 1,
+        });
         payload = {foo: 'bar'};
 
         onShowError = sinon.stub();
@@ -1816,16 +1818,19 @@
       suite('happy path', () => {
         let sendStub;
         setup(() => {
-          sinon.stub(element, 'fetchChangeUpdates')
-              .returns(Promise.resolve({isLatest: true}));
+          sinon.stub(element.$.restAPI, 'getChangeDetail')
+              .returns(Promise.resolve(
+                  generateChange({
+                    // element has latest info
+                    revisionsCount: element.latestPatchNum,
+                    messagesCount: 1,
+                  })));
           sendStub = sinon.stub(element.$.restAPI, 'executeChangeAction')
               .returns(Promise.resolve({}));
           getResponseObjectStub = sinon.stub(element.$.restAPI,
               'getResponseObject');
           sinon.stub(GerritNav,
               'navigateToChange').returns(Promise.resolve(true));
-          sinon.stub(element, 'computeLatestPatchNum')
-              .returns(element.latestPatchNum);
         });
 
         test('change action', done => {
@@ -1936,8 +1941,13 @@
 
       suite('failure modes', () => {
         test('non-latest', () => {
-          sinon.stub(element, 'fetchChangeUpdates')
-              .returns(Promise.resolve({isLatest: false}));
+          sinon.stub(element.$.restAPI, 'getChangeDetail')
+              .returns(Promise.resolve(
+                  generateChange({
+                    // new patchset was uploaded
+                    revisionsCount: element.latestPatchNum + 1,
+                    messagesCount: 1,
+                  })));
           const sendStub = sinon.stub(element.$.restAPI,
               'executeChangeAction');
 
@@ -1951,8 +1961,13 @@
         });
 
         test('send fails', () => {
-          sinon.stub(element, 'fetchChangeUpdates')
-              .returns(Promise.resolve({isLatest: true}));
+          sinon.stub(element.$.restAPI, 'getChangeDetail')
+              .returns(Promise.resolve(
+                  generateChange({
+                    // element has latest info
+                    revisionsCount: element.latestPatchNum,
+                    messagesCount: 1,
+                  })));
           const sendStub = sinon.stub(element.$.restAPI,
               'executeChangeAction').callsFake(
               (num, method, patchNum, endpoint, payload, onErr) => {
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 b098a93..4f86390 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
@@ -35,15 +35,14 @@
 import '../gr-reviewer-list/gr-reviewer-list.js';
 import '../../shared/gr-account-list/gr-account-list.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-metadata_html.js';
-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';
+import {changeIsOpen} from '../../../utils/change-util.js';
 
 const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
@@ -79,11 +78,8 @@
 /**
  * @extends PolymerElement
  */
-class GrChangeMetadata extends mixinBehaviors( [
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeMetadata extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-metadata'; }
@@ -177,7 +173,7 @@
   }
 
   _labelsChanged(labels) {
-    this.labels = Object.assign({}, labels) || null;
+    this.labels = ({...labels}) || null;
   }
 
   _changeChanged(change) {
@@ -203,7 +199,7 @@
   }
 
   _computeHideStrategy(change) {
-    return !this.changeIsOpen(change);
+    return !changeIsOpen(change);
   }
 
   /**
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
rename to polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index 7d84835..e96912d 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-change-metadata-shared-styles">
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 d2d6037..8dfee81 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
@@ -20,21 +20,16 @@
 import '../../shared/gr-label/gr-label.js';
 import '../../shared/gr-label-info/gr-label-info.js';
 import '../../shared/gr-limited-text/gr-limited-text.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 {htmlTemplate} from './gr-change-requirements_html.js';
-import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
  * @extends PolymerElement
  */
-class GrChangeRequirements extends mixinBehaviors( [
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeRequirements extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-requirements'; }
@@ -91,6 +86,7 @@
     }
     if (change.work_in_progress) {
       _requirements.push({
+        type: 'wip',
         fallback_text: 'Work-in-progress',
         tooltip: 'Change must not be in \'Work in Progress\' state.',
       });
@@ -165,6 +161,10 @@
   _handleShowHide(e) {
     this._showOptionalLabels = !this._showOptionalLabels;
   }
+
+  _computeSubmitRequirementEndpoint(item) {
+    return `submit-requirement-item-${item.type}`;
+  }
 }
 
 customElements.define(GrChangeRequirements.is, GrChangeRequirements);
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.ts
similarity index 87%
rename from polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
rename to polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
index 657915b..fc2346a 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -42,6 +42,7 @@
     .status iron-icon {
       vertical-align: top;
     }
+    gr-endpoint-decorator.submit-requirement-endpoints,
     section {
       display: table-row;
     }
@@ -82,9 +83,18 @@
     .spacer {
       height: var(--spacing-m);
     }
+    gr-endpoint-param {
+      display: none;
+    }
   </style>
   <template is="dom-repeat" items="[[_requirements]]">
-    <section>
+    <gr-endpoint-decorator
+      class="submit-requirement-endpoints"
+      name$="[[_computeSubmitRequirementEndpoint(item)]]"
+    >
+      <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+      <gr-endpoint-param name="requirement" value="[[item]]">
+      </gr-endpoint-param>
       <div class="title requirement">
         <span class$="status [[item.style]]">
           <iron-icon
@@ -95,10 +105,14 @@
         <gr-limited-text
           class="name"
           limit="40"
+          tooltip="[[item.tooltip]]"
           text="[[item.fallback_text]]"
         ></gr-limited-text>
       </div>
-    </section>
+      <div class="value">
+        <gr-endpoint-slot name="value"></gr-endpoint-slot>
+      </div>
+    </gr-endpoint-decorator>
   </template>
   <template is="dom-repeat" items="[[_requiredLabels]]">
     <section>
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 8f27730..9547ce4 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
@@ -46,14 +46,11 @@
 import '../gr-thread-list/gr-thread-list.js';
 import '../gr-upload-help-dialog/gr-upload-help-dialog.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-view_html.js';
-import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {GrEditConstants} from '../../edit/gr-edit-constants.js';
 import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import {getComputedStyleValue} from '../../../utils/dom-util.js';
@@ -66,6 +63,16 @@
 import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
 import {appContext} from '../../../services/app-context.js';
 import {ChangeStatus} from '../../../constants/constants.js';
+import {
+  computeAllPatchSets,
+  computeLatestPatchNum,
+  fetchChangeUpdates,
+  hasEditBasedOnCurrentPatchSet,
+  hasEditPatchsetLoaded,
+  patchNumEquals,
+  SPECIAL_PATCH_SET_NUM,
+} from '../../../utils/patch-set-util.js';
+import {changeStatuses, changeStatusString} from '../../../utils/change-util.js';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -126,17 +133,10 @@
  */
 
 /**
- * @appliesMixin RESTClientMixin
- * @appliesMixin PatchSetMixin
  * @extends PolymerElement
  */
-class GrChangeView extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  PatchSetBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeView extends KeyboardShortcutMixin(
+    GestureEventListeners(LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-view'; }
@@ -294,7 +294,7 @@
       _currentRevisionActions: Object,
       _allPatchSets: {
         type: Array,
-        computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+        computed: '_computeAllPatchSets(_change, _change.revisions.*)',
       },
       _loggedIn: {
         type: Boolean,
@@ -321,7 +321,7 @@
       },
       _changeStatus: {
         type: String,
-        computed: 'changeStatusString(_change)',
+        computed: '_changeStatusString(_change)',
       },
       _changeStatuses: {
         type: String,
@@ -426,26 +426,26 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
-      [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding
-      [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
-      [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
-      [this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
+      [Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
+      [Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding
+      [Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
+      [Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
+      [Shortcut.OPEN_DOWNLOAD_DIALOG]:
           '_handleOpenDownloadDialogShortcut',
-      [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
-      [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
-      [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
-      [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
-      [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
-      [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_expandAllDiffs',
-      [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
-      [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic',
-      [this.Shortcut.DIFF_AGAINST_BASE]: '_handleDiffAgainstBase',
-      [this.Shortcut.DIFF_AGAINST_LATEST]: '_handleDiffAgainstLatest',
-      [this.Shortcut.DIFF_BASE_AGAINST_LEFT]: '_handleDiffBaseAgainstLeft',
-      [this.Shortcut.DIFF_RIGHT_AGAINST_LATEST]:
+      [Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+      [Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
+      [Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
+      [Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
+      [Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
+      [Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_expandAllDiffs',
+      [Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
+      [Shortcut.EDIT_TOPIC]: '_handleEditTopic',
+      [Shortcut.DIFF_AGAINST_BASE]: '_handleDiffAgainstBase',
+      [Shortcut.DIFF_AGAINST_LATEST]: '_handleDiffAgainstLatest',
+      [Shortcut.DIFF_BASE_AGAINST_LEFT]: '_handleDiffBaseAgainstLeft',
+      [Shortcut.DIFF_RIGHT_AGAINST_LATEST]:
         '_handleDiffRightAgainstLatest',
-      [this.Shortcut.DIFF_BASE_AGAINST_LATEST]:
+      [Shortcut.DIFF_BASE_AGAINST_LATEST]:
         '_handleDiffBaseAgainstLatest',
     };
   }
@@ -476,6 +476,9 @@
 
     this.addEventListener('diff-comments-modified',
         () => this._handleReloadCommentThreads());
+
+    this.addEventListener('open-reply-dialog',
+        e => this._openReplyDialog());
   }
 
   /** @override */
@@ -529,6 +532,10 @@
         e => this._setActivePrimaryTab(e));
     this.addEventListener('show-secondary-tab',
         e => this._setActiveSecondaryTab(e));
+    this.addEventListener('reload', e => {
+      e.stopPropagation();
+      this._reload();
+    });
   }
 
   /** @override */
@@ -550,6 +557,10 @@
     return this.shadowRoot.querySelector('gr-thread-list');
   }
 
+  _changeStatusString(change) {
+    return changeStatusString(change);
+  }
+
   /**
    * @param {boolean=} opt_reset
    */
@@ -733,7 +744,7 @@
       mergeable: !!mergeable,
       submitEnabled: !!submitEnabled,
     };
-    return this.changeStatuses(change, options);
+    return changeStatuses(change, options);
   }
 
   _computeHideEditCommitMessage(
@@ -858,7 +869,7 @@
     // because the paths could contain dots in them. A new object must be
     // created to satisfy Polymer’s dirty checking.
     // https://github.com/Polymer/polymer/issues/3127
-    const diffDrafts = Object.assign({}, this._diffDrafts);
+    const diffDrafts = {...this._diffDrafts};
     if (!diffDrafts[draft.path]) {
       diffDrafts[draft.path] = [draft];
       this._diffDrafts = diffDrafts;
@@ -906,7 +917,7 @@
     // because the paths could contain dots in them. A new object must be
     // created to satisfy Polymer’s dirty checking.
     // https://github.com/Polymer/polymer/issues/3127
-    const diffDrafts = Object.assign({}, this._diffDrafts);
+    const diffDrafts = {...this._diffDrafts};
     diffDrafts[draft.path].splice(index, 1);
     if (diffDrafts[draft.path].length === 0) {
       delete diffDrafts[draft.path];
@@ -1046,7 +1057,7 @@
     // in the patch range, then don't do a full reload.
     if (!changeChanged && patchChanged) {
       if (patchRange.patchNum == null) {
-        patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+        patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
       }
       this._reloadPatchNumDependentResources().then(() => {
         this._sendShowChangeEvent();
@@ -1217,7 +1228,7 @@
     const parent = this._getBasePatchNum(change, this._patchRange);
 
     this.set('_patchRange.patchNum', this._patchRange.patchNum ||
-            this.computeLatestPatchNum(this._allPatchSets));
+            computeLatestPatchNum(this._allPatchSets));
 
     this.set('_patchRange.basePatchNum', parent);
 
@@ -1402,7 +1413,8 @@
 
   _handleDiffAgainstBase(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    if (this.patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) {
+    if (patchNumEquals(this._patchRange.basePatchNum,
+        SPECIAL_PATCH_SET_NUM.PARENT)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Base is already selected.',
@@ -1416,7 +1428,8 @@
 
   _handleDiffBaseAgainstLeft(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    if (this.patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) {
+    if (patchNumEquals(this._patchRange.basePatchNum,
+        SPECIAL_PATCH_SET_NUM.PARENT)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Left is already base.',
@@ -1430,8 +1443,8 @@
 
   _handleDiffAgainstLatest(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    const latestPatchNum = this.computeLatestPatchNum(this._allPatchSets);
-    if (this.patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
+    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Latest is already selected.',
@@ -1446,8 +1459,8 @@
 
   _handleDiffRightAgainstLatest(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    const latestPatchNum = this.computeLatestPatchNum(this._allPatchSets);
-    if (this.patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
+    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Right is already latest.',
@@ -1462,9 +1475,10 @@
 
   _handleDiffBaseAgainstLatest(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    const latestPatchNum = this.computeLatestPatchNum(this._allPatchSets);
-    if (this.patchNumEquals(this._patchRange.patchNum, latestPatchNum) &&
-      this.patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) {
+    const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
+    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum) &&
+      patchNumEquals(this._patchRange.basePatchNum,
+          SPECIAL_PATCH_SET_NUM.PARENT)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Already diffing base against latest.',
@@ -1618,7 +1632,7 @@
   _processEdit(change, edit) {
     if (!edit) { return; }
     change.revisions[edit.commit.commit] = {
-      _number: this.EDIT_NAME,
+      _number: SPECIAL_PATCH_SET_NUM.EDIT,
       basePatchNum: edit.base_patch_set_number,
       commit: edit.commit,
       fetch: edit.fetch,
@@ -1628,7 +1642,7 @@
     if (!this._patchRange.patchNum &&
         change.current_revision === edit.base_revision) {
       change.current_revision = edit.commit.commit;
-      this.set('_patchRange.patchNum', this.EDIT_NAME);
+      this.set('_patchRange.patchNum', SPECIAL_PATCH_SET_NUM.EDIT);
       // Because edits are fibbed as revisions and added to the revisions
       // array, and revision actions are always derived from the 'latest'
       // patch set, we must copy over actions from the patch set base.
@@ -1674,7 +1688,7 @@
 
           this._change = change;
           if (!this._patchRange || !this._patchRange.patchNum ||
-              this.patchNumEquals(this._patchRange.patchNum,
+              patchNumEquals(this._patchRange.patchNum,
                   currentRevision._number)) {
             // CommitInfo.commit is optional, and may need patching.
             if (!currentRevision.commit.commit) {
@@ -1717,7 +1731,7 @@
 
   _getLatestCommitMessage() {
     return this.$.restAPI.getChangeCommitInfo(this._changeNum,
-        this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
+        computeLatestPatchNum(this._allPatchSets)).then(commitInfo => {
       if (!commitInfo) return Promise.resolve();
       this._latestCommitMessage =
                   this._prepareCommitMsgForLinkify(commitInfo.message);
@@ -2109,7 +2123,7 @@
     }
 
     this._updateCheckTimerHandle = this.async(() => {
-      this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
+      fetchChangeUpdates(this._change, this.$.restAPI).then(result => {
         let toastMessage = null;
         if (!result.isLatest) {
           toastMessage = ReloadToastMessage.NEWER_REVISION;
@@ -2179,7 +2193,7 @@
     if (paramsRecord.base && paramsRecord.base.edit) { return true; }
 
     const patchRange = patchRangeRecord.base || {};
-    return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+    return patchNumEquals(patchRange.patchNum, SPECIAL_PATCH_SET_NUM.EDIT);
   }
 
   _handleFileActionTap(e) {
@@ -2232,18 +2246,18 @@
    */
   _handleEditTap() {
     const editInfo = Object.values(this._change.revisions).find(info =>
-      info._number === this.EDIT_NAME);
+      info._number === SPECIAL_PATCH_SET_NUM.EDIT);
 
     if (editInfo) {
-      GerritNav.navigateToChange(this._change, this.EDIT_NAME);
+      GerritNav.navigateToChange(this._change, SPECIAL_PATCH_SET_NUM.EDIT);
       return;
     }
 
     // Avoid putting patch set in the URL unless a non-latest patch set is
     // selected.
     let patchNum;
-    if (!this.patchNumEquals(this._patchRange.patchNum,
-        this.computeLatestPatchNum(this._allPatchSets))) {
+    if (!patchNumEquals(this._patchRange.patchNum,
+        computeLatestPatchNum(this._allPatchSets))) {
       patchNum = this._patchRange.patchNum;
     }
     GerritNav.navigateToChange(this._change, patchNum, null, true);
@@ -2273,6 +2287,34 @@
   _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) {
     return disableDiffPrefs || !loggedIn;
   }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _computeLatestPatchNum(allPatchSets) {
+    return computeLatestPatchNum(allPatchSets);
+  }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _hasEditBasedOnCurrentPatchSet(allPatchSets) {
+    return hasEditBasedOnCurrentPatchSet(allPatchSets);
+  }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _hasEditPatchsetLoaded(patchRangeRecord) {
+    return hasEditPatchsetLoaded(patchRangeRecord);
+  }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _computeAllPatchSets(change) {
+    return computeAllPatchSets(change);
+  }
 }
 
 customElements.define(GrChangeView.is, GrChangeView);
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
rename to polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 5b89c15..04a810f 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -403,11 +403,11 @@
             change-num="[[_changeNum]]"
             change-status="[[_change.status]]"
             commit-num="[[_commitInfo.commit]]"
-            latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
+            latest-patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
             commit-message="[[_latestCommitMessage]]"
-            edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]"
+            edit-patchset-loaded="[[_hasEditPatchsetLoaded(_patchRange.*)]]"
             edit-mode="[[_editMode]]"
-            edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
+            edit-based-on-current-patch-set="[[_hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
             private-by-default="[[_projectConfig.private_by_default]]"
             on-reload-change="_handleReloadChange"
             on-edit-tap="_handleEditTap"
@@ -517,7 +517,7 @@
                 mergeable="[[_mergeable]]"
                 has-parent="{{hasParent}}"
                 on-update="_updateRelatedChangeMaxHeight"
-                patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
+                patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
                 on-new-section-loaded="_computeShowRelatedToggle"
               >
               </gr-related-changes-list>
@@ -756,7 +756,7 @@
     <gr-reply-dialog
       id="replyDialog"
       change="{{_change}}"
-      patch-num="[[computeLatestPatchNum(_allPatchSets)]]"
+      patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
       permitted-labels="[[_change.permitted_labels]]"
       draft-comment-threads="[[_draftCommentThreads]]"
       project-config="[[_projectConfig]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index c8b13b9..c853449 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -29,7 +29,9 @@
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 
 import 'lodash/lodash.js';
-import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {generateChange, TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 const fixture = fixtureFromElement('gr-change-view');
@@ -41,17 +43,17 @@
 
   suiteSetup(() => {
     const kb = TestKeyboardShortcutBinder.push();
-    kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
-    kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
-    kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
-    kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
-    kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
-    kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
-    kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
-    kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
-    kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
+    kb.bindShortcut(Shortcut.SEND_REPLY, 'ctrl+enter');
+    kb.bindShortcut(Shortcut.REFRESH_CHANGE, 'shift+r');
+    kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
+    kb.bindShortcut(Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
+    kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
+    kb.bindShortcut(Shortcut.TOGGLE_CHANGE_STAR, 's');
+    kb.bindShortcut(Shortcut.UP_TO_DASHBOARD, 'u');
+    kb.bindShortcut(Shortcut.EXPAND_ALL_MESSAGES, 'x');
+    kb.bindShortcut(Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+    kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
+    kb.bindShortcut(Shortcut.EDIT_TOPIC, 't');
   });
 
   suiteTeardown(() => {
@@ -339,57 +341,54 @@
   });
 
   test('_handleDiffAgainstBase', () => {
-    element._changeNum = '1';
+    element._change = generateChange({revisionsCount: 10});
     element._patchRange = {
       patchNum: 3,
       basePatchNum: 1,
     };
-    sinon.stub(element, 'computeLatestPatchNum').returns(10);
     sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffAgainstBase(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
+    assert.equal(args[0], element._change);
     assert.equal(args[1], 3);
-    assert.isNotOk(args[0]);
   });
 
   test('_handleDiffAgainstLatest', () => {
-    element._changeNum = '1';
+    element._change = generateChange({revisionsCount: 10});
     element._patchRange = {
       basePatchNum: 1,
       patchNum: 3,
     };
-    sinon.stub(element, 'computeLatestPatchNum').returns(10);
     sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffAgainstLatest(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
+    assert.equal(args[0], element._change);
     assert.equal(args[1], 10);
     assert.equal(args[2], 1);
   });
 
   test('_handleDiffBaseAgainstLeft', () => {
-    element._changeNum = '1';
+    element._change = generateChange({revisionsCount: 10});
     element._patchRange = {
       patchNum: 3,
       basePatchNum: 1,
     };
-    sinon.stub(element, 'computeLatestPatchNum').returns(10);
     sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffBaseAgainstLeft(new CustomEvent(''));
     assert(navigateToChangeStub.called);
     const args = navigateToChangeStub.getCall(0).args;
+    assert.equal(args[0], element._change);
     assert.equal(args[1], 1);
-    assert.isNotOk(args[0]);
   });
 
   test('_handleDiffRightAgainstLatest', () => {
-    element._changeNum = '1';
+    element._change = generateChange({revisionsCount: 10});
     element._patchRange = {
       basePatchNum: 1,
       patchNum: 3,
     };
-    sinon.stub(element, 'computeLatestPatchNum').returns(10);
     sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffRightAgainstLatest(new CustomEvent(''));
     assert(navigateToChangeStub.called);
@@ -399,12 +398,11 @@
   });
 
   test('_handleDiffBaseAgainstLatest', () => {
-    element._changeNum = '1';
+    element._change = generateChange({revisionsCount: 10});
     element._patchRange = {
       basePatchNum: 1,
       patchNum: 3,
     };
-    sinon.stub(element, 'computeLatestPatchNum').returns(10);
     sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
     element._handleDiffBaseAgainstLatest(new CustomEvent(''));
     assert(navigateToChangeStub.called);
@@ -459,11 +457,9 @@
       const queryMap = new Map();
       queryMap.set('tab', PrimaryTab.FINDINGS);
       // view is required
-      element.params = Object.assign(
-          {
-            view: GerritNav.View.CHANGE,
-          },
-          element.params, {queryMap});
+      element.params = {
+        view: GerritNav.View.CHANGE,
+        ...element.params, queryMap};
       flush(() => {
         assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
         done();
@@ -475,11 +471,9 @@
       const queryMap = new Map();
       queryMap.set('tab', 'random');
       // view is required
-      element.params = Object.assign(
-          {
-            view: GerritNav.View.CHANGE,
-          },
-          element.params, {queryMap});
+      element.params = {
+        view: GerritNav.View.CHANGE,
+        ...element.params, queryMap};
       flush(() => {
         assert.equal(element._activeTabs[0], PrimaryTab.FILES);
         done();
@@ -552,9 +546,18 @@
 
     test('A toggles overlay when logged in', done => {
       sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-      sinon.stub(element.$.replyDialog, 'fetchChangeUpdates')
-          .returns(Promise.resolve({isLatest: true}));
-      element._change = {labels: {}};
+      element._change = generateChange({
+        revisionsCount: 1,
+        messagesCount: 1,
+      });
+      element._change.labels = {};
+      sinon.stub(element.$.restAPI, 'getChangeDetail')
+          .callsFake(() => Promise.resolve(generateChange({
+            // element has latest info
+            revisionsCount: 1,
+            messagesCount: 1,
+          })));
+
       const openSpy = sinon.spy(element, '_openReplyDialog');
 
       MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
@@ -1017,8 +1020,6 @@
   });
 
   test('_changeStatuses', () => {
-    sinon.stub(element, 'changeStatuses').returns(
-        ['Merged', 'WIP']);
     element._loading = false;
     element._change = {
       change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
@@ -1029,6 +1030,8 @@
         rev3: {_number: 3},
       },
       current_revision: 'rev3',
+      status: ChangeStatus.MERGED,
+      work_in_progress: true,
       labels: {
         test: {
           all: [],
@@ -1516,7 +1519,7 @@
       assert.equal(Object.keys(revs).length, 2);
       assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}});
       assert.deepEqual(revs['bar'], {
-        _number: element.EDIT_NAME,
+        _number: SPECIAL_PATCH_SET_NUM.EDIT,
         basePatchNum: 1,
         commit: {commit: 'bar'},
         fetch: undefined,
@@ -1706,9 +1709,31 @@
   suite('reply dialog tests', () => {
     setup(() => {
       sinon.stub(element.$.replyDialog, '_draftChanged');
-      sinon.stub(element.$.replyDialog, 'fetchChangeUpdates').callsFake(
-          () => Promise.resolve({isLatest: true}));
-      element._change = {labels: {}};
+      element._change = generateChange({
+        revisionsCount: 1,
+        messagesCount: 1,
+      });
+      element._change.labels = {};
+      sinon.stub(element.$.restAPI, 'getChangeDetail')
+          .callsFake(() => Promise.resolve(generateChange({
+            // element has latest info
+            revisionsCount: 1,
+            messagesCount: 1,
+          })));
+    });
+
+    test('show reply dialog on open-reply-dialog event', done => {
+      sinon.stub(element, '_openReplyDialog');
+      element.dispatchEvent(
+          new CustomEvent('open-reply-dialog', {
+            composed: true,
+            bubbles: true,
+            detail: {},
+          }));
+      flush(() => {
+        assert.isTrue(element._openReplyDialog.calledOnce);
+        done();
+      });
     });
 
     test('reply from comment adds quote text', () => {
@@ -1755,8 +1780,17 @@
 
   suite('commit message expand/collapse', () => {
     setup(() => {
-      sinon.stub(element, 'fetchChangeUpdates').callsFake(
-          () => Promise.resolve({isLatest: false}));
+      element._change = generateChange({
+        revisionsCount: 1,
+        messagesCount: 1,
+      });
+      element._change.labels = {};
+      sinon.stub(element.$.restAPI, 'getChangeDetail')
+          .callsFake(() => Promise.resolve(generateChange({
+            // new patchset was uploaded
+            revisionsCount: 2,
+            messagesCount: 1,
+          })));
     });
 
     test('commitCollapseToggle hidden for short commit message', () => {
@@ -1912,31 +1946,51 @@
           if (element.async.callCount > 1) { return; }
           f.call(element);
         });
+        element._change = generateChange({
+          revisionsCount: 1,
+          messagesCount: 1,
+        });
       });
 
       test('_startUpdateCheckTimer negative delay', () => {
-        sinon.stub(element, 'fetchChangeUpdates');
+        const getChangeDetailStub =
+            sinon.stub(element.$.restAPI, 'getChangeDetail')
+                .callsFake(() => Promise.resolve(generateChange({
+                  // element has latest info
+                  revisionsCount: 1,
+                  messagesCount: 1,
+                })));
 
         element._serverConfig = {change: {update_delay: -1}};
 
         assert.isTrue(element._startUpdateCheckTimer.called);
-        assert.isFalse(element.fetchChangeUpdates.called);
+        assert.isFalse(getChangeDetailStub.called);
       });
 
       test('_startUpdateCheckTimer up-to-date', () => {
-        sinon.stub(element, 'fetchChangeUpdates').callsFake(
-            () => Promise.resolve({isLatest: true}));
+        const getChangeDetailStub =
+            sinon.stub(element.$.restAPI, 'getChangeDetail')
+                .callsFake(() => Promise.resolve(generateChange({
+                  // element has latest info
+                  revisionsCount: 1,
+                  messagesCount: 1,
+                })));
 
         element._serverConfig = {change: {update_delay: 12345}};
 
         assert.isTrue(element._startUpdateCheckTimer.called);
-        assert.isTrue(element.fetchChangeUpdates.called);
+        assert.isTrue(getChangeDetailStub.called);
         assert.equal(element.async.lastCall.args[1], 12345 * 1000);
       });
 
       test('_startUpdateCheckTimer out-of-date shows an alert', done => {
-        sinon.stub(element, 'fetchChangeUpdates').callsFake(
-            () => Promise.resolve({isLatest: false}));
+        sinon.stub(element.$.restAPI, 'getChangeDetail')
+            .callsFake(() => Promise.resolve(generateChange({
+              // new patchset was uploaded
+              revisionsCount: 2,
+              messagesCount: 1,
+            })));
+
         element.addEventListener('show-alert', e => {
           assert.equal(e.detail.message,
               'A newer patch set has been uploaded');
@@ -1946,11 +2000,14 @@
       });
 
       test('_startUpdateCheckTimer new status shows an alert', done => {
-        sinon.stub(element, 'fetchChangeUpdates')
-            .returns(Promise.resolve({
-              isLatest: true,
-              newStatus: ChangeStatus.MERGED,
-            }));
+        sinon.stub(element.$.restAPI, 'getChangeDetail')
+            .callsFake(() => Promise.resolve(generateChange({
+              // element has latest info
+              revisionsCount: 1,
+              messagesCount: 1,
+              status: ChangeStatus.MERGED,
+            })));
+
         element.addEventListener('show-alert', e => {
           assert.equal(e.detail.message, 'This change has been merged');
           done();
@@ -1959,11 +2016,12 @@
       });
 
       test('_startUpdateCheckTimer new messages shows an alert', done => {
-        sinon.stub(element, 'fetchChangeUpdates')
-            .returns(Promise.resolve({
-              isLatest: true,
-              newMessages: true,
-            }));
+        sinon.stub(element.$.restAPI, 'getChangeDetail')
+            .callsFake(() => Promise.resolve(generateChange({
+              revisionsCount: 1,
+              // element has new message
+              messagesCount: 2,
+            })));
         element.addEventListener('show-alert', e => {
           assert.equal(e.detail.message,
               'There are new messages on this change');
@@ -2053,7 +2111,7 @@
     };
     element._processEdit(mockChange = _.cloneDeep(change), edit);
     assert.notDeepEqual(mockChange, change);
-    assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
+    assert.equal(mockChange.revisions.bar._number, SPECIAL_PATCH_SET_NUM.EDIT);
     assert.equal(mockChange.current_revision, change.current_revision);
     assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
     assert.notOk(mockChange.revisions.bar.actions);
@@ -2217,11 +2275,12 @@
     test('edit exists in revisions', done => {
       sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
         assert.equal(args.length, 2);
-        assert.equal(args[1], element.EDIT_NAME); // patchNum
+        assert.equal(args[1], SPECIAL_PATCH_SET_NUM.EDIT); // patchNum
         done();
       });
 
-      element.set('_change.revisions.rev2', {_number: element.EDIT_NAME});
+      element.set('_change.revisions.rev2',
+          {_number: SPECIAL_PATCH_SET_NUM.EDIT});
       flushAsynchronousOperations();
 
       fireEdit();
@@ -2262,7 +2321,6 @@
   test('_handleStopEditTap', done => {
     sinon.stub(element.$.metadata, '_computeLabelNames');
     navigateToChangeStub.restore();
-    sinon.stub(element, 'computeLatestPatchNum').returns(1);
     sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
       assert.equal(args.length, 2);
       assert.equal(args[1], 1); // patchNum
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
rename to polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
index 608d12b..e350593 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 ada7dae..a8f1903 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
@@ -17,21 +17,19 @@
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-confirm-abandon-dialog_html.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 /**
  * @extends PolymerElement
  */
-class GrConfirmAbandonDialog extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrConfirmAbandonDialog extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(
+            PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-confirm-abandon-dialog'; }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts
index 050df25..7c1b725 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.ts
index c7fb70c..5cf56b54 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 e6d529c..cb15d97 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
@@ -133,7 +133,7 @@
   }
 
   updateStatus(change, status) {
-    this._statuses = Object.assign({}, this._statuses, {[change.id]: status});
+    this._statuses = {...this._statuses, [change.id]: status};
   }
 
   _computeStatus(change, statuses) {
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.ts
index eaf9cc8..072f110 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 f2ea45d..61cd78d2b3 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
@@ -18,23 +18,21 @@
 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';
-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 {htmlTemplate} from './gr-confirm-move-dialog_html.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const SUGGESTIONS_LIMIT = 15;
 
 /**
  * @extends PolymerElement
  */
-class GrConfirmMoveDialog extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrConfirmMoveDialog extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(
+            PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-confirm-move-dialog'; }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts
index f5ddf41..b5b46d6 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
index e9a8424..687d31f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
index 7875fa7..f5561fc 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.ts
index 48051a0..cae4e1f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <!-- TODO(taoalpha): move all shared styles to a style module. -->
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
index cf1a332..84668ed 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 42f031e..8c11129 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
@@ -16,23 +16,18 @@
  */
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-download-commands/gr-download-commands.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 {htmlTemplate} from './gr-download-dialog_html.js';
-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 {patchNumEquals} from '../../../utils/patch-set-util.js';
+import {changeBaseURL} from '../../../utils/change-util.js';
 
 /**
  * @extends PolymerElement
  */
-class GrDownloadDialog extends mixinBehaviors( [
-  PatchSetBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrDownloadDialog extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-download-dialog'; }
@@ -87,7 +82,7 @@
     let commandObj;
     if (!change) return [];
     for (const rev of Object.values(change.revisions || {})) {
-      if (this.patchNumEquals(rev._number, patchNum) &&
+      if (patchNumEquals(rev._number, patchNum) &&
           rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) {
         commandObj = rev.fetch[_selectedScheme].commands;
         break;
@@ -136,7 +131,7 @@
     if ([change, patchNum].includes(undefined)) {
       return '';
     }
-    return this.changeBaseURL(change.project, change._number, patchNum) +
+    return changeBaseURL(change.project, change._number, patchNum) +
         '/patch?' + (opt_zip ? 'zip' : 'download');
   }
 
@@ -155,7 +150,7 @@
 
     let shortRev = '';
     for (const rev in change.revisions) {
-      if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
+      if (patchNumEquals(change.revisions[rev]._number, patchNum)) {
         shortRev = rev.substr(0, 7);
         break;
       }
@@ -169,7 +164,7 @@
       return false;
     }
     for (const rev of Object.values(change.revisions || {})) {
-      if (this.patchNumEquals(rev._number, patchNum)) {
+      if (patchNumEquals(rev._number, patchNum)) {
         const parentLength = rev.commit && rev.commit.parents ?
           rev.commit.parents.length : 0;
         return parentLength == 0;
@@ -183,7 +178,7 @@
     if ([change, patchNum, format].includes(undefined)) {
       return '';
     }
-    return this.changeBaseURL(change.project, change._number, patchNum) +
+    return changeBaseURL(change.project, change._number, patchNum) +
         '/archive?format=' + format;
   }
 
@@ -194,7 +189,7 @@
     }
 
     for (const rev of Object.values(change.revisions || {})) {
-      if (this.patchNumEquals(rev._number, patchNum)) {
+      if (patchNumEquals(rev._number, patchNum)) {
         const fetch = rev.fetch;
         if (fetch) {
           return Object.keys(fetch).sort();
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.ts
index 0446e4e..90185a6 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 d26f733..05f2ab0 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
@@ -26,15 +26,18 @@
 import '../../shared/gr-icons/gr-icons.js';
 import '../gr-commit-info/gr-commit-info.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-file-list-header_html.js';
-import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {GrFileListConstants} from '../gr-file-list-constants.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {
+  computeLatestPatchNum,
+  getRevisionByPatchNum,
+  patchNumEquals,
+} from '../../../utils/patch-set-util.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -43,12 +46,10 @@
 /**
  * @extends PolymerElement
  */
-class GrFileListHeader extends mixinBehaviors( [
-  PatchSetBehavior,
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrFileListHeader extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(
+            PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-file-list-header'; }
@@ -173,7 +174,7 @@
       return;
     }
 
-    const rev = this.getRevisionByPatchNum(change.revisions, patchNum);
+    const rev = getRevisionByPatchNum(change.revisions, patchNum);
     this._patchsetDescription = (rev && rev.description) ?
       rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
   }
@@ -211,7 +212,7 @@
   _updateDescription(desc, e) {
     const target = dom(e).rootTarget;
     if (target) { target.disabled = true; }
-    const rev = this.getRevisionByPatchNum(this.change.revisions,
+    const rev = getRevisionByPatchNum(this.change.revisions,
         this.patchNum);
     const sha = this._getPatchsetHash(this.change.revisions, rev);
     return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc)
@@ -238,8 +239,8 @@
 
   _handlePatchChange(e) {
     const {basePatchNum, patchNum} = e.detail;
-    if (this.patchNumEquals(basePatchNum, this.basePatchNum) &&
-        this.patchNumEquals(patchNum, this.patchNum)) { return; }
+    if (patchNumEquals(basePatchNum, this.basePatchNum) &&
+        patchNumEquals(patchNum, this.patchNum)) { return; }
     GerritNav.navigateToChange(this.change, patchNum, basePatchNum);
   }
 
@@ -269,8 +270,8 @@
   }
 
   _computePatchInfoClass(patchNum, allPatchSets) {
-    const latestNum = this.computeLatestPatchNum(allPatchSets);
-    if (this.patchNumEquals(patchNum, latestNum)) {
+    const latestNum = computeLatestPatchNum(allPatchSets);
+    if (patchNumEquals(patchNum, latestNum)) {
       return '';
     }
     return 'patchInfoOldPatchSet';
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
rename to polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index bb04114..beabeef 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index d756bf3..d0155d6 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -20,6 +20,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrFileListConstants} from '../gr-file-list-constants.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {generateChange} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-file-list-header');
 
@@ -258,7 +259,7 @@
 
     test('patch specific elements', () => {
       element.editMode = true;
-      sinon.stub(element, 'computeLatestPatchNum').returns('2');
+      element.allPatchSets = generateChange({revisionsCount: 2}).revisions;
       flushAsynchronousOperations();
 
       assert.isFalse(isVisible(element.$.diffPrefsContainer));
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 df6490d..93b2cce 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
@@ -28,16 +28,12 @@
 import '../../shared/gr-tooltip-content/gr-tooltip-content.js';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
 import {dom, 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-file-list_html.js';
 import {asyncForeach} from '../../../utils/async-util.js';
-import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.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 {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {GrFileListConstants} from '../gr-file-list-constants.js';
 import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
@@ -45,6 +41,15 @@
 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';
+import {descendedFromClass} from '../../../utils/dom-util.js';
+import {getRevisionByPatchNum} from '../../../utils/patch-set-util.js';
+import {
+  addUnmodifiedFiles,
+  computeDisplayPath,
+  computeTruncatedPath,
+  isMagicPath,
+  specialFilePathCompare,
+} from '../../../utils/path-list-util.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -90,14 +95,9 @@
 /**
  * @extends PolymerElement
  */
-class GrFileList extends mixinBehaviors( [
-  DomUtilBehavior,
-  KeyboardShortcutBehavior,
-  PatchSetBehavior,
-  PathListBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrFileList extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-file-list'; }
@@ -270,28 +270,28 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
-      [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]:
+      [Shortcut.LEFT_PANE]: '_handleLeftPane',
+      [Shortcut.RIGHT_PANE]: '_handleRightPane',
+      [Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
+      [Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+      [Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
         '_handleToggleHideAllCommentThreads',
-      [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
-      [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
-      [this.Shortcut.NEXT_LINE]: '_handleCursorNext',
-      [this.Shortcut.PREV_LINE]: '_handleCursorPrev',
-      [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
-      [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
-      [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
-      [this.Shortcut.OPEN_FILE]: '_handleOpenFile',
-      [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
-      [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
-      [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
-      [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+      [Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
+      [Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
+      [Shortcut.NEXT_LINE]: '_handleCursorNext',
+      [Shortcut.PREV_LINE]: '_handleCursorPrev',
+      [Shortcut.NEW_COMMENT]: '_handleNewComment',
+      [Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
+      [Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
+      [Shortcut.OPEN_FILE]: '_handleOpenFile',
+      [Shortcut.NEXT_CHUNK]: '_handleNextChunk',
+      [Shortcut.PREV_CHUNK]: '_handlePrevChunk',
+      [Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+      [Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
 
       // Final two are actually handled by gr-comment-thread.
-      [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
-      [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+      [Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+      [Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
     };
   }
 
@@ -420,7 +420,7 @@
 
   _calculatePatchChange(files) {
     const magicFilesExcluded = files.filter(files =>
-      !this.isMagicPath(files.__path)
+      !isMagicPath(files.__path)
     );
 
     return magicFilesExcluded.reduce((acc, obj) => {
@@ -658,14 +658,12 @@
   }
 
   /**
-   * The closure compiler doesn't realize this.specialFilePathCompare is
-   * valid.
    *
    * @returns {!Array<FileInfo>}
    */
   _normalizeChangeFilesResponse(response) {
     if (!response) { return []; }
-    const paths = Object.keys(response).sort(this.specialFilePathCompare);
+    const paths = Object.keys(response).sort(specialFilePathCompare);
     const files = [];
     for (let i = 0; i < paths.length; i++) {
       const info = response[paths[i]];
@@ -731,10 +729,10 @@
     // 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.
-    if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; }
+    if (!path || descendedFromClass(e.target, 'pathLink')) { return; }
 
     // Disregard the event if the click target is in the edit controls.
-    if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
+    if (descendedFromClass(e.target, 'editFileControls')) { return; }
 
     e.preventDefault();
     this.$.fileCursor.setCursor(fileRow.element);
@@ -1075,8 +1073,8 @@
     if (loading || !changeComments) { return; }
 
     const commentedPaths = changeComments.getPaths(patchRange);
-    const files = Object.assign({}, filesByPath);
-    this.addUnmodifiedFiles(files, commentedPaths);
+    const files = {...filesByPath};
+    addUnmodifiedFiles(files, commentedPaths);
     const reviewedSet = new Set(reviewed || []);
     for (const filePath in files) {
       if (!files.hasOwnProperty(filePath)) { continue; }
@@ -1168,7 +1166,7 @@
       return '';
     }
 
-    const rev = this.getRevisionByPatchNum(revisions, patchNum);
+    const rev = getRevisionByPatchNum(revisions, patchNum);
     return (rev && rev.description) ?
       rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
   }
@@ -1296,7 +1294,7 @@
       this._cancelForEachDiff = cancel;
 
       iter++;
-      console.log('Expanding diff', iter, 'of', initialCount, ':',
+      console.info('Expanding diff', iter, 'of', initialCount, ':',
           path);
       const diffElem = this._findDiffByPath(path, diffElements);
       if (!diffElem) {
@@ -1313,7 +1311,7 @@
     }).then(() => {
       this._cancelForEachDiff = null;
       this._nextRenderParams = null;
-      console.log('Finished expanding', initialCount, 'diff(s)');
+      console.info('Finished expanding', initialCount, 'diff(s)');
       this.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
           EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
       /* Block diff cursor from auto scrolling after files are done rendering.
@@ -1610,6 +1608,20 @@
       this.diffPrefs = prefs;
     });
   }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _computeDisplayPath(path) {
+    return computeDisplayPath(path);
+  }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _computeTruncatedPath(path) {
+    return computeTruncatedPath(path);
+  }
 }
 
 customElements.define(GrFileList.is, GrFileList);
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
rename to polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index a2714d9..e141b70 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -409,16 +409,16 @@
               href$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
             >
               <span
-                title$="[[computeDisplayPath(file.__path)]]"
+                title$="[[_computeDisplayPath(file.__path)]]"
                 class="fullFileName"
               >
-                [[computeDisplayPath(file.__path)]]
+                [[_computeDisplayPath(file.__path)]]
               </span>
               <span
-                title$="[[computeDisplayPath(file.__path)]]"
+                title$="[[_computeDisplayPath(file.__path)]]"
                 class="truncatedFileName"
               >
-                [[computeTruncatedPath(file.__path)]]
+                [[_computeTruncatedPath(file.__path)]]
               </span>
               <span
                 class$="[[_computeStatusClass(file)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 051e6b3..932000c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -26,6 +26,7 @@
 import {runA11yAudit} from '../../../test/a11y-test-utils.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const commentApiMock = createCommentApiMockWithTemplateElement(
     'gr-file-list-comment-api-mock', html`
@@ -52,22 +53,22 @@
 
   suiteSetup(() => {
     const kb = TestKeyboardShortcutBinder.push();
-    kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
-    kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
-    kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
-    kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
-    kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
-    kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
-    kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
-    kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
-    kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
-    kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
-    kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
-    kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
-    kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+    kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
+    kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
+    kb.bindShortcut(Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+    kb.bindShortcut(Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+    kb.bindShortcut(Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+    kb.bindShortcut(Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+    kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
+    kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
+    kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
+    kb.bindShortcut(Shortcut.OPEN_LAST_FILE, '[');
+    kb.bindShortcut(Shortcut.OPEN_FIRST_FILE, ']');
+    kb.bindShortcut(Shortcut.OPEN_FILE, 'o');
+    kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
+    kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
+    kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+    kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
   });
 
   suiteTeardown(() => {
@@ -927,27 +928,6 @@
       assert.isFalse(toggleExpandSpy.called);
     });
 
-    test('patch set from revisions', () => {
-      const expected = [
-        {num: 4, desc: 'test', sha: 'rev4'},
-        {num: 3, desc: 'test', sha: 'rev3'},
-        {num: 2, desc: 'test', sha: 'rev2'},
-        {num: 1, desc: 'test', sha: 'rev1'},
-      ];
-      const patchNums = element.computeAllPatchSets({
-        revisions: {
-          rev3: {_number: 3, description: 'test', date: 3},
-          rev1: {_number: 1, description: 'test', date: 1},
-          rev4: {_number: 4, description: 'test', date: 4},
-          rev2: {_number: 2, description: 'test', date: 2},
-        },
-      });
-      assert.equal(patchNums.length, expected.length);
-      for (let i = 0; i < expected.length; i++) {
-        assert.deepEqual(patchNums[i], expected[i]);
-      }
-    });
-
     test('checkbox shows/hides diff inline', () => {
       element._filesByPath = {
         'myfile.txt': {},
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.ts
index 4670e6a..8e90f3b 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
rename to polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
index fb0f9e8..312532e 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-voting-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
rename to polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.ts
index 1e3e7ec..974e55d 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
rename to polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index af57782..22d495b 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-voting-styles">
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 cd61396..635336f 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
@@ -20,12 +20,14 @@
 import '../gr-message/gr-message.js';
 import '../../../styles/shared-styles.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 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 {
+  KeyboardShortcutMixin,
+  Shortcut, ShortcutSection,
+} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {parseDate} from '../../../utils/date-util.js';
 import {MessageTag} from '../../../constants/constants.js';
 import {appContext} from '../../../services/app-context.js';
@@ -151,11 +153,10 @@
 /**
  * @extends PolymerElement
  */
-class GrMessagesList extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrMessagesList extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(
+            PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-messages-list'; }
@@ -335,11 +336,11 @@
   _computeExpandAllTitle(_expandAllState) {
     if (_expandAllState === ExpandAllState.COLLAPSED_ALL) {
       return this.createTitle(
-          this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
+          Shortcut.COLLAPSE_ALL_MESSAGES, ShortcutSection.ACTIONS);
     }
     if (_expandAllState === ExpandAllState.EXPAND_ALL) {
       return this.createTitle(
-          this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
+          Shortcut.EXPAND_ALL_MESSAGES, ShortcutSection.ACTIONS);
     }
     return '';
   }
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
rename to polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
index c696c8c..9c4ef04 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 bddb15a..d0613fc 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
@@ -20,25 +20,20 @@
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
 import '../../plugins/gr-endpoint-slot/gr-endpoint-slot.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-related-changes-list_html.js';
-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';
+import {patchNumEquals} from '../../../utils/patch-set-util.js';
+import {changeIsOpen} from '../../../utils/change-util.js';
 
 /**
  * @extends PolymerElement
  */
-class GrRelatedChangesList extends mixinBehaviors( [
-  PatchSetBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrRelatedChangesList extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-related-changes-list'; }
@@ -141,7 +136,7 @@
     ];
 
     // Get conflicts if change is open and is mergeable.
-    if (this.changeIsOpen(this.change) && this.mergeable) {
+    if (changeIsOpen(this.change) && this.mergeable) {
       promises.push(this._getConflicts().then(response => {
         // Because the server doesn't always return a response and the
         // template expects an array, always return an array.
@@ -367,7 +362,7 @@
     let changeRevision;
     if (!change) { return []; }
     for (const rev in change.revisions) {
-      if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) {
+      if (patchNumEquals(change.revisions[rev]._number, patchNum)) {
         changeRevision = rev;
       }
     }
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
rename to polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.ts
index 2721b2e2..6fe8cea 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 63ab267..56612ff 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -24,7 +24,7 @@
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 const pluginApi = _testOnly_initGerritPluginApi();
 
-suite('gr-reply-dialog tests', () => {
+suite('gr-reply-dialog-it tests', () => {
   let element;
   let changeNum;
   let patchNum;
@@ -67,8 +67,6 @@
         '+1',
       ],
     };
-    sinon.stub(element, 'fetchChangeUpdates')
-        .returns(Promise.resolve({isLatest: true}));
   };
 
   setup(() => {
@@ -82,7 +80,6 @@
 
     element = basicFixture.instantiate();
     setupElement(element);
-
     // Allow the elements created by dom-repeat to be stamped.
     flushAsynchronousOperations();
   });
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 d5416b2..aa032cc 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
@@ -28,19 +28,17 @@
 import '../gr-label-scores/gr-label-scores.js';
 import '../gr-thread-list/gr-thread-list.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-reply-dialog_html.js';
-import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-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';
+import {fetchChangeUpdates} from '../../../utils/patch-set-util.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -80,13 +78,8 @@
 /**
  * @extends PolymerElement
  */
-class GrReplyDialog extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  PatchSetBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrReplyDialog extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-reply-dialog'; }
@@ -292,6 +285,15 @@
         type: Boolean,
         value: true,
       },
+
+      /**
+       * A copy of added reviewers, a new copy is created when any change
+       * made to the reviewers.
+       */
+      _allReviewers: {
+        type: Array,
+        computed: '_computeAllReviewers(_reviewers.*)',
+      },
     };
   }
 
@@ -346,7 +348,7 @@
 
   open(opt_focusTarget) {
     this.knownLatestState = LatestPatchState.CHECKING;
-    this.fetchChangeUpdates(this.change, this.$.restAPI)
+    fetchChangeUpdates(this.change, this.$.restAPI)
         .then(result => {
           this.knownLatestState = result.isLatest ?
             LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
@@ -455,7 +457,7 @@
     for (const splice of indexSplices) {
       for (const account of splice.removed) {
         if (!this._reviewersPendingRemove[type]) {
-          console.err('Invalid type ' + type + ' for reviewer.');
+          console.error('Invalid type ' + type + ' for reviewer.');
           return;
         }
         this._reviewersPendingRemove[type].push(account);
@@ -1109,6 +1111,10 @@
     }
     this.reporting.reportInteraction('attention-set-actions', {actions});
   }
+
+  _computeAllReviewers() {
+    return [...this._reviewers];
+  }
 }
 
 customElements.define(GrReplyDialog.is, GrReplyDialog);
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index dc20c0d..3cc32b2 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -178,6 +178,8 @@
     <section class="peopleContainer">
       <gr-endpoint-decorator name="reply-reviewers">
         <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+        <gr-endpoint-param name="reviewers" value="[[_allReviewers]]">
+        </gr-endpoint-param>
         <div class="peopleList">
           <div class="peopleListLabel">Reviewers</div>
           <gr-account-list
@@ -284,38 +286,6 @@
       <div id="pluginMessage">[[_pluginMessage]]</div>
     </section>
     <section
-      class="draftsContainer"
-      hidden$="[[_computeHideDraftList(draftCommentThreads)]]"
-    >
-      <div class="includeComments">
-        <input
-          type="checkbox"
-          id="includeComments"
-          checked="{{_includeComments::change}}"
-        />
-        <label for="includeComments"
-          >Publish [[_computeDraftsTitle(draftCommentThreads)]]</label
-        >
-      </div>
-      <gr-thread-list
-        id="commentList"
-        hidden$="[[!_includeComments]]"
-        threads="[[draftCommentThreads]]"
-        change="[[change]]"
-        change-num="[[change._number]]"
-        logged-in="true"
-        hide-toggle-buttons=""
-        on-thread-list-modified="_onThreadListModified"
-      >
-      </gr-thread-list>
-      <span
-        id="savingLabel"
-        class$="[[_computeSavingLabelClass(_savingComments)]]"
-      >
-        Saving comments...
-      </span>
-    </section>
-    <section
       hidden$="[[!_showAttentionSummary(serverConfig, _attentionModified)]]"
       class="attention"
     >
@@ -351,13 +321,13 @@
       <div class="attentionDetailsTitle">
         <iron-icon class="attention-icon" icon="gr-icons:attention"></iron-icon>
         <span>Bring to attention of ...</span>
-        <span class="selectUsers">(select users)</span>
+        <span class="selectUsers">(click chips to select users)</span>
       </div>
       <div class="peopleList">
         <div class="peopleListLabel">Owner</div>
         <gr-account-label
           account="[[_owner]]"
-          show-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
+          force-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
           blurred="[[!_computeHasNewAttention(_owner, _newAttentionSet)]]"
           hide-hovercard=""
           on-click="_handleAttentionClick"
@@ -369,7 +339,7 @@
         <template is="dom-repeat" items="[[_reviewers]]" as="account">
           <gr-account-label
             account="[[account]]"
-            show-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
+            force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
             blurred="[[!_computeHasNewAttention(account, _newAttentionSet)]]"
             hide-hovercard=""
             on-click="_handleAttentionClick"
@@ -382,7 +352,7 @@
         <template is="dom-repeat" items="[[_ccs]]" as="account">
           <gr-account-label
             account="[[account]]"
-            show-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
+            force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
             blurred="[[!_computeHasNewAttention(account, _newAttentionSet)]]"
             hide-hovercard=""
             on-click="_handleAttentionClick"
@@ -391,6 +361,38 @@
         </template>
       </div>
     </section>
+    <section
+      class="draftsContainer"
+      hidden$="[[_computeHideDraftList(draftCommentThreads)]]"
+    >
+      <div class="includeComments">
+        <input
+          type="checkbox"
+          id="includeComments"
+          checked="{{_includeComments::change}}"
+        />
+        <label for="includeComments"
+          >Publish [[_computeDraftsTitle(draftCommentThreads)]]</label
+        >
+      </div>
+      <gr-thread-list
+        id="commentList"
+        hidden$="[[!_includeComments]]"
+        threads="[[draftCommentThreads]]"
+        change="[[change]]"
+        change-num="[[change._number]]"
+        logged-in="true"
+        hide-toggle-buttons=""
+        on-thread-list-modified="_onThreadListModified"
+      >
+      </gr-thread-list>
+      <span
+        id="savingLabel"
+        class$="[[_computeSavingLabelClass(_savingComments)]]"
+      >
+        Saving comments...
+      </span>
+    </section>
     <section class="actions">
       <div class="left">
         <span
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 29f1e8a..0254652 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -115,8 +115,8 @@
     eraseDraftCommentStub = sinon.stub(element.$.storage,
         'eraseDraftComment');
 
-    sinon.stub(element, 'fetchChangeUpdates')
-        .returns(Promise.resolve({isLatest: true}));
+    // sinon.stub(patchSetUtilMockProxy, 'fetchChangeUpdates')
+    //     .returns(Promise.resolve({isLatest: true}));
 
     // Allow the elements created by dom-repeat to be stamped.
     flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
rename to polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index 13a41ed..616a7db 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
rename to polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
index 80983ca..d74c985 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
rename to polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.ts
index ec010a1..1ee3a3a 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js
index d1af425..2d1da92 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.js
@@ -73,8 +73,8 @@
       assert.isUndefined(
           element._computeFetchCommand(testRev, '', 'badscheme'));
 
-      const rev = Object.assign({}, testRev);
-      rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}});
+      const rev = {...testRev};
+      rev.fetch = {...testRev.fetch, nocmds: {commands: {}}};
       assert.isUndefined(
           element._computeFetchCommand(rev, '', 'nocmds'));
 
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 f06cfe9..7608137 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
@@ -19,23 +19,20 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-avatar/gr-avatar.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 {htmlTemplate} from './gr-account-dropdown_html.js';
-import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
+import {getUserName} from '../../../utils/display-name-util.js';
 
 const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
 
 /**
  * @extends PolymerElement
  */
-class GrAccountDropdown extends mixinBehaviors( [
-  DisplayNameBehavior,
-], GestureEventListeners(
+class GrAccountDropdown extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-account-dropdown'; }
@@ -128,7 +125,7 @@
   }
 
   _accountName(account) {
-    return this.getUserName(this.config, account);
+    return getUserName(this.config, account);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
rename to polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts
index 5db7923..b67e1e8 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
rename to polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts
index 39d4f2d..10476cd 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 7e4e568..e619eab 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
@@ -26,7 +26,6 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-error-manager_html.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
-import {authService} from '../../shared/gr-rest-api-interface/gr-auth.js';
 import {appContext} from '../../../services/app-context.js';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
@@ -37,6 +36,21 @@
 const TOO_MANY_FILES = 'too many files to find conflicts';
 const AUTHENTICATION_REQUIRED = 'Authentication required\n';
 
+const ErrorType = {
+  AUTH: 'AUTH',
+  NETWORK: 'NETWORK',
+  GENERIC: 'GENERIC',
+};
+
+// Bigger number has higher priority
+const ErrorTypePriority = {
+  [ErrorType.AUTH]: 3,
+  [ErrorType.NETWORK]: 2,
+  [ErrorType.GENERIC]: 1,
+};
+
+export const __testOnly_ErrorType = ErrorType;
+
 /**
  * @extends PolymerElement
  */
@@ -83,7 +97,7 @@
     super();
 
     /** @type {!Auth} */
-    this._authService = authService;
+    this._authService = appContext.authService;
 
     /** @type {?Function} */
     this._authErrorHandlerDeregistrationHook;
@@ -98,6 +112,7 @@
     this.listen(document, 'server-error', '_handleServerError');
     this.listen(document, 'network-error', '_handleNetworkError');
     this.listen(document, 'show-alert', '_handleShowAlert');
+    this.listen(document, 'hide-alert', '_hideAlert');
     this.listen(document, 'show-error', '_handleShowErrorDialog');
     this.listen(document, 'visibilitychange', '_handleVisibilityChange');
     this.listen(document, 'show-auth-required', '_handleAuthRequired');
@@ -116,6 +131,7 @@
     this.unlisten(document, 'server-error', '_handleServerError');
     this.unlisten(document, 'network-error', '_handleNetworkError');
     this.unlisten(document, 'show-alert', '_handleShowAlert');
+    this.unlisten(document, 'hide-alert', '_hideAlert');
     this.unlisten(document, 'show-error', '_handleShowErrorDialog');
     this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
     this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
@@ -178,7 +194,7 @@
           }));
         }
       }
-      console.log(`server error: ${errorText}`);
+      console.info(`server error: ${errorText}`);
     });
   }
 
@@ -224,17 +240,24 @@
     console.error(e.detail.error.message);
   }
 
+  // TODO(dhruvsr): allow less priority alerts to override high priority alerts
+  // In some use cases we may want generic alerts to show along/over errors
+  _canOverride(incoming = ErrorType.GENERIC, existing = ErrorType.GENERIC) {
+    return ErrorTypePriority[incoming] >= ErrorTypePriority[existing];
+  }
+
   /**
    * @param {string} text
    * @param {?string=} opt_actionText
    * @param {?Function=} opt_actionCallback
    * @param {?boolean=} opt_dismissOnNavigation
+   * @param {?string=} opt_type
    */
   _showAlert(text, opt_actionText, opt_actionCallback,
-      opt_dismissOnNavigation) {
+      opt_dismissOnNavigation, opt_type) {
     if (this._alertElement) {
-      // do not override auth alerts
-      if (this._alertElement.type === 'AUTH') return;
+      // check priority before hiding
+      if (!this._canOverride(opt_type, this._alertElement.type)) return;
       this._hideAlert();
     }
 
@@ -276,7 +299,7 @@
     }
 
     this._alertElement = this._createToastAlert();
-    this._alertElement.type = 'AUTH';
+    this._alertElement.type = ErrorType.AUTH;
     this._alertElement.show(errorText, actionText,
         this._createLoginPopup.bind(this));
 
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.ts
similarity index 94%
rename from polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
rename to polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.ts
index 2fa9b95..1cefb78 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-overlay with-backdrop="" id="errorOverlay">
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
index 934f244..b527786 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
@@ -19,6 +19,7 @@
 import './gr-error-manager.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {__testOnly_ErrorType} from './gr-error-manager.js';
 
 const basicFixture = fixtureFromElement('gr-error-manager');
 
@@ -219,6 +220,26 @@
       });
     });
 
+    test('_canOverride alerts', () => {
+      assert.isFalse(element._canOverride(undefined,
+          __testOnly_ErrorType.AUTH));
+      assert.isFalse(element._canOverride(undefined,
+          __testOnly_ErrorType.NETWORK));
+      assert.isTrue(element._canOverride(undefined,
+          __testOnly_ErrorType.GENERIC));
+      assert.isTrue(element._canOverride(undefined, undefined));
+
+      assert.isTrue(element._canOverride(__testOnly_ErrorType.NETWORK,
+          undefined));
+      assert.isTrue(element._canOverride(__testOnly_ErrorType.AUTH,
+          undefined));
+      assert.isFalse(element._canOverride(__testOnly_ErrorType.NETWORK,
+          __testOnly_ErrorType.AUTH));
+
+      assert.isTrue(element._canOverride(__testOnly_ErrorType.AUTH,
+          __testOnly_ErrorType.NETWORK));
+    });
+
     test('show auth refresh toast', async () => {
       // starts with authed state
       element.$.restAPI.getLoggedIn();
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
rename to polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.ts
index 334a40a..0a75104 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 eeeb86b..31fece5 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
@@ -17,23 +17,18 @@
 import '../../shared/gr-button/gr-button.js';
 import '../gr-key-binding-display/gr-key-binding-display.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html.js';
-import {KeyboardShortcutBehavior, KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-
-const {ShortcutSection} = KeyboardShortcutBinder;
+import {KeyboardShortcutMixin, ShortcutSection} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 /**
  * @extends PolymerElement
  */
-class GrKeyboardShortcutsDialog extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrKeyboardShortcutsDialog extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-keyboard-shortcuts-dialog'; }
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
rename to polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
index e850fbe..1860f38 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js
index 2e6f5d3..cb7e87b8 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.js
@@ -17,12 +17,11 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-keyboard-shortcuts-dialog.js';
-import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {ShortcutSection} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const basicFixture = fixtureFromElement('gr-keyboard-shortcuts-dialog');
 
 suite('gr-keyboard-shortcuts-dialog tests', () => {
-  const kb = KeyboardShortcutBinder;
   let element;
 
   setup(() => {
@@ -42,13 +41,13 @@
 
     test('everywhere goes on left', () => {
       update(new Map([
-        [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+        [ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
       ]));
       assert.deepEqual(
           element._left,
           [
             {
-              section: kb.ShortcutSection.EVERYWHERE,
+              section: ShortcutSection.EVERYWHERE,
               shortcuts: ['everywhere shortcuts'],
             },
           ]);
@@ -57,13 +56,13 @@
 
     test('navigation goes on left', () => {
       update(new Map([
-        [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+        [ShortcutSection.NAVIGATION, ['navigation shortcuts']],
       ]));
       assert.deepEqual(
           element._left,
           [
             {
-              section: kb.ShortcutSection.NAVIGATION,
+              section: ShortcutSection.NAVIGATION,
               shortcuts: ['navigation shortcuts'],
             },
           ]);
@@ -72,13 +71,13 @@
 
     test('actions go on right', () => {
       update(new Map([
-        [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
+        [ShortcutSection.ACTIONS, ['actions shortcuts']],
       ]));
       assert.deepEqual(
           element._right,
           [
             {
-              section: kb.ShortcutSection.ACTIONS,
+              section: ShortcutSection.ACTIONS,
               shortcuts: ['actions shortcuts'],
             },
           ]);
@@ -87,13 +86,13 @@
 
     test('reply dialog goes on right', () => {
       update(new Map([
-        [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
+        [ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
       ]));
       assert.deepEqual(
           element._right,
           [
             {
-              section: kb.ShortcutSection.REPLY_DIALOG,
+              section: ShortcutSection.REPLY_DIALOG,
               shortcuts: ['reply dialog shortcuts'],
             },
           ]);
@@ -102,13 +101,13 @@
 
     test('file list goes on right', () => {
       update(new Map([
-        [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
+        [ShortcutSection.FILE_LIST, ['file list shortcuts']],
       ]));
       assert.deepEqual(
           element._right,
           [
             {
-              section: kb.ShortcutSection.FILE_LIST,
+              section: ShortcutSection.FILE_LIST,
               shortcuts: ['file list shortcuts'],
             },
           ]);
@@ -117,13 +116,13 @@
 
     test('diffs go on right', () => {
       update(new Map([
-        [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
+        [ShortcutSection.DIFFS, ['diffs shortcuts']],
       ]));
       assert.deepEqual(
           element._right,
           [
             {
-              section: kb.ShortcutSection.DIFFS,
+              section: ShortcutSection.DIFFS,
               shortcuts: ['diffs shortcuts'],
             },
           ]);
@@ -132,20 +131,20 @@
 
     test('multiple sections on each side', () => {
       update(new Map([
-        [kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
-        [kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
-        [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
-        [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
+        [ShortcutSection.ACTIONS, ['actions shortcuts']],
+        [ShortcutSection.DIFFS, ['diffs shortcuts']],
+        [ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
+        [ShortcutSection.NAVIGATION, ['navigation shortcuts']],
       ]));
       assert.deepEqual(
           element._left,
           [
             {
-              section: kb.ShortcutSection.EVERYWHERE,
+              section: ShortcutSection.EVERYWHERE,
               shortcuts: ['everywhere shortcuts'],
             },
             {
-              section: kb.ShortcutSection.NAVIGATION,
+              section: ShortcutSection.NAVIGATION,
               shortcuts: ['navigation shortcuts'],
             },
           ]);
@@ -153,11 +152,11 @@
           element._right,
           [
             {
-              section: kb.ShortcutSection.ACTIONS,
+              section: ShortcutSection.ACTIONS,
               shortcuts: ['actions shortcuts'],
             },
             {
-              section: kb.ShortcutSection.DIFFS,
+              section: ShortcutSection.DIFFS,
               shortcuts: ['diffs shortcuts'],
             },
           ]);
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 17a7b0b..b36435e 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
@@ -21,14 +21,13 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-account-dropdown/gr-account-dropdown.js';
 import '../gr-smart-search/gr-smart-search.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 {htmlTemplate} from './gr-main-header_html.js';
 import {getBaseUrl, getDocsBaseUrl} from '../../../utils/url-util.js';
-import {AdminNavBehavior} from '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {getAdminLinks} from '../../../utils/admin-nav-util.js';
 
 const DEFAULT_LINKS = [{
   title: 'Changes',
@@ -85,11 +84,9 @@
 /**
  * @extends PolymerElement
  */
-class GrMainHeader extends mixinBehaviors( [
-  AdminNavBehavior,
-], GestureEventListeners(
+class GrMainHeader extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-main-header'; }
@@ -272,7 +269,7 @@
       this.loading = false;
       this._topMenus = result[1];
 
-      return this.getAdminLinks(account,
+      return getAdminLinks(account,
           this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
           this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI))
           .then(res => {
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
rename to polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts
index e50b408..8ef54d6 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 2f32706..1f3fad9 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -228,8 +228,10 @@
   /**
    * Setup router implementation.
    *
-   * @param {function(!string)} navigate the router-abstracted equivalent of
-   *     `window.location.href = ...`. Takes a string.
+   * @param {function(!string, boolean=)} navigate the router-abstracted equivalent of
+   *     `window.location.href = ...` or window.location.replace(...). The
+   *     string is a new location and boolean defines is it redirect or not
+   *     (true means redirect, i.e. equivalent of window.location.replace).
    * @param {function(!Object): string} generateUrl generates a URL given
    *     navigation parameters, detailed in the file header.
    * @param {function(!Object): string} generateWeblinks weblinks generator
@@ -417,10 +419,15 @@
    * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
    *     used for none.
    * @param {boolean=} opt_isEdit
+   * @param {boolean=} opt_redirect redirect to a change - if true, the current
+   *     location (i.e. page which makes redirect) is not added to a history.
+   *     I.e. back/forward buttons skip current location
+   *
    */
-  navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
+  navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
+      opt_redirect) {
     this._navigate(this.getUrlForChange(change, opt_patchNum,
-        opt_basePatchNum, opt_isEdit));
+        opt_basePatchNum, opt_isEdit), opt_redirect);
   },
 
   /**
@@ -751,10 +758,10 @@
         .filter(section => (attentionEnabled || !section.attentionSetOnly))
         .filter(section => (assigneeEnabled || !section.assigneeOnly))
         .filter(section => (user === 'self' || !section.selfOnly))
-        .map(section => Object.assign({}, section, {
-          name: section.name,
-          query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
-        }));
+        .map(section => {
+          return {...section, name: section.name,
+            query: section.query.replace(USER_PLACEHOLDER_PATTERN, user)};
+        });
     return {title, sections};
   },
 };
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 83c91e0..60722d3 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -15,17 +15,15 @@
  * limitations under the License.
  */
 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import page from 'page/page.mjs';
 import {htmlTemplate} from './gr-router_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-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 {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
 import {appContext} from '../../../services/app-context.js';
+import {patchNumEquals} from '../../../utils/patch-set-util.js';
 
 const RoutePattern = {
   ROOT: '/',
@@ -203,7 +201,7 @@
 // gr-router and gr-app in the PolyGerritIndexHtml.soy file if needed
 const app = document.querySelector('#app');
 if (!app) {
-  console.log('No gr-app found (running tests)');
+  console.info('No gr-app found (running tests)');
 }
 
 // Setup listeners outside of the router component initialization.
@@ -216,12 +214,8 @@
 /**
  * @extends PolymerElement
  */
-class GrRouter extends mixinBehaviors( [
-  PatchSetBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrRouter extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-router'; }
@@ -381,34 +375,34 @@
     }
 
     if (params.query) {
-      return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
+      return '/q/' + encodeURL(params.query, true) + offsetExpr;
     }
 
     const operators = [];
     if (params.owner) {
-      operators.push('owner:' + this.encodeURL(params.owner, false));
+      operators.push('owner:' + encodeURL(params.owner, false));
     }
     if (params.project) {
-      operators.push('project:' + this.encodeURL(params.project, false));
+      operators.push('project:' + encodeURL(params.project, false));
     }
     if (params.branch) {
-      operators.push('branch:' + this.encodeURL(params.branch, false));
+      operators.push('branch:' + encodeURL(params.branch, false));
     }
     if (params.topic) {
-      operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
+      operators.push('topic:"' + encodeURL(params.topic, false) + '"');
     }
     if (params.hashtag) {
       operators.push('hashtag:"' +
-          this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
+          encodeURL(params.hashtag.toLowerCase(), false) + '"');
     }
     if (params.statuses) {
       if (params.statuses.length === 1) {
         operators.push(
-            'status:' + this.encodeURL(params.statuses[0], false));
+            'status:' + encodeURL(params.statuses[0], false));
       } else if (params.statuses.length > 1) {
         operators.push(
             '(' +
-            params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
+            params.statuses.map(s => `status:${encodeURL(s, false)}`)
                 .join(' OR ') +
             ')');
       }
@@ -434,7 +428,7 @@
       suffix += params.messageHash;
     }
     if (params.project) {
-      const encodedProject = this.encodeURL(params.project, true);
+      const encodedProject = encodeURL(params.project, true);
       return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
     } else {
       return `/c/${params.changeNum}${suffix}`;
@@ -458,7 +452,7 @@
       return `/dashboard/${user}?${queryParams.join('&')}`;
     } else if (repoName) {
       // Project dashboard.
-      const encodedRepo = this.encodeURL(repoName, true);
+      const encodedRepo = encodeURL(repoName, true);
       return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
     } else {
       // User dashboard.
@@ -491,7 +485,7 @@
     let range = this._getPatchRangeExpression(params);
     if (range.length) { range = '/' + range; }
 
-    let suffix = `${range}/${this.encodeURL(params.path, true)}`;
+    let suffix = `${range}/${encodeURL(params.path, true)}`;
 
     if (params.view === GerritNav.View.EDIT) { suffix += ',edit'; }
 
@@ -502,7 +496,7 @@
     }
 
     if (params.project) {
-      const encodedProject = this.encodeURL(params.project, true);
+      const encodedProject = encodeURL(params.project, true);
       return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
     } else {
       return `/c/${params.changeNum}${suffix}`;
@@ -514,7 +508,7 @@
    * @return {string}
    */
   _generateGroupUrl(params) {
-    let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
+    let url = `/admin/groups/${encodeURL(params.groupId + '', true)}`;
     if (params.detail === GerritNav.GroupDetailView.MEMBERS) {
       url += ',members';
     } else if (params.detail === GerritNav.GroupDetailView.LOG) {
@@ -528,7 +522,7 @@
    * @return {string}
    */
   _generateRepoUrl(params) {
-    let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
+    let url = `/admin/repos/${encodeURL(params.repoName + '', true)}`;
     if (params.detail === GerritNav.RepoDetailView.ACCESS) {
       url += ',access';
     } else if (params.detail === GerritNav.RepoDetailView.BRANCHES) {
@@ -607,7 +601,7 @@
     // Diffing a patch against itself is invalid, so if the base and revision
     // patches are equal clear the base.
     if (hasBasePatchNum &&
-        this.patchNumEquals(params.basePatchNum, params.patchNum)) {
+        patchNumEquals(params.basePatchNum, params.patchNum)) {
       needsRedirect = true;
       params.basePatchNum = null;
     } else if (hasBasePatchNum && !hasPatchNum) {
@@ -730,7 +724,13 @@
     }
 
     GerritNav.setup(
-        url => { page.show(url); },
+        (url, opt_redirect) => {
+          if (opt_redirect) {
+            page.redirect(url);
+          } else {
+            page.show(url);
+          }
+        },
         this._generateUrl.bind(this),
         params => this._generateWeblinks(params),
         x => x
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.ts
similarity index 91%
rename from polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
rename to polygerrit-ui/app/elements/core/gr-router/gr-router_html.ts
index 8aa0835..91d8b41 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 39d00069..6d50fcf 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
@@ -18,13 +18,11 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-search-bar_html.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 // Possible static search options for auto complete, without negations.
 const SEARCH_OPERATORS = [
@@ -115,12 +113,8 @@
 /**
  * @extends PolymerElement
  */
-class GrSearchBar extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  URLEncodingBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrSearchBar extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-search-bar'; }
@@ -205,7 +199,7 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.SEARCH]: '_handleSearch',
+      [Shortcut.SEARCH]: '_handleSearch',
     };
   }
 
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.ts
similarity index 95%
rename from polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
rename to polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.ts
index b0ef7af..a4d5e69 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
index 9529f9b..e5d04be 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
@@ -19,6 +19,7 @@
 import './gr-search-bar.js';
 import '../../../scripts/util.js';
 import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const basicFixture = fixtureFromElement('gr-search-bar');
 
@@ -27,7 +28,7 @@
 
   suiteSetup(() => {
     const kb = TestKeyboardShortcutBinder.push();
-    kb.bindShortcut(kb.Shortcut.SEARCH, '/');
+    kb.bindShortcut(Shortcut.SEARCH, '/');
   });
 
   suiteTeardown(() => {
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 2d1212b..772b412 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
@@ -16,13 +16,12 @@
  */
 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';
 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-smart-search_html.js';
-import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
+import {getUserName} from '../../../utils/display-name-util.js';
 
 const MAX_AUTOCOMPLETE_RESULTS = 10;
 const SELF_EXPRESSION = 'self';
@@ -31,11 +30,9 @@
 /**
  * @extends PolymerElement
  */
-class GrSmartSearch extends mixinBehaviors( [
-  DisplayNameBehavior,
-], GestureEventListeners(
+class GrSmartSearch extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-smart-search'; }
@@ -166,7 +163,7 @@
 
   _mapAccountsHelper(accounts, predicate) {
     return accounts.map(account => {
-      const userName = this.getUserName(this._serverConfig, account);
+      const userName = getUserName(this._serverConfig, account);
       return {
         label: account.name || '',
         text: account.email ?
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.ts
similarity index 94%
rename from polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
rename to polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.ts
index d490308..7088937 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles"></style>
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 6394bff..d051d30 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
@@ -141,7 +141,7 @@
 
   overridePartialPrefs(prefs) {
     // generate a smaller gr-diff than fullscreen for dialog
-    return Object.assign({}, prefs, {line_length: 50});
+    return {...prefs, line_length: 50};
   }
 
   onCancel(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
rename to polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
index a5a6ff2..057fd01 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 040b0bd..165d28c 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
@@ -15,13 +15,16 @@
  * limitations under the License.
  */
 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 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 {parseDate} from '../../../utils/date-util.js';
+import {
+  getParentIndex,
+  isMergeParent,
+  patchNumEquals,
+} from '../../../utils/patch-set-util.js';
 
 const PARENT = 'PARENT';
 
@@ -37,14 +40,6 @@
  */
 class ChangeComments {
   constructor(comments, robotComments, drafts, changeNum) {
-    // TODO(taoalpha): replace these with exported methods from patchset behavior
-    this._patchNumEquals =
-      PatchSetBehavior.patchNumEquals;
-    this._isMergeParent =
-      PatchSetBehavior.isMergeParent;
-    this._getParentIndex =
-      PatchSetBehavior.getParentIndex;
-
     this._comments = this._addPath(comments);
     this._robotComments = this._addPath(robotComments);
     this._drafts = this._addPath(drafts);
@@ -225,7 +220,7 @@
     }
     if (opt_patchNum) {
       allComments = allComments.filter(c =>
-        this._patchNumEquals(c.patch_set, opt_patchNum)
+        patchNumEquals(c.patch_set, opt_patchNum)
       );
     }
     return allComments.map(c => { return {...c}; });
@@ -271,7 +266,7 @@
     let comments = this._drafts[path] || [];
     if (opt_patchNum) {
       comments = comments.filter(c =>
-        this._patchNumEquals(c.patch_set, opt_patchNum)
+        patchNumEquals(c.patch_set, opt_patchNum)
       );
     }
     return comments.map(c => { return {...c, __draft: true}; });
@@ -385,7 +380,7 @@
     for (const file of Object.keys(comments)) {
       const commentsForFile = [];
       for (const comment of comments[file]) {
-        commentsForFile.push(Object.assign({__path: file}, comment));
+        commentsForFile.push({__path: file, ...comment});
       }
       commentArr = commentArr.concat(commentsForFile);
     }
@@ -530,20 +525,20 @@
   // appears on a specific parent then only show the comment if the parent
   // index of the comment matches that of the range.
     if (comment.parent && comment.side === PARENT) {
-      return this._isMergeParent(range.basePatchNum) &&
-        comment.parent === this._getParentIndex(range.basePatchNum);
+      return isMergeParent(range.basePatchNum) &&
+        comment.parent === getParentIndex(range.basePatchNum);
     }
 
     // If the base of the range is the parent of the patch:
     if (range.basePatchNum === PARENT &&
       comment.side === PARENT &&
-      this._patchNumEquals(comment.patch_set, range.patchNum)) {
+      patchNumEquals(comment.patch_set, range.patchNum)) {
       return true;
     }
     // If the base of the range is not the parent of the patch:
     return range.basePatchNum !== PARENT &&
         comment.side !== PARENT &&
-        this._patchNumEquals(comment.patch_set, range.basePatchNum);
+        patchNumEquals(comment.patch_set, range.basePatchNum);
   }
 
   /**
@@ -557,7 +552,7 @@
   _isInRevisionOfPatchRange(comment,
       range) {
     return comment.side !== PARENT &&
-      this._patchNumEquals(comment.patch_set, range.patchNum);
+      patchNumEquals(comment.patch_set, range.patchNum);
   }
 
   /**
@@ -576,11 +571,9 @@
 /**
  * @extends PolymerElement
  */
-class GrCommentApi extends mixinBehaviors( [
-  PatchSetBehavior,
-], GestureEventListeners(
+class GrCommentApi extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-comment-api'; }
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
deleted file mode 100644
index 8aa0835..0000000
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
+++ /dev/null
@@ -1,21 +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-rest-api-interface id="restAPI"></gr-rest-api-interface>
-`;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.ts
similarity index 91%
copy from polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
copy to polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.ts
index 8aa0835..91d8b41 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
rename to polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.ts
index 3ed33d1..1489006 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html``;
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 bb617f0..ef75da0 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
@@ -312,7 +312,7 @@
       return;
     }
 
-    const localPrefs = Object.assign({}, prefs);
+    const localPrefs = {...prefs};
     if (this.path === COMMIT_MSG_PATH) {
       // override line_length for commit msg the same way as
       // in gr-diff
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts
index 4d6b890..573f559 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <div class="contentWrapper">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index a88ac27..dfa4599 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -21,6 +21,7 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff-builder-element.js';
+import {stubBaseUrl} from '../../../test/test-utils.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
@@ -62,6 +63,7 @@
       getLoggedIn() { return Promise.resolve(false); },
       getProjectConfig() { return Promise.resolve({}); },
     });
+    stubBaseUrl('/r');
     prefs = {
       line_length: 10,
       show_tabs: true,
@@ -1230,6 +1232,9 @@
           `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}`
         + '<br><br>Testing Commit'
       );
+
+      const url = blameNode.getElementsByClassName('blameDate');
+      assert.equal(url[0].getAttribute('href'), '/r/q/1234567890');
     });
   });
 });
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 04f1548..3a6e0a3 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
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {getBaseUrl} from '../../../utils/url-util.js';
 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';
@@ -611,7 +612,7 @@
 
   const shaNode = this._createElement('a', 'blameDate');
   shaNode.innerText = `${date}`;
-  shaNode.setAttribute('href', `/q/${commit.id}`);
+  shaNode.setAttribute('href', `${getBaseUrl()}/q/${commit.id}`);
   blameNode.appendChild(shaNode);
 
   const shortName = commit.author.split(' ')[0];
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.ts
similarity index 93%
rename from polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.ts
index e400792..712a93d 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-cursor-manager
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.ts
similarity index 93%
rename from polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.ts
index afd4ac5..5a6cb1c 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 bdf41a9..d90221a 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
@@ -20,17 +20,16 @@
 import '../gr-diff/gr-diff.js';
 import '../gr-syntax-layer/gr-syntax-layer.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 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 {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';
+import {getParentIndex, isMergeParent} from '../../../utils/patch-set-util.js';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -85,11 +84,9 @@
  *
  * @extends PolymerElement
  */
-class GrDiffHost extends mixinBehaviors( [
-  PatchSetBehavior,
-], GestureEventListeners(
+class GrDiffHost extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-diff-host'; }
@@ -732,7 +729,7 @@
         isOnParent: comment.side === 'PARENT',
       };
       if (comment.range) {
-        newThread.range = Object.assign({}, comment.range);
+        newThread.range = {...comment.range};
       }
       threads.push(newThread);
     }
@@ -967,8 +964,8 @@
    * @return {number|null}
    */
   _computeParentIndex(patchRangeRecord) {
-    return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
-      this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
+    return isMergeParent(patchRangeRecord.base.basePatchNum) ?
+      getParentIndex(patchRangeRecord.base.basePatchNum) : null;
   }
 
   _handleCommentSave(e) {
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.ts
index aa05bb5..38b0d6d 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-diff
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
index b5393ea..40f6a32 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts
index d65ba1f..9c942a3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 b9c97a9..a4954a1 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
@@ -509,14 +509,20 @@
 
       if (chunk.ab) {
         result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
-            .map(({lines, keyLocation}) =>
-              Object.assign({}, chunk, {ab: lines, keyLocation})));
+            .map(({lines, keyLocation}) => {
+              return {
+                ...chunk,
+                ab: lines,
+                keyLocation,
+              };
+            }));
       } else if (chunk.common) {
         const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds);
         const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds);
-        result.push(...aChunks.map(({lines, keyLocation}, i) =>
-          Object.assign(
-              {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation})));
+        result.push(...aChunks.map(({lines, keyLocation}, i) => {
+          return {
+            ...chunk, a: lines, b: bChunks[i].lines, keyLocation};
+        }));
       }
     }
 
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 cfbe481..6d9060f 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
@@ -17,14 +17,12 @@
 import '../../../styles/shared-styles.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 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 {querySelectorAll} from '../../../utils/dom-util.js';
+import {descendedFromClass, querySelectorAll} from '../../../utils/dom-util.js';
 
 /**
  * Possible CSS classes indicating the state of selection. Dynamically added/
@@ -42,11 +40,9 @@
 /**
  * @extends PolymerElement
  */
-class GrDiffSelection extends mixinBehaviors( [
-  DomUtilBehavior,
-], GestureEventListeners(
+class GrDiffSelection extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-diff-selection'; }
@@ -178,7 +174,7 @@
    * @return {boolean}
    */
   _elementDescendedFromClass(element, className) {
-    return this.descendedFromClass(element, className,
+    return descendedFromClass(element, className,
         this.diffBuilder.diffElement);
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.ts
index 620ef02..bd0e034 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <div class="contentWrapper">
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 d3cdfbe..6823156 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
@@ -33,19 +33,26 @@
 import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
 import '../gr-patch-range-select/gr-patch-range-select.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-view_html.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 {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
+import {KeyboardShortcutMixin, Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 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';
+import {
+  computeAllPatchSets,
+  computeLatestPatchNum,
+  patchNumEquals,
+  SPECIAL_PATCH_SET_NUM,
+} from '../../../utils/patch-set-util.js';
+import {
+  addUnmodifiedFiles, computeDisplayPath, computeTruncatedPath,
+  isMagicPath, specialFilePathCompare,
+} from '../../../utils/path-list-util.js';
+import {changeBaseURL, changeIsOpen} from '../../../utils/change-util.js';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
 const MSG_LOADING_BLAME = 'Loading blame...';
@@ -64,17 +71,10 @@
 };
 
 /**
- * @appliesMixin PatchSetMixin
  * @extends PolymerElement
  */
-class GrDiffView extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  PatchSetBehavior,
-  PathListBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrDiffView extends KeyboardShortcutMixin(
+    GestureEventListeners(LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-diff-view'; }
@@ -226,7 +226,7 @@
       },
       _allPatchSets: {
         type: Array,
-        computed: 'computeAllPatchSets(_change, _change.revisions.*)',
+        computed: '_computeAllPatchSets(_change, _change.revisions.*)',
       },
       _revisionInfo: {
         type: Object,
@@ -270,47 +270,47 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.LEFT_PANE]: '_handleLeftPane',
-      [this.Shortcut.RIGHT_PANE]: '_handleRightPane',
-      [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
-      [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
-      [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
-      [this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
+      [Shortcut.LEFT_PANE]: '_handleLeftPane',
+      [Shortcut.RIGHT_PANE]: '_handleRightPane',
+      [Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
+      [Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
+      [Shortcut.VISIBLE_LINE]: '_handleVisibleLine',
+      [Shortcut.NEXT_FILE_WITH_COMMENTS]:
           '_handleNextLineOrFileWithComments',
-      [this.Shortcut.PREV_FILE_WITH_COMMENTS]:
+      [Shortcut.PREV_FILE_WITH_COMMENTS]:
           '_handlePrevLineOrFileWithComments',
-      [this.Shortcut.NEW_COMMENT]: '_handleNewComment',
-      [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
-      [this.Shortcut.NEXT_FILE]: '_handleNextFile',
-      [this.Shortcut.PREV_FILE]: '_handlePrevFile',
-      [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
-      [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
-      [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
-      [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
-      [this.Shortcut.OPEN_REPLY_DIALOG]:
+      [Shortcut.NEW_COMMENT]: '_handleNewComment',
+      [Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
+      [Shortcut.NEXT_FILE]: '_handleNextFile',
+      [Shortcut.PREV_FILE]: '_handlePrevFile',
+      [Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
+      [Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
+      [Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
+      [Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
+      [Shortcut.OPEN_REPLY_DIALOG]:
           '_handleOpenReplyDialogOrToggleLeftPane',
-      [this.Shortcut.TOGGLE_LEFT_PANE]:
+      [Shortcut.TOGGLE_LEFT_PANE]:
           '_handleOpenReplyDialogOrToggleLeftPane',
-      [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
-      [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
-      [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
-      [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
-      [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]:
+      [Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
+      [Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
+      [Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
+      [Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
+      [Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
+      [Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
+      [Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
+      [Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
           '_handleToggleHideAllCommentThreads',
-      [this.Shortcut.DIFF_AGAINST_BASE]: '_handleDiffAgainstBase',
-      [this.Shortcut.DIFF_AGAINST_LATEST]: '_handleDiffAgainstLatest',
-      [this.Shortcut.DIFF_BASE_AGAINST_LEFT]: '_handleDiffBaseAgainstLeft',
-      [this.Shortcut.DIFF_RIGHT_AGAINST_LATEST]:
+      [Shortcut.DIFF_AGAINST_BASE]: '_handleDiffAgainstBase',
+      [Shortcut.DIFF_AGAINST_LATEST]: '_handleDiffAgainstLatest',
+      [Shortcut.DIFF_BASE_AGAINST_LEFT]: '_handleDiffBaseAgainstLeft',
+      [Shortcut.DIFF_RIGHT_AGAINST_LATEST]:
         '_handleDiffRightAgainstLatest',
-      [this.Shortcut.DIFF_BASE_AGAINST_LATEST]:
+      [Shortcut.DIFF_BASE_AGAINST_LATEST]:
         '_handleDiffBaseAgainstLatest',
 
       // Final two are actually handled by gr-comment-thread.
-      [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
-      [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
+      [Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
+      [Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
     };
   }
 
@@ -390,10 +390,10 @@
         changeNum, patchRange).then(changeFiles => {
       if (!changeFiles) return;
       const commentedPaths = changeComments.getPaths(patchRange);
-      const files = Object.assign({}, changeFiles);
-      this.addUnmodifiedFiles(files, commentedPaths);
+      const files = {...changeFiles};
+      addUnmodifiedFiles(files, commentedPaths);
       this._files = {
-        sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
+        sortedFileList: Object.keys(files).sort(specialFilePathCompare),
         changeFilesByPath: files,
       };
     });
@@ -765,7 +765,7 @@
     // has been queued, the event can bubble up to the handler in gr-app.
     this.async(() => {
       this.dispatchEvent(new CustomEvent('title-change', {
-        detail: {title: this.computeTruncatedPath(this._path)},
+        detail: {title: computeTruncatedPath(this._path)},
         composed: true, bubbles: true,
       }));
     });
@@ -820,7 +820,7 @@
           const edit = r[4];
           if (edit) {
             this.set('_change.revisions.' + edit.commit.commit, {
-              _number: this.EDIT_NAME,
+              _number: SPECIAL_PATCH_SET_NUM.EDIT,
               basePatchNum: edit.base_patch_set_number,
               commit: edit.commit,
             });
@@ -897,7 +897,7 @@
   _pathChanged(path) {
     if (path) {
       this.dispatchEvent(new CustomEvent('title-change', {
-        detail: {title: this.computeTruncatedPath(path)},
+        detail: {title: computeTruncatedPath(path)},
         composed: true, bubbles: true,
       }));
     }
@@ -981,8 +981,8 @@
     const dropdownContent = [];
     for (const path of files.sortedFileList) {
       dropdownContent.push({
-        text: this.computeDisplayPath(path),
-        mobileText: this.computeTruncatedPath(path),
+        text: computeDisplayPath(path),
+        mobileText: computeTruncatedPath(path),
         value: path,
         bottomText: this._computeCommentString(changeComments, patchNum,
             path, files.changeFilesByPath[path]),
@@ -1034,8 +1034,8 @@
 
   _handlePatchChange(e) {
     const {basePatchNum, patchNum} = e.detail;
-    if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
-        this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
+    if (patchNumEquals(basePatchNum, this._patchRange.basePatchNum) &&
+        patchNumEquals(patchNum, this._patchRange.patchNum)) { return; }
     GerritNav.navigateToDiff(
         this._change, this._path, patchNum, basePatchNum);
   }
@@ -1134,7 +1134,7 @@
       patchNum = patchRange.basePatchNum;
     }
 
-    let url = this.changeBaseURL(project, changeNum, patchNum) +
+    let url = changeBaseURL(project, changeNum, patchNum) +
         `/files/${encodeURIComponent(path)}/download`;
 
     if (isBase && comparedAgainsParent) {
@@ -1145,7 +1145,7 @@
   }
 
   _computeDownloadPatchLink(project, changeNum, patchRange, path) {
-    let url = this.changeBaseURL(project, changeNum, patchRange.patchNum);
+    let url = changeBaseURL(project, changeNum, patchRange.patchNum);
     url += '/patch?zip&path=' + encodeURIComponent(path);
     return url;
   }
@@ -1239,7 +1239,7 @@
    */
   _computeEditMode(patchRangeRecord) {
     const patchRange = patchRangeRecord.base || {};
-    return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME);
+    return patchNumEquals(patchRange.patchNum, SPECIAL_PATCH_SET_NUM.EDIT);
   }
 
   /**
@@ -1299,7 +1299,8 @@
 
   _handleDiffAgainstBase(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    if (this.patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) {
+    if (patchNumEquals(this._patchRange.basePatchNum,
+        SPECIAL_PATCH_SET_NUM.PARENT)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Base is already selected.',
@@ -1314,7 +1315,8 @@
 
   _handleDiffBaseAgainstLeft(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    if (this.patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) {
+    if (patchNumEquals(this._patchRange.basePatchNum,
+        SPECIAL_PATCH_SET_NUM.PARENT)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Left is already base.',
@@ -1330,8 +1332,8 @@
   _handleDiffAgainstLatest(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
 
-    const latestPatchNum = this.computeLatestPatchNum(this._allPatchSets);
-    if (this.patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
+    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Latest is already selected.',
@@ -1348,8 +1350,8 @@
 
   _handleDiffRightAgainstLatest(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    const latestPatchNum = this.computeLatestPatchNum(this._allPatchSets);
-    if (this.patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
+    const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
+    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Right is already latest.',
@@ -1364,9 +1366,10 @@
 
   _handleDiffBaseAgainstLatest(e) {
     if (this.shouldSuppressKeyboardShortcut(e)) { return; }
-    const latestPatchNum = this.computeLatestPatchNum(this._allPatchSets);
-    if (this.patchNumEquals(this._patchRange.patchNum, latestPatchNum) &&
-      this.patchNumEquals(this._patchRange.basePatchNum, 'PARENT')) {
+    const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
+    if (patchNumEquals(this._patchRange.patchNum, latestPatchNum) &&
+      patchNumEquals(this._patchRange.basePatchNum,
+          SPECIAL_PATCH_SET_NUM.PARENT)) {
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {
           message: 'Already diffing base against latest.',
@@ -1379,7 +1382,7 @@
   }
 
   _computeBlameLoaderClass(isImageDiff, path) {
-    return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
+    return !isMagicPath(path) && !isImageDiff ? 'show' : '';
   }
 
   _getRevisionInfo(change) {
@@ -1440,7 +1443,21 @@
         .some(arg => arg === undefined)) {
       return false;
     }
-    return loggedIn && this.changeIsOpen(changeChangeRecord.base);
+    return loggedIn && changeIsOpen(changeChangeRecord.base);
+  }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _computeAllPatchSets(change) {
+    return computeAllPatchSets(change);
+  }
+
+  /**
+   * Wrapper for using in the element template and computed properties
+   */
+  _computeDisplayPath(path) {
+    return computeDisplayPath(path);
   }
 }
 
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index b05a4ac..e2cb880 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -371,7 +371,7 @@
       >
         &lt;</a
       >
-      <div class="fullFileName mobile">[[computeDisplayPath(_path)]]</div>
+      <div class="fullFileName mobile">[[_computeDisplayPath(_path)]]</div>
       <a
         class="mobileNavLink"
         href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 8646336..ce36a06 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -20,7 +20,9 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {ChangeStatus} from '../../../constants/constants.js';
-import {TestKeyboardShortcutBinder} from '../../../test/test-utils';
+import {generateChange, TestKeyboardShortcutBinder} from '../../../test/test-utils';
+import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const basicFixture = fixtureFromElement('gr-diff-view');
 
@@ -32,32 +34,32 @@
 
     suiteSetup(() => {
       const kb = TestKeyboardShortcutBinder.push();
-      kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
-      kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
-      kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
-      kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
-      kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
-      kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
-      kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
-      kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
-      kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
-      kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
-      kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
-      kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
-      kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
-      kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
-      kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
-      kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
-      kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
-      kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
-      kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
-      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');
+      kb.bindShortcut(Shortcut.LEFT_PANE, 'shift+left');
+      kb.bindShortcut(Shortcut.RIGHT_PANE, 'shift+right');
+      kb.bindShortcut(Shortcut.NEXT_LINE, 'j', 'down');
+      kb.bindShortcut(Shortcut.PREV_LINE, 'k', 'up');
+      kb.bindShortcut(Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+      kb.bindShortcut(Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+      kb.bindShortcut(Shortcut.NEW_COMMENT, 'c');
+      kb.bindShortcut(Shortcut.SAVE_COMMENT, 'ctrl+s');
+      kb.bindShortcut(Shortcut.NEXT_FILE, ']');
+      kb.bindShortcut(Shortcut.PREV_FILE, '[');
+      kb.bindShortcut(Shortcut.NEXT_CHUNK, 'n');
+      kb.bindShortcut(Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+      kb.bindShortcut(Shortcut.PREV_CHUNK, 'p');
+      kb.bindShortcut(Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+      kb.bindShortcut(Shortcut.OPEN_REPLY_DIALOG, 'a');
+      kb.bindShortcut(Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+      kb.bindShortcut(Shortcut.UP_TO_CHANGE, 'u');
+      kb.bindShortcut(Shortcut.OPEN_DIFF_PREFS, ',');
+      kb.bindShortcut(Shortcut.TOGGLE_DIFF_MODE, 'm');
+      kb.bindShortcut(Shortcut.TOGGLE_FILE_REVIEWED, 'r');
+      kb.bindShortcut(Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+      kb.bindShortcut(Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+      kb.bindShortcut(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
+      kb.bindShortcut(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
+      kb.bindShortcut(Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+      kb.bindShortcut(Shortcut.TOGGLE_BLAME, 'b');
     });
 
     suiteTeardown(() => {
@@ -283,12 +285,12 @@
     });
 
     test('diff against latest', () => {
+      element._change = generateChange({revisionsCount: 12});
       element._patchRange = {
         basePatchNum: '5',
         patchNum: '10',
       };
       sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
-      sinon.stub(element, 'computeLatestPatchNum').returns(12);
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffAgainstLatest(new CustomEvent(''));
       const args = diffNavStub.getCall(0).args;
@@ -297,12 +299,11 @@
     });
 
     test('_handleDiffBaseAgainstLeft', () => {
-      element._changeNum = '1';
+      element._change = generateChange({revisionsCount: 10});
       element._patchRange = {
         patchNum: 3,
         basePatchNum: 1,
       };
-      sinon.stub(element, 'computeLatestPatchNum').returns(10);
       sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffBaseAgainstLeft(new CustomEvent(''));
@@ -313,12 +314,11 @@
     });
 
     test('_handleDiffRightAgainstLatest', () => {
-      element._changeNum = '1';
+      element._change = generateChange({revisionsCount: 10});
       element._patchRange = {
         basePatchNum: 1,
         patchNum: 3,
       };
-      sinon.stub(element, 'computeLatestPatchNum').returns(10);
       sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffRightAgainstLatest(new CustomEvent(''));
@@ -329,12 +329,11 @@
     });
 
     test('_handleDiffBaseAgainstLatest', () => {
-      element._changeNum = '1';
+      element._change = generateChange({revisionsCount: 10});
       element._patchRange = {
         basePatchNum: 1,
         patchNum: 3,
       };
-      sinon.stub(element, 'computeLatestPatchNum').returns(10);
       sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
       const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
       element._handleDiffBaseAgainstLatest(new CustomEvent(''));
@@ -912,7 +911,7 @@
     test('file review status with edit loaded', () => {
       const saveReviewedStub = sinon.stub(element, '_saveReviewedState');
 
-      element._patchRange = {patchNum: element.EDIT_NAME};
+      element._patchRange = {patchNum: SPECIAL_PATCH_SET_NUM.EDIT};
       flushAsynchronousOperations();
 
       assert.isTrue(element._editMode);
@@ -1382,7 +1381,7 @@
         element._patchRange = {patchNum: '1'};
         // Reviewed checkbox should be shown.
         assert.isTrue(isVisible(element.$.reviewed));
-        element.set('_patchRange.patchNum', element.EDIT_NAME);
+        element.set('_patchRange.patchNum', SPECIAL_PATCH_SET_NUM.EDIT);
         flushAsynchronousOperations();
 
         assert.isFalse(isVisible(element.$.reviewed));
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 4d96d53..2c5787a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -23,14 +23,17 @@
 import '../gr-ranged-comment-themes/gr-ranged-comment-theme.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {htmlTemplate} from './gr-diff_html.js';
-import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {GrDiffLine} from './gr-diff-line.js';
 import {DiffSide, rangesEqual} from './gr-diff-utils.js';
 import {getHiddenScroll} from '../../../scripts/hiddenscroll.js';
+import {
+  isMergeParent,
+  patchNumEquals,
+  SPECIAL_PATCH_SET_NUM,
+} from '../../../utils/patch-set-util.js';
 
 const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
 const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
@@ -69,11 +72,9 @@
 /**
  * @extends PolymerElement
  */
-class GrDiff extends mixinBehaviors( [
-  PatchSetBehavior,
-], GestureEventListeners(
+class GrDiff extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-diff'; }
@@ -291,10 +292,11 @@
     this._unobserveNodes();
   }
 
-  showNoChangeMessage(loading, prefs, diffLength) {
+  showNoChangeMessage(loading, prefs, diffLength, diff) {
     return !loading &&
-      prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
-      diffLength === 0;
+        diff && !diff.binary &&
+        prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' &&
+        diffLength === 0;
   }
 
   _enableSelectionObserver(loggedIn, isAttached) {
@@ -566,9 +568,9 @@
       this.patchRange.basePatchNum :
       this.patchRange.patchNum;
 
-    const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
-    const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
-        this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
+    const isEdit = patchNumEquals(patchNum, SPECIAL_PATCH_SET_NUM.EDIT);
+    const isEditBase = patchNumEquals(patchNum, SPECIAL_PATCH_SET_NUM.PARENT) &&
+        patchNumEquals(this.patchRange.patchNum, SPECIAL_PATCH_SET_NUM.EDIT);
 
     if (isEdit) {
       this.dispatchEvent(new CustomEvent('show-alert', {
@@ -657,7 +659,7 @@
     if ((lineEl.classList.contains(DiffSide.LEFT) ||
         contentEl.classList.contains('remove')) &&
         this.patchRange.basePatchNum !== 'PARENT' &&
-        !this.isMergeParent(this.patchRange.basePatchNum)) {
+        !isMergeParent(this.patchRange.basePatchNum)) {
       patchNum = this.patchRange.basePatchNum;
     }
     return patchNum;
@@ -668,7 +670,7 @@
     return (lineEl.classList.contains(DiffSide.LEFT) ||
         contentEl.classList.contains('remove')) &&
         (this.patchRange.basePatchNum === 'PARENT' ||
-            this.isMergeParent(this.patchRange.basePatchNum));
+            isMergeParent(this.patchRange.basePatchNum));
   }
 
   /** @return {string} */
@@ -878,7 +880,7 @@
    */
   _getBypassPrefs() {
     if (this._safetyBypass !== null) {
-      return Object.assign({}, this.prefs, {context: this._safetyBypass});
+      return {...this.prefs, context: this._safetyBypass};
     }
     return this.prefs;
   }
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.ts
similarity index 99%
rename from polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index 279f968..f18af23 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -432,7 +432,7 @@
 
           <template
             is="dom-if"
-            if="[[showNoChangeMessage(loading, prefs, _diffLength)]]"
+            if="[[showNoChangeMessage(loading, prefs, _diffLength, diff)]]"
           >
             <div class="whitespace-change-only-message">
               This file only contains whitespace changes. Modify the whitespace
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index d60d174..02f09a8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -25,6 +25,7 @@
 import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
 import {runA11yAudit} from '../../../test/a11y-test-utils.js';
 import '@polymer/paper-button/paper-button.js';
+import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
 
 const basicFixture = fixtureFromElement('gr-diff');
 
@@ -75,14 +76,14 @@
 
   test('line limit with line_wrapping', () => {
     element = basicFixture.instantiate();
-    element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
+    element.prefs = {...MINIMAL_PREFS, line_wrapping: true};
     flushAsynchronousOperations();
     assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
   });
 
   test('line limit without line_wrapping', () => {
     element = basicFixture.instantiate();
-    element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
+    element.prefs = {...MINIMAL_PREFS, line_wrapping: false};
     flushAsynchronousOperations();
     assert.isNotOk(getComputedStyleValue('--line-limit', element));
   });
@@ -224,7 +225,7 @@
       element.path = 'file.txt';
 
       element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
-          getMockDiffResponse(), Object.assign({}, MINIMAL_PREFS));
+          getMockDiffResponse(), {...MINIMAL_PREFS});
 
       // No thread groups.
       assert.isNotOk(element._getThreadGroupForLine(contentEl));
@@ -654,7 +655,7 @@
     });
 
     test('addDraftAtLine on an edit', () => {
-      element.patchRange.basePatchNum = element.EDIT_NAME;
+      element.patchRange.basePatchNum = SPECIAL_PATCH_SET_NUM.EDIT;
       sinon.stub(element, '_selectLine');
       sinon.stub(element, '_createComment');
       const alertSpy = sinon.spy();
@@ -665,8 +666,8 @@
     });
 
     test('addDraftAtLine on an edit base', () => {
-      element.patchRange.patchNum = element.EDIT_NAME;
-      element.patchRange.basePatchNum = element.PARENT_NAME;
+      element.patchRange.patchNum = SPECIAL_PATCH_SET_NUM.EDIT;
+      element.patchRange.basePatchNum = SPECIAL_PATCH_SET_NUM.PARENT;
       sinon.stub(element, '_selectLine');
       sinon.stub(element, '_createComment');
       const alertSpy = sinon.spy();
@@ -692,22 +693,22 @@
 
       test('change in preferences re-renders diff', () => {
         sinon.stub(element, '_renderDiffTable');
-        element.prefs = Object.assign(
-            {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+        element.prefs = {
+          ...MINIMAL_PREFS, time_format: 'HHMM_12'};
         element.flushDebouncer('renderDiffTable');
         assert.isTrue(element._renderDiffTable.called);
       });
 
       test('adding/removing property in preferences re-renders diff', () => {
         const stub = sinon.stub(element, '_renderDiffTable');
-        const newPrefs1 = Object.assign({}, MINIMAL_PREFS,
-            {line_wrapping: true});
+        const newPrefs1 = {...MINIMAL_PREFS,
+          line_wrapping: true};
         element.prefs = newPrefs1;
         element.flushDebouncer('renderDiffTable');
         assert.isTrue(element._renderDiffTable.called);
         stub.reset();
 
-        const newPrefs2 = Object.assign({}, newPrefs1);
+        const newPrefs2 = {...newPrefs1};
         delete newPrefs2.line_wrapping;
         element.prefs = newPrefs2;
         element.flushDebouncer('renderDiffTable');
@@ -718,8 +719,8 @@
           'noRenderOnPrefsChange', () => {
         sinon.stub(element, '_renderDiffTable');
         element.noRenderOnPrefsChange = true;
-        element.prefs = Object.assign(
-            {}, MINIMAL_PREFS, {time_format: 'HHMM_12'});
+        element.prefs = {
+          ...MINIMAL_PREFS, time_format: 'HHMM_12'};
         element.flushDebouncer('renderDiffTable');
         assert.isFalse(element._renderDiffTable.called);
       });
@@ -786,7 +787,7 @@
     });
 
     test('large render w/ context = 10', done => {
-      element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10});
+      element.prefs = {...MINIMAL_PREFS, context: 10};
       function rendered() {
         assert.isTrue(renderStub.called);
         assert.isFalse(element._showWarning);
@@ -798,7 +799,7 @@
     });
 
     test('large render w/ whole file and bypass', done => {
-      element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+      element.prefs = {...MINIMAL_PREFS, context: -1};
       element._safetyBypass = 10;
       function rendered() {
         assert.isTrue(renderStub.called);
@@ -811,7 +812,7 @@
     });
 
     test('large render w/ whole file and no bypass', done => {
-      element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1});
+      element.prefs = {...MINIMAL_PREFS, context: -1};
       function rendered() {
         assert.isFalse(renderStub.called);
         assert.isTrue(element._showWarning);
@@ -978,6 +979,8 @@
   });
   const setupSampleDiff = function(params) {
     const {ignore_whitespace, content} = params;
+    // binary can't be undefined, use false if not set
+    const binary = params.binary || false;
     element = basicFixture.instantiate();
     element.prefs = {
       ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
@@ -1006,7 +1009,7 @@
         'file differ',
       ],
       content,
-      binary: false,
+      binary,
     };
     element._renderDiffTable();
     flushAsynchronousOperations();
@@ -1025,7 +1028,7 @@
     }
     setupSampleDiff({content});
     assertDiffTableWithContent();
-    element.diff = Object.assign({}, element.diff);
+    element.diff = {...element.diff};
     // immediately cleaned up
     assert.equal(element.$.diffTable.innerHTML, '');
     element._renderDiffTable();
@@ -1080,7 +1083,18 @@
       assert.isTrue(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
+      ));
+    });
+
+    test('do not show the message for binary files', () => {
+      setupSampleDiff({content: [{skip: 100}], binary: true});
+      assert.isFalse(element.showNoChangeMessage(
+          /* loading= */ false,
+          element.prefs,
+          element._diffLength,
+          element.diff
       ));
     });
 
@@ -1089,7 +1103,8 @@
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ true,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
       ));
     });
 
@@ -1108,7 +1123,8 @@
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
       ));
     });
 
@@ -1126,7 +1142,8 @@
       assert.isFalse(element.showNoChangeMessage(
           /* loading= */ false,
           element.prefs,
-          element._diffLength
+          element._diffLength,
+          element.diff
       ));
     });
   });
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 6a9b0bf..0744d0d 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
@@ -18,14 +18,19 @@
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
 import '../../shared/gr-select/gr-select.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 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';
+import {
+  computeLatestPatchNum, findSortedIndex, getParentIndex,
+  getRevisionByPatchNum,
+  isMergeParent,
+  patchNumEquals, sortRevisions,
+  SPECIAL_PATCH_SET_NUM,
+} from '../../../utils/patch-set-util.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -39,11 +44,9 @@
  * @property {string} basePatchNum
  * @extends PolymerElement
  */
-class GrPatchRangeSelect extends mixinBehaviors( [
-  PatchSetBehavior,
-], GestureEventListeners(
+class GrPatchRangeSelect extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-patch-range-select'; }
@@ -112,10 +115,8 @@
       const basePatchNum = basePatch.num;
       const entry = this._createDropdownEntry(basePatchNum, 'Patchset ',
           _sortedRevisions, changeComments, this._getShaForPatch(basePatch));
-      dropdownContent.push(Object.assign({}, entry, {
-        disabled: this._computeLeftDisabled(
-            basePatch.num, patchNum, _sortedRevisions),
-      }));
+      dropdownContent.push({...entry, disabled: this._computeLeftDisabled(
+          basePatch.num, patchNum, _sortedRevisions)});
     }
 
     dropdownContent.push({
@@ -160,10 +161,11 @@
       const entry = this._createDropdownEntry(
           patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions,
           changeComments, this._getShaForPatch(patch));
-      dropdownContent.push(Object.assign({}, entry, {
-        disabled: this._computeRightDisabled(basePatchNum, patchNum,
-            _sortedRevisions),
-      }));
+      dropdownContent.push({
+        ...entry,
+        disabled: this._computeRightDisabled(
+            basePatchNum, patchNum, _sortedRevisions),
+      });
     }
     return dropdownContent;
   }
@@ -194,7 +196,7 @@
 
   _updateSortedRevisions(revisionsRecord) {
     const revisions = revisionsRecord.base;
-    this._sortedRevisions = this.sortRevisions(Object.values(revisions));
+    this._sortedRevisions = sortRevisions(Object.values(revisions));
   }
 
   /**
@@ -207,8 +209,8 @@
    * @param {!Array} sortedRevisions
    */
   _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) {
-    return this.findSortedIndex(basePatchNum, sortedRevisions) <=
-        this.findSortedIndex(patchNum, sortedRevisions);
+    return findSortedIndex(basePatchNum, sortedRevisions) <=
+        findSortedIndex(patchNum, sortedRevisions);
   }
 
   /**
@@ -228,16 +230,18 @@
    * @return {boolean}
    */
   _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) {
-    if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; }
-
-    if (this.isMergeParent(basePatchNum)) {
-      // Note: parent indices use 1-offset.
-      return this.revisionInfo.getParentCount(patchNum) <
-          this.getParentIndex(basePatchNum);
+    if (patchNumEquals(basePatchNum, SPECIAL_PATCH_SET_NUM.PARENT)) {
+      return false;
     }
 
-    return this.findSortedIndex(basePatchNum, sortedRevisions) <=
-        this.findSortedIndex(patchNum, sortedRevisions);
+    if (isMergeParent(basePatchNum)) {
+      // Note: parent indices use 1-offset.
+      return this.revisionInfo.getParentCount(patchNum) <
+          getParentIndex(basePatchNum);
+    }
+
+    return findSortedIndex(basePatchNum, sortedRevisions) <=
+        findSortedIndex(patchNum, sortedRevisions);
   }
 
   _computePatchSetCommentsString(changeComments, patchNum) {
@@ -267,7 +271,7 @@
    * @param {boolean=} opt_addFrontSpace
    */
   _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) {
-    const rev = this.getRevisionByPatchNum(revisions, patchNum);
+    const rev = getRevisionByPatchNum(revisions, patchNum);
     return (rev && rev.description) ?
       (opt_addFrontSpace ? ' ' : '') +
         rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
@@ -278,7 +282,7 @@
    * @param {number|string} patchNum
    */
   _computePatchSetDate(revisions, patchNum) {
-    const rev = this.getRevisionByPatchNum(revisions, patchNum);
+    const rev = getRevisionByPatchNum(revisions, patchNum);
     return rev ? rev.created : undefined;
   }
 
@@ -289,7 +293,7 @@
   _handlePatchChange(e) {
     const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum};
     const target = dom(e).localTarget;
-    const latestPatchNum = this.computeLatestPatchNum(this.availablePatches);
+    const latestPatchNum = computeLatestPatchNum(this.availablePatches);
     if (target === this.$.patchNumDropdown) {
       if (detail.patchNum === e.detail.value) return;
       this.reporting.reportInteraction('right-patchset-changed',
@@ -302,7 +306,7 @@
           });
       detail.patchNum = e.detail.value;
     } else {
-      if (this.patchNumEquals(detail.basePatchNum, e.detail.value)) return;
+      if (patchNumEquals(detail.basePatchNum, e.detail.value)) return;
       this.reporting.reportInteraction('left-patchset-changed',
           {
             previous: detail.basePatchNum,
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
rename to polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
index cc11cfa..415ef4b 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
index 9145b19..782ffc5 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
@@ -24,6 +24,7 @@
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
 import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
 
 const commentApiMockElement = createCommentApiMockWithTemplateElement(
     'gr-patch-range-select-comment-api-mock', html`
@@ -71,7 +72,7 @@
     };
     const sortedRevisions = [
       {_number: 3},
-      {_number: element.EDIT_NAME, basePatchNum: 2},
+      {_number: SPECIAL_PATCH_SET_NUM.EDIT, basePatchNum: 2},
       {_number: 2},
       {_number: 1},
     ];
@@ -85,7 +86,7 @@
     }
     assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
 
-    patchRange.basePatchNum = element.EDIT_NAME;
+    patchRange.basePatchNum = SPECIAL_PATCH_SET_NUM.EDIT;
     assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
         sortedRevisions));
     assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
@@ -95,7 +96,7 @@
     assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
         sortedRevisions));
     assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
-        element.EDIT_NAME, sortedRevisions));
+        SPECIAL_PATCH_SET_NUM.EDIT, sortedRevisions));
   });
 
   test('_computeBaseDropdownContent', () => {
@@ -119,7 +120,7 @@
     const patchNum = 1;
     const sortedRevisions = [
       {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
-      {_number: element.EDIT_NAME, basePatchNum: 2},
+      {_number: SPECIAL_PATCH_SET_NUM.EDIT, basePatchNum: 2},
       {_number: 2, description: 'description'},
       {_number: 1},
     ];
@@ -283,7 +284,7 @@
     const basePatchNum = 1;
     const sortedRevisions = [
       {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
-      {_number: element.EDIT_NAME, basePatchNum: 2},
+      {_number: SPECIAL_PATCH_SET_NUM.EDIT, basePatchNum: 2},
       {_number: 2, description: 'description'},
       {_number: 1},
     ];
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 231e4b5..830a91a 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
@@ -223,7 +223,7 @@
         .map(range => {
           // Make a copy, so that the normalization below does not mess with
           // our map.
-          range = Object.assign({}, range);
+          range = {...range};
           range.end = range.end === -1 ? line.text.length : range.end;
 
           // Normalize invalid ranges where the start is after the end but the
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
rename to polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.ts
index 3ed33d1..1489006 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html``;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
similarity index 85%
rename from polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
rename to polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
index 49ed980..70ee196 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-ranged-comment-theme">
@@ -38,4 +44,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
rename to polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.ts
index e7795b9..24d63b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
rename to polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.ts
index 433f814..ac59f4f 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-lib-loader id="libLoader"></gr-lib-loader>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
similarity index 94%
rename from polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
rename to polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
index 76a01de..ac015e1 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-syntax-theme">
@@ -118,4 +124,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
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 352f1d8..8e7bd2d 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
@@ -18,20 +18,17 @@
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-list-view/gr-list-view.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-documentation-search_html.js';
-import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
+import {ListViewMixin} from '../../../mixins/gr-list-view-mixin/gr-list-view-mixin.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
 
 /**
  * @extends PolymerElement
  */
-class GrDocumentationSearch extends mixinBehaviors( [
-  ListViewBehavior,
-], GestureEventListeners(
+class GrDocumentationSearch extends ListViewMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
rename to polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
index b637b75..de0a990 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
rename to polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.ts
index 7368289..ba8af3c 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 d3a09fe..86a2e61 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
@@ -23,23 +23,19 @@
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-edit-controls_html.js';
-import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {GrEditConstants} from '../gr-edit-constants.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
  * @extends PolymerElement
  */
-class GrEditControls extends mixinBehaviors( [
-  PatchSetBehavior,
-], GestureEventListeners(
+class GrEditControls extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-edit-controls'; }
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
rename to polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.ts
index 02639c0..b67f6cd 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
rename to polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.ts
index ec0b8b4..c6a6de7 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 0f459a0..d1677bd 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
@@ -23,15 +23,14 @@
 import '../../shared/gr-storage/gr-storage.js';
 import '../gr-default-editor/gr-default-editor.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-editor-view_html.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 {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
+import {computeTruncatedPath} from '../../../utils/path-list-util.js';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const SAVING_MESSAGE = 'Saving changes...';
@@ -43,11 +42,7 @@
 /**
  * @extends PolymerElement
  */
-class GrEditorView extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  PatchSetBehavior,
-  PathListBehavior,
-], GestureEventListeners(
+class GrEditorView extends KeyboardShortcutMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
@@ -139,14 +134,14 @@
 
     this._changeNum = value.changeNum;
     this._path = value.path;
-    this._patchNum = value.patchNum || this.EDIT_NAME;
+    this._patchNum = value.patchNum || SPECIAL_PATCH_SET_NUM.EDIT;
     this._lineNum = value.lineNum;
 
     // NOTE: This may be called before attachment (e.g. while parentElement is
     // null). Fire title-change in an async so that, if attachment to the DOM
     // has been queued, the event can bubble up to the handler in gr-app.
     this.async(() => {
-      const title = `Editing ${this.computeTruncatedPath(this._path)}`;
+      const title = `Editing ${computeTruncatedPath(this._path)}`;
       this.dispatchEvent(new CustomEvent('title-change', {
         detail: {title},
         composed: true, bubbles: true,
@@ -182,9 +177,10 @@
   }
 
   _viewEditInChangeView() {
-    const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
+    const patch = this._successfulSave ? SPECIAL_PATCH_SET_NUM.EDIT
+      : this._patchNum;
     GerritNav.navigateToChange(this._change, patch, null,
-        patch !== this.EDIT_NAME);
+        patch !== SPECIAL_PATCH_SET_NUM.EDIT);
   }
 
   _getFileData(changeNum, path, patchNum) {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
rename to polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
index 9dc35b2..bd8304f 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
index 618f595..a673955 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-editor-view.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
 
 const basicFixture = fixtureFromElement('gr-editor-view');
 
@@ -50,7 +51,7 @@
   suite('_paramsChanged', () => {
     test('incorrect view returns immediately', () => {
       element._paramsChanged(
-          Object.assign({}, mockParams, {view: GerritNav.View.DIFF}));
+          {...mockParams, view: GerritNav.View.DIFF});
       assert.notOk(element._changeNum);
     });
 
@@ -63,7 +64,7 @@
       });
 
       const promises = element._paramsChanged(
-          Object.assign({}, mockParams, {view: GerritNav.View.EDIT}));
+          {...mockParams, view: GerritNav.View.EDIT});
 
       flushAsynchronousOperations();
       assert.equal(element._changeNum, mockParams.changeNum);
@@ -284,15 +285,15 @@
   test('_viewEditInChangeView respects _patchNum', () => {
     navigateStub.restore();
     const navStub = sinon.stub(GerritNav, 'navigateToChange');
-    element._patchNum = element.EDIT_NAME;
+    element._patchNum = SPECIAL_PATCH_SET_NUM.EDIT;
     element._viewEditInChangeView();
-    assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
+    assert.equal(navStub.lastCall.args[1], SPECIAL_PATCH_SET_NUM.EDIT);
     element._patchNum = '1';
     element._viewEditInChangeView();
     assert.equal(navStub.lastCall.args[1], '1');
     element._successfulSave = true;
     element._viewEditInChangeView();
-    assert.equal(navStub.lastCall.args[1], element.EDIT_NAME);
+    assert.equal(navStub.lastCall.args[1], SPECIAL_PATCH_SET_NUM.EDIT);
   });
 
   suite('keyboard shortcuts', () => {
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 63a7e9c..394d929 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -40,24 +40,25 @@
 import './shared/gr-fixed-panel/gr-fixed-panel.js';
 import './shared/gr-lib-loader/gr-lib-loader.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-app-element_html.js';
 import {getBaseUrl} from '../utils/url-util.js';
-import {KeyboardShortcutBehavior} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {
+  KeyboardShortcutMixin,
+  Shortcut,
+  SPECIAL_SHORTCUT,
+} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
 import {appContext} from '../services/app-context.js';
 
 /**
  * @extends PolymerElement
  */
-class GrAppElement extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrAppElement extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-app-element'; }
@@ -147,12 +148,12 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
-      [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
-      [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
-      [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
-      [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
-      [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
+      [Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
+      [Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard',
+      [Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
+      [Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
+      [Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
+      [Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges',
     };
   }
 
@@ -175,6 +176,10 @@
         e => this._handleRpcLog(e));
     this.addEventListener('shortcut-triggered',
         e => this._handleShortcutTriggered(e));
+    // Ideally individual views should handle this event and respond with a soft
+    // reload. This is a catch-all for all views that cannot or have not
+    // implemented that.
+    this.addEventListener('reload', e => window.location.reload());
   }
 
   /** @override */
@@ -231,143 +236,146 @@
   }
 
   _bindKeyboardShortcuts() {
-    this.bindShortcut(this.Shortcut.SEND_REPLY,
-        this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
-    this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN,
-        this.DOC_ONLY, ':');
+    this.bindShortcut(Shortcut.SEND_REPLY,
+        SPECIAL_SHORTCUT.DOC_ONLY, 'ctrl+enter', 'meta+enter');
+    this.bindShortcut(Shortcut.EMOJI_DROPDOWN,
+        SPECIAL_SHORTCUT.DOC_ONLY, ':');
 
     this.bindShortcut(
-        this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
+        Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
     this.bindShortcut(
-        this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i');
+        Shortcut.GO_TO_USER_DASHBOARD, SPECIAL_SHORTCUT.GO_KEY, 'i');
     this.bindShortcut(
-        this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+        Shortcut.GO_TO_OPENED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'o');
     this.bindShortcut(
-        this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
+        Shortcut.GO_TO_MERGED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'm');
     this.bindShortcut(
-        this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
+        Shortcut.GO_TO_ABANDONED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'a');
     this.bindShortcut(
-        this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w');
+        Shortcut.GO_TO_WATCHED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'w');
 
     this.bindShortcut(
-        this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
+        Shortcut.CURSOR_NEXT_CHANGE, 'j');
     this.bindShortcut(
-        this.Shortcut.CURSOR_PREV_CHANGE, 'k');
+        Shortcut.CURSOR_PREV_CHANGE, 'k');
     this.bindShortcut(
-        this.Shortcut.OPEN_CHANGE, 'o');
+        Shortcut.OPEN_CHANGE, 'o');
     this.bindShortcut(
-        this.Shortcut.NEXT_PAGE, 'n', ']');
+        Shortcut.NEXT_PAGE, 'n', ']');
     this.bindShortcut(
-        this.Shortcut.PREV_PAGE, 'p', '[');
+        Shortcut.PREV_PAGE, 'p', '[');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
+        Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
+        Shortcut.TOGGLE_CHANGE_STAR, 's:keyup');
     this.bindShortcut(
-        this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
+        Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup');
     this.bindShortcut(
-        this.Shortcut.EDIT_TOPIC, 't');
+        Shortcut.EDIT_TOPIC, 't');
 
     this.bindShortcut(
-        this.Shortcut.OPEN_REPLY_DIALOG, 'a:keyup');
+        Shortcut.OPEN_REPLY_DIALOG, 'a:keyup');
     this.bindShortcut(
-        this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd:keyup');
+        Shortcut.OPEN_DOWNLOAD_DIALOG, 'd:keyup');
     this.bindShortcut(
-        this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
+        Shortcut.EXPAND_ALL_MESSAGES, 'x');
     this.bindShortcut(
-        this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
+        Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
     this.bindShortcut(
-        this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
+        Shortcut.REFRESH_CHANGE, 'shift+r:keyup');
     this.bindShortcut(
-        this.Shortcut.UP_TO_DASHBOARD, 'u');
+        Shortcut.UP_TO_DASHBOARD, 'u');
     this.bindShortcut(
-        this.Shortcut.UP_TO_CHANGE, 'u');
+        Shortcut.UP_TO_CHANGE, 'u');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
+        Shortcut.TOGGLE_DIFF_MODE, 'm:keyup');
     this.bindShortcut(
-        this.Shortcut.DIFF_AGAINST_BASE, this.V_KEY, 'down', 's');
+        Shortcut.DIFF_AGAINST_BASE, SPECIAL_SHORTCUT.V_KEY, 'down', 's');
     this.bindShortcut(
-        this.Shortcut.DIFF_AGAINST_LATEST, this.V_KEY, 'up', 'w');
+        Shortcut.DIFF_AGAINST_LATEST, SPECIAL_SHORTCUT.V_KEY, 'up', 'w');
     this.bindShortcut(
-        this.Shortcut.DIFF_BASE_AGAINST_LEFT, this.V_KEY, 'left', 'a');
+        Shortcut.DIFF_BASE_AGAINST_LEFT,
+        SPECIAL_SHORTCUT.V_KEY, 'left', 'a');
     this.bindShortcut(
-        this.Shortcut.DIFF_RIGHT_AGAINST_LATEST, this.V_KEY, 'right', 'd');
+        Shortcut.DIFF_RIGHT_AGAINST_LATEST,
+        SPECIAL_SHORTCUT.V_KEY, 'right', 'd');
     this.bindShortcut(
-        this.Shortcut.DIFF_BASE_AGAINST_LATEST, this.V_KEY, 'b');
+        Shortcut.DIFF_BASE_AGAINST_LATEST, SPECIAL_SHORTCUT.V_KEY, 'b');
 
     this.bindShortcut(
-        this.Shortcut.NEXT_LINE, 'j', 'down');
+        Shortcut.NEXT_LINE, 'j', 'down');
     this.bindShortcut(
-        this.Shortcut.PREV_LINE, 'k', 'up');
+        Shortcut.PREV_LINE, 'k', 'up');
     if (this._isCursorManagerSupportMoveToVisibleLine()) {
       this.bindShortcut(
-          this.Shortcut.VISIBLE_LINE, '.');
+          Shortcut.VISIBLE_LINE, '.');
     }
     this.bindShortcut(
-        this.Shortcut.NEXT_CHUNK, 'n');
+        Shortcut.NEXT_CHUNK, 'n');
     this.bindShortcut(
-        this.Shortcut.PREV_CHUNK, 'p');
+        Shortcut.PREV_CHUNK, 'p');
     this.bindShortcut(
-        this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
+        Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
     this.bindShortcut(
-        this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
+        Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
     this.bindShortcut(
-        this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
+        Shortcut.PREV_COMMENT_THREAD, 'shift+p');
     this.bindShortcut(
-        this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+        Shortcut.EXPAND_ALL_COMMENT_THREADS,
+        SPECIAL_SHORTCUT.DOC_ONLY, 'e');
     this.bindShortcut(
-        this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
-        this.DOC_ONLY, 'shift+e');
+        Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+        SPECIAL_SHORTCUT.DOC_ONLY, 'shift+e');
     this.bindShortcut(
-        this.Shortcut.LEFT_PANE, 'shift+left');
+        Shortcut.LEFT_PANE, 'shift+left');
     this.bindShortcut(
-        this.Shortcut.RIGHT_PANE, 'shift+right');
+        Shortcut.RIGHT_PANE, 'shift+right');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+        Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
     this.bindShortcut(
-        this.Shortcut.NEW_COMMENT, 'c');
+        Shortcut.NEW_COMMENT, 'c');
     this.bindShortcut(
-        this.Shortcut.SAVE_COMMENT,
+        Shortcut.SAVE_COMMENT,
         'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
     this.bindShortcut(
-        this.Shortcut.OPEN_DIFF_PREFS, ',');
+        Shortcut.OPEN_DIFF_PREFS, ',');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
+        Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup');
 
     this.bindShortcut(
-        this.Shortcut.NEXT_FILE, ']');
+        Shortcut.NEXT_FILE, ']');
     this.bindShortcut(
-        this.Shortcut.PREV_FILE, '[');
+        Shortcut.PREV_FILE, '[');
     this.bindShortcut(
-        this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
+        Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
     this.bindShortcut(
-        this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
+        Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
     this.bindShortcut(
-        this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+        Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
     this.bindShortcut(
-        this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
+        Shortcut.CURSOR_PREV_FILE, 'k', 'up');
     this.bindShortcut(
-        this.Shortcut.OPEN_FILE, 'o', 'enter');
+        Shortcut.OPEN_FILE, 'o', 'enter');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
+        Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup');
     this.bindShortcut(
-        this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
+        Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
+        Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
+        Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_BLAME, 'b');
+        Shortcut.TOGGLE_BLAME, 'b');
     this.bindShortcut(
-        this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
+        Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
 
     this.bindShortcut(
-        this.Shortcut.OPEN_FIRST_FILE, ']');
+        Shortcut.OPEN_FIRST_FILE, ']');
     this.bindShortcut(
-        this.Shortcut.OPEN_LAST_FILE, '[');
+        Shortcut.OPEN_LAST_FILE, '[');
 
     this.bindShortcut(
-        this.Shortcut.SEARCH, '/');
+        Shortcut.SEARCH, '/');
   }
 
   _isCursorManagerSupportMoveToVisibleLine() {
@@ -567,13 +575,13 @@
 
   _logWelcome() {
     console.group('Runtime Info');
-    console.log('Gerrit UI (PolyGerrit)');
-    console.log(`Gerrit Server Version: ${this._version}`);
+    console.info('Gerrit UI (PolyGerrit)');
+    console.info(`Gerrit Server Version: ${this._version}`);
     if (window.VERSION_INFO) {
-      console.log(`UI Version Info: ${window.VERSION_INFO}`);
+      console.info(`UI Version Info: ${window.VERSION_INFO}`);
     }
     if (this._feedbackUrl) {
-      console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`);
+      console.info(`Please file bugs and feedback at: ${this._feedbackUrl}`);
     }
     console.groupEnd();
   }
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/gr-app-element_html.js
rename to polygerrit-ui/app/elements/gr-app-element_html.ts
index 75295bc..66624e3 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.js
+++ b/polygerrit-ui/app/elements/gr-app-element_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 9961971..402dff2 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -22,7 +22,7 @@
  * expose these variables until plugins switch to direct import from polygerrit.
  */
 
-import {GrDisplayNameUtils} from '../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import {getAccountDisplayName, getDisplayName, getGroupDisplayName, getUserName} from '../utils/display-name-util.js';
 import {GrAnnotation} from './diff/gr-diff-highlight/gr-annotation.js';
 import {GrAttributeHelper} from './plugins/gr-attribute-helper/gr-attribute-helper.js';
 import {GrDiffLine} from './diff/gr-diff/gr-diff-line.js';
@@ -49,7 +49,6 @@
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {util} from '../scripts/util.js';
 import page from 'page/page.mjs';
-import {Auth} from './shared/gr-rest-api-interface/gr-auth.js';
 import {appContext} from '../services/app-context.js';
 import {GrAdminApi} from './plugins/gr-admin-api/gr-admin-api.js';
 import {GrAnnotationActionsContext} from './shared/gr-js-api-interface/gr-annotation-actions-context.js';
@@ -74,7 +73,12 @@
 import {_setHiddenScroll, getHiddenScroll} from '../scripts/hiddenscroll.js';
 
 export function initGlobalVariables() {
-  window.GrDisplayNameUtils = GrDisplayNameUtils;
+  window.GrDisplayNameUtils = {
+    getUserName,
+    getDisplayName,
+    getAccountDisplayName,
+    getGroupDisplayName,
+  };
   window.GrAnnotation = GrAnnotation;
   window.GrAttributeHelper = GrAttributeHelper;
   window.GrDiffLine = GrDiffLine;
@@ -104,7 +108,7 @@
   window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
   window.util = util;
   window.page = page;
-  window.Auth = Auth;
+  window.Auth = appContext.authService;
   window.EventEmitter = appContext.eventEmitter;
   window.GrAdminApi = GrAdminApi;
   window.GrAnnotationActionsContext = GrAnnotationActionsContext;
@@ -132,6 +136,7 @@
   window.Gerrit = window.Gerrit || {};
   window.Gerrit.Nav = GerritNav;
   window.Gerrit.getRootElement = getRootElement;
+  window.Gerrit.Auth = appContext.authService;
 
   window.Gerrit._pluginLoader = pluginLoader;
   window.Gerrit._endpoints = pluginEndpoints;
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index ce925e1..fef7ab9 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {safeTypesBridge} from '../utils/safe-types-util.js';
+
 // We need to use goog.declareModuleId internally in google for TS-imports-JS
 // case. To avoid errors when goog is not available, the empty implementation is
 // added.
@@ -41,14 +43,13 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-app_html.js';
-import {SafeTypes} from '../behaviors/safe-types-behavior/safe-types-behavior.js';
 import {initGerritPluginApi} from './shared/gr-js-api-interface/gr-gerrit.js';
 import {appContext} from '../services/app-context.js';
 
 security.polymer_resin.install({
   allowedIdentifierPrefixes: [''],
   reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER,
-  safeTypesBridge: SafeTypes.safeTypesBridge,
+  safeTypesBridge,
 });
 
 /** @extends PolymerElement */
diff --git a/polygerrit-ui/app/elements/gr-app_html.js b/polygerrit-ui/app/elements/gr-app_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/gr-app_html.js
rename to polygerrit-ui/app/elements/gr-app_html.ts
index 3da1b69..f6172c9 100644
--- a/polygerrit-ui/app/elements/gr-app_html.js
+++ b/polygerrit-ui/app/elements/gr-app_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-app-element id="app-element"></gr-app-element>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
rename to polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.ts
index c4310fc..94196df 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html` <slot></slot> `;
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
deleted file mode 100644
index c4310fc..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-export const htmlTemplate = html` <slot></slot> `;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.ts
similarity index 91%
copy from polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
copy to polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.ts
index c4310fc..94196df 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html` <slot></slot> `;
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
rename to polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.ts
index 5d2cae7..aa7a92d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
rename to polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.ts
index 1e5088b3..6eee643 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
rename to polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
index fd94821..d69a279 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
rename to polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.ts
index 1cd9ce2..194ca2b 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 2051947..55ce596 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
@@ -20,21 +20,17 @@
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-form-styles.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-table-editor_html.js';
-import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
+import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin.js';
 
 /**
  * @extends PolymerElement
  */
-class GrChangeTableEditor extends mixinBehaviors( [
-  ChangeTableBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrChangeTableEditor extends ChangeTableMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-change-table-editor'; }
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
rename to polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
index d63e627..1233cf1 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
rename to polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.ts
index b1a951b7..c461718 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
rename to polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts
index f2c476a..47e4592 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
rename to polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.ts
index 977e95d..525fca6 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 6c6ad01..d1bc7eb 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
@@ -72,9 +72,8 @@
   }
 
   save() {
-    const promises = this._keysToRemove.map(key => {
-      this.$.restAPI.deleteAccountGPGKey(key.id);
-    });
+    const promises = this._keysToRemove
+        .map(key => this.$.restAPI.deleteAccountGPGKey(key.id));
 
     return Promise.all(promises).then(() => {
       this._keysToRemove = [];
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
rename to polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
index 19b8d0c..432bc4f 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
rename to polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.ts
index d5350aa..e52583d 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
rename to polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.ts
index 0474b99..41084b5 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
rename to polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.ts
index bf50124..1472103 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
rename to polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.ts
index ceb8958..e4d66e2 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 fe4a61c..0f92428 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
@@ -86,7 +86,7 @@
       // Using Object.assign here allows preservation of the default values
       // supplied in the value generating function of this._account, unless
       // they are overridden by properties in the account from the response.
-      this._account = Object.assign({}, this._account, account);
+      this._account = {...this._account, ...account};
     });
 
     const loadConfig = this.$.restAPI.getConfig().then(config => {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
rename to polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts
index 3559ba6..11fbbc9 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-form-styles">
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.ts
similarity index 92%
rename from polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
rename to polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.ts
index 26200d4..786abc0 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
rename to polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.ts
index 95433ac..fc3edcd 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 c1ca460..06d9183 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
@@ -40,13 +40,12 @@
 import '../gr-menu-editor/gr-menu-editor.js';
 import '../gr-ssh-editor/gr-ssh-editor.js';
 import '../gr-watched-projects-editor/gr-watched-projects-editor.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 {htmlTemplate} from './gr-settings-view_html.js';
 import {getDocsBaseUrl} from '../../../utils/url-util.js';
-import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
+import {ChangeTableMixin} from '../../../mixins/gr-change-table-mixin/gr-change-table-mixin.js';
 
 const PREFS_SECTION_FIELDS = [
   'changes_per_page',
@@ -77,9 +76,7 @@
 /**
  * @extends PolymerElement
  */
-class GrSettingsView extends mixinBehaviors( [
-  ChangeTableBehavior,
-], GestureEventListeners(
+class GrSettingsView extends ChangeTableMixin(GestureEventListeners(
     LegacyElementMixin(
         PolymerElement))) {
   static get template() { return htmlTemplate; }
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.ts
similarity index 99%
rename from polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
rename to polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 0e0f86c..e80e646 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 d869128..9c819e9 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
@@ -63,9 +63,8 @@
   }
 
   save() {
-    const promises = this._keysToRemove.map(key => {
-      this.$.restAPI.deleteAccountSSHKey(key.seq);
-    });
+    const promises = this._keysToRemove
+        .map(key => this.$.restAPI.deleteAccountSSHKey(key.seq));
 
     return Promise.all(promises).then(() => {
       this._keysToRemove = [];
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
rename to polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.ts
index 1f3c793..96f770a 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
rename to polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts
index b1ca653..e3a90a2 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
rename to polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.ts
index 7c75488..f77b8cc 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
rename to polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
index afd427a..c6c2b7f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 f666148..a6c2bd7 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
@@ -19,21 +19,18 @@
 import '../gr-avatar/gr-avatar.js';
 import '../gr-hovercard-account/gr-hovercard-account.js';
 import '../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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-account-label_html.js';
-import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
+import {getDisplayName} from '../../../utils/display-name-util.js';
 
 /**
  * @extends PolymerElement
  */
-class GrAccountLabel extends mixinBehaviors( [
-  DisplayNameBehavior,
-], GestureEventListeners(
+class GrAccountLabel extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-account-label'; }
@@ -52,6 +49,15 @@
       change: Object,
       voteableText: String,
       /**
+       * Should this user be considered to be in the attention set, regardless
+       * of the current state of the change object? This can be used in a widget
+       * that allows the user to make adjustments to the attention set.
+       */
+      forceAttention: {
+        type: Boolean,
+        value: false,
+      },
+      /**
        * Should attention set related features be shown in the component? Note
        * that the information whether the user is in the attention set or not is
        * part of the ChangeInfo object in the change property.
@@ -93,23 +99,21 @@
     this.$.restAPI.getConfig().then(config => { this._config = config; });
   }
 
-  get isAttentionSetEnabled() {
-    return !!this._config && !!this._config.change
-        && !!this._config.change.enable_attention_set
-        && !!this.highlightAttention && !!this.change && !!this.account;
+  _isAttentionSetEnabled(config, highlight, account, change) {
+    return !!config && !!config.change
+        && !!config.change.enable_attention_set
+        && !!highlight && !!change && !!account;
   }
 
-  get hasAttention() {
-    if (!this.isAttentionSetEnabled || !this.change.attention_set) return false;
-    return this.change.attention_set.hasOwnProperty(this.account._account_id);
-  }
-
-  _computeShowAttentionIcon(config, highlightAttention, account, change) {
-    return this.isAttentionSetEnabled && this.hasAttention;
+  _hasAttention(config, highlight, account, change, force) {
+    if (force) return true;
+    return this._isAttentionSetEnabled(config, highlight, account, change)
+        && change.attention_set
+        && change.attention_set.hasOwnProperty(account._account_id);
   }
 
   _computeName(account, config) {
-    return this.getDisplayName(config, account);
+    return getDisplayName(config, account);
   }
 }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
rename to polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
index c090416..b338c4c 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -78,7 +78,7 @@
     </template>
     <template
       is="dom-if"
-      if="[[_computeShowAttentionIcon(_config, highlightAttention, account, change)]]"
+      if="[[_hasAttention(_config, highlightAttention, account, change, forceAttention)]]"
     >
       <iron-icon class="attention" icon="gr-icons:attention"></iron-icon>
     </template>
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
rename to polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.ts
index 44afb84..dfcef4e 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 6f5f9e4..0cf9af2 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
@@ -168,7 +168,7 @@
     let itemTypeAdded = 'unknown';
     if (item.account) {
       const account =
-          Object.assign({}, item.account, {_pendingAdd: true});
+          {...item.account, _pendingAdd: true};
       this.push('accounts', account);
       itemTypeAdded = 'account';
     } else if (item.group) {
@@ -176,8 +176,8 @@
         this.pendingConfirmation = item;
         return;
       }
-      const group = Object.assign({}, item.group,
-          {_pendingAdd: true, _group: true});
+      const group = {...item.group,
+        _pendingAdd: true, _group: true};
       this.push('accounts', group);
       itemTypeAdded = 'group';
     } else if (this.allowAnyInput) {
@@ -204,8 +204,8 @@
   }
 
   confirmGroup(group) {
-    group = Object.assign(
-        {}, group, {confirmed: true, _pendingAdd: true, _group: true});
+    group = {
+      ...group, confirmed: true, _pendingAdd: true, _group: true};
     this.push('accounts', group);
     this.pendingConfirmation = null;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
rename to polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
index 6fee9f3..2824bb5 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
rename to polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
index e9f386d..d2aed40 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 3cf91fe..f4c4886 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
@@ -15,26 +15,21 @@
  * limitations under the License.
  */
 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';
 import '../../../styles/shared-styles.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-autocomplete-dropdown_html.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
+import {IronFitMixin} from '../../../mixins/iron-fit-mixin/iron-fit-mixin.js';
 
 /**
  * @extends PolymerElement
  */
-class GrAutocompleteDropdown extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  IronFitBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrAutocompleteDropdown extends IronFitMixin(KeyboardShortcutMixin(
+    GestureEventListeners(LegacyElementMixin(PolymerElement)))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-autocomplete-dropdown'; }
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
rename to polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.ts
index fecaa73..d3d2481 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 554559a..90b966f 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -20,12 +20,11 @@
 import '../gr-icons/gr-icons.js';
 import '../../../styles/shared-styles.js';
 import {flush, dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-autocomplete_html.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
 const DEBOUNCE_WAIT_MS = 200;
@@ -33,11 +32,9 @@
 /**
  * @extends PolymerElement
  */
-class GrAutocomplete extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrAutocomplete extends KeyboardShortcutMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-autocomplete'; }
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
rename to polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.ts
index c0e8abf..8ab4828 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
rename to polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
index cc8a42f..0d8e78f 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 346d84e..3039255 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -16,25 +16,21 @@
  */
 import '@polymer/paper-button/paper-button.js';
 import '../../../styles/shared-styles.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 {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 {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {getEventPath} from '../../../utils/dom-util.js';
 import {appContext} from '../../../services/app-context.js';
 
 /**
  * @extends PolymerElement
  */
-class GrButton extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-  TooltipBehavior,
-], GestureEventListeners(
+class GrButton extends KeyboardShortcutMixin(TooltipMixin(GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-button'; }
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
rename to polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
index e992ffc..b272951 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 95%
rename from polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
rename to polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
index 3b7b1af..c7930c0 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
rename to polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 38e620f..542d8be 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 eb68024..e6f4a01 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
@@ -19,17 +19,16 @@
 import '../gr-storage/gr-storage.js';
 import '../gr-comment/gr-comment.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 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 {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.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';
+import {computeDisplayPath} from '../../../utils/path-list-util.js';
 
 const UNRESOLVED_EXPAND_COUNT = 5;
 const NEWLINE_PATTERN = /\n/g;
@@ -37,15 +36,10 @@
 /**
  * @extends PolymerElement
  */
-class GrCommentThread extends mixinBehaviors( [
-  /**
-   * Not used in this element rather other elements tests
-   */
-  KeyboardShortcutBehavior,
-  PathListBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrCommentThread extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
+  // KeyboardShortcutMixin Not used in this element rather other elements tests
+
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-comment-thread'; }
@@ -245,7 +239,7 @@
   }
 
   _computeDisplayPath(path) {
-    const displayPath = this.computeDisplayPath(path);
+    const displayPath = computeDisplayPath(path);
     if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
       return `Patchset`;
     }
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
rename to polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
index 837a58f..4c15383 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 e27940e..93f3fed 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -30,14 +30,13 @@
 import '../gr-tooltip-content/gr-tooltip-content.js';
 import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js';
 import {flush, dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-comment_html.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
-import {GrDisplayNameUtils} from '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import {getDisplayName} from '../../../utils/display-name-util.js';
 import {appContext} from '../../../services/app-context.js';
 
 const STORAGE_DEBOUNCE_INTERVAL = 400;
@@ -47,6 +46,7 @@
 const DRAFT_SINGULAR = 'draft...';
 const DRAFT_PLURAL = 'drafts...';
 const SAVED_MESSAGE = 'All changes saved';
+const UNSAVED_MESSAGE = 'Unable to save draft';
 
 const REPORT_CREATE_DRAFT = 'CreateDraftComment';
 const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
@@ -54,6 +54,8 @@
 
 const FILE = 'FILE';
 
+export const __testOnly_UNSAVED_MESSAGE = UNSAVED_MESSAGE;
+
 /**
  * All candidates tips to show, will pick randomly.
  */
@@ -69,11 +71,8 @@
 /**
  * @extends PolymerElement
  */
-class GrComment extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrComment extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-comment'; }
@@ -223,6 +222,10 @@
         value: false,
       },
       _serverConfig: Object,
+      _unableToSave: {
+        type: Boolean,
+        value: false,
+      },
     };
   }
 
@@ -377,6 +380,16 @@
     return this.$.restAPI.getIsAdmin();
   }
 
+  _computeDraftTooltip(unableToSave) {
+    return unableToSave ? `Unable to save draft. Please try to save again.` :
+      `This draft is only visible to you. To publish drafts, click the 'Reply'`
+    + `or 'Start review' button at the top of the change or press the 'A' key.`;
+  }
+
+  _computeDraftText(unableToSave) {
+    return 'DRAFT' + (unableToSave ? '(Failed to save)' : '');
+  }
+
   /**
    * @param {*=} opt_comment
    */
@@ -457,10 +470,8 @@
    * @return {!Object}
    */
   _getEventPayload(opt_mixin) {
-    return Object.assign({}, opt_mixin, {
-      comment: this.comment,
-      patchNum: this.patchNum,
-    });
+    return {...opt_mixin, comment: this.comment,
+      patchNum: this.patchNum};
   }
 
   _fireSave() {
@@ -714,7 +725,10 @@
     this._closeOverlay(this.confirmDiscardOverlay);
   }
 
-  _getSavingMessage(numPending) {
+  _getSavingMessage(numPending, requestFailed) {
+    if (requestFailed) {
+      return UNSAVED_MESSAGE;
+    }
     if (numPending === 0) {
       return SAVED_MESSAGE;
     }
@@ -741,10 +755,12 @@
     // Cancel the debouncer so that error toasts from the error-manager will
     // not be overridden.
     this.cancelDebouncer('draft-toast');
+    this._updateRequestToast(this._numPendingDraftRequests.number,
+        /* requestFailed=*/true);
   }
 
-  _updateRequestToast(numPending) {
-    const message = this._getSavingMessage(numPending);
+  _updateRequestToast(numPending, requestFailed) {
+    const message = this._getSavingMessage(numPending, requestFailed);
     this.debounce('draft-toast', () => {
       // Note: the event is fired on the body rather than this element because
       // this element may not be attached by the time this executes, in which
@@ -758,9 +774,13 @@
     this._showStartRequest();
     return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
         .then(result => {
-          if (result.ok) {
+          if (result.ok) { // remove
+            this._unableToSave = false;
+            this.$.container.classList.remove('unableToSave');
             this._showEndRequest();
           } else {
+            this.$.container.classList.add('unableToSave');
+            this._unableToSave = true;
             this._handleFailedDraftRequest();
           }
           return result;
@@ -849,7 +869,7 @@
       return comment.robot_id;
     }
     if (comment.author) {
-      return GrDisplayNameUtils.getDisplayName(serverConfig, comment.author);
+      return getDisplayName(serverConfig, comment.author);
     }
     return '';
   }
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.ts
similarity index 95%
rename from polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
rename to polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index 1d04969..5087977 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -112,7 +112,7 @@
     .draft .draftTooltip {
       display: inline;
     }
-    .draft:not(.editing) .save,
+    .draft:not(.editing):not(.unableToSave) .save,
     .draft:not(.editing) .cancel {
       display: none;
     }
@@ -242,14 +242,15 @@
         <span class="authorName">
           [[_computeAuthorName(comment, _serverConfig)]]
         </span>
-        <span class="draftLabel">DRAFT</span>
         <gr-tooltip-content
           class="draftTooltip"
           has-tooltip=""
-          title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
+          title="[[_computeDraftTooltip(_unableToSave)]]"
           max-width="20em"
           show-icon=""
-        ></gr-tooltip-content>
+        >
+          <span class="draftLabel">[[_computeDraftText(_unableToSave)]]</span>
+        </gr-tooltip-content>
       </div>
       <div class="headerMiddle">
         <span class="collapsedContent">[[comment.message]]</span>
@@ -277,12 +278,14 @@
         <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>
+      <template is="dom-if" if="[[comment.updated]]">
+        <span class="date" tabindex="0" on-click="_handleAnchorClick">
+          <gr-date-formatter
+            has-tooltip=""
+            date-str="[[comment.updated]]"
+          ></gr-date-formatter>
+        </span>
+      </template>
       <div class="show-hide" tabindex="0">
         <label
           class="show-hide"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index b227383..16664cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-comment.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {__testOnly_UNSAVED_MESSAGE} from './gr-comment.js';
 
 const basicFixture = fixtureFromElement('gr-comment');
 
@@ -97,6 +98,7 @@
       element.side = 'PARENT';
       const stub = sinon.stub();
       element.addEventListener('comment-anchor-tap', stub);
+      flushAsynchronousOperations();
       const dateEl = element.shadowRoot
           .querySelector('.date');
       assert.ok(dateEl);
@@ -353,6 +355,40 @@
           .querySelector('.discard'));
       assert.isTrue(reportStub.calledOnce);
     });
+
+    test('failed save draft request', done => {
+      element.draft = true;
+      const updateRequestStub = sinon.stub(element, '_updateRequestToast');
+      const diffDraftStub =
+        sinon.stub(element.$.restAPI, 'saveDiffDraft').returns(
+            Promise.resolve({ok: false}));
+      element._saveDraft();
+      flush(() => {
+        let args = updateRequestStub.lastCall.args;
+        assert.deepEqual(args, [0, true]);
+        assert.equal(element._getSavingMessage(...args),
+            __testOnly_UNSAVED_MESSAGE);
+        assert.equal(element.shadowRoot.querySelector('.draftLabel').innerText,
+            'DRAFT(Failed to save)');
+        assert.isTrue(isVisible(element.shadowRoot
+            .querySelector('.save')), 'save is visible');
+        diffDraftStub.returns(
+            Promise.resolve({ok: true}));
+        element._saveDraft();
+        flush(() => {
+          args = updateRequestStub.lastCall.args;
+          assert.deepEqual(args, [0]);
+          assert.equal(element._getSavingMessage(...args),
+              'All changes saved');
+          assert.equal(element.shadowRoot.querySelector('.draftLabel')
+              .innerText, 'DRAFT');
+          assert.isFalse(isVisible(element.shadowRoot
+              .querySelector('.save')), 'save is not visible');
+          assert.isFalse(element._unableToSave);
+          done();
+        });
+      });
+    });
   });
 
   suite('gr-comment draft tests', () => {
@@ -589,13 +625,11 @@
     });
 
     test('robot comment layout', done => {
-      const comment = Object.assign({
-        robot_id: 'happy_robot_id',
+      const comment = {robot_id: 'happy_robot_id',
         url: '/robot/comment',
         author: {
           name: 'Happy Robot',
-        },
-      }, element.comment);
+        }, ...element.comment};
       element.comment = comment;
       element.collapsed = false;
       flush(() => {
@@ -626,12 +660,10 @@
     });
 
     test('author name fallback to email', done => {
-      const comment = Object.assign({
-        url: '/robot/comment',
+      const comment = {url: '/robot/comment',
         author: {
           email: 'test@test.com',
-        },
-      }, element.comment);
+        }, ...element.comment};
       element.comment = comment;
       element.collapsed = false;
       flush(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
rename to polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts
index 2d0fa6f..6876c1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
rename to polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.ts
index 9fc83c8..197c94c 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
rename to polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.ts
index 3ed33d1..1489006 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html``;
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 40aa808..682b7f7 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
@@ -16,12 +16,11 @@
  */
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-date-formatter_html.js';
-import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin.js';
 import {parseDate, fromNow, isValidDate, isWithinDay, isWithinHalfYear, formatDate, utcOffsetString} from '../../../utils/date-util.js';
 
 const TimeFormats = {
@@ -57,11 +56,9 @@
 /**
  * @extends PolymerElement
  */
-class GrDateFormatter extends mixinBehaviors( [
-  TooltipBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrDateFormatter extends TooltipMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-date-formatter'; }
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
rename to polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
index 2571065..a5dd6d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
rename to polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
index 54b404d..f8ddcfd 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
rename to polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts
index 3ea40d9..f9d971d 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 7b92e3d..e3ae8e2 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
@@ -18,21 +18,16 @@
 import '../gr-shell-command/gr-shell-command.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';
 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-download-commands_html.js';
-import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
  * @extends PolymerElement
  */
-class GrDownloadCommands extends mixinBehaviors( [
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrDownloadCommands extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-download-commands'; }
@@ -98,6 +93,11 @@
   _computeShowTabs(schemes) {
     return schemes.length > 1 ? '' : 'hidden';
   }
+
+  _computeClass(title) {
+    // Only retain [a-z] chars, so "Cherry Pick" becomes "cherrypick".
+    return '_label_' + title.replace(/[^a-z]+/gi, '').toLowerCase();
+  }
 }
 
 customElements.define(GrDownloadCommands.is, GrDownloadCommands);
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
rename to polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
index 7248e65..35385fa 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
@@ -66,6 +66,7 @@
   <div class="commands" hidden$="[[!schemes.length]]" hidden="">
     <template is="dom-repeat" items="[[commands]]" as="command">
       <gr-shell-command
+        class$="[[_computeClass(command.title)]]"
         label="[[command.title]]"
         command="[[command.command]]"
       ></gr-shell-command>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
similarity index 98%
rename from polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
rename to polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
index 0f80af2..629b0ad 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 0828bf6..0f3d566 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -21,13 +21,12 @@
 import '../gr-tooltip-content/gr-tooltip-content.js';
 import '../../../styles/shared-styles.js';
 import {flush, dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-dropdown_html.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const REL_NOOPENER = 'noopener';
 const REL_EXTERNAL = 'external';
@@ -35,11 +34,8 @@
 /**
  * @extends PolymerElement
  */
-class GrDropdown extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrDropdown extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-dropdown'; }
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.ts
similarity index 98%
rename from polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
rename to polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
index 6617a0e..8ea0d21 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
rename to polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index 24eb0b0..81a2c2f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 6c3c72f..a323528 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
@@ -20,12 +20,11 @@
 import '../../../styles/shared-styles.js';
 import '../gr-button/gr-button.js';
 import {dom} 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-editable-label_html.js';
-import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 const AWAIT_MAX_ITERS = 10;
 const AWAIT_STEP = 5;
@@ -33,11 +32,8 @@
 /**
  * @extends PolymerElement
  */
-class GrEditableLabel extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrEditableLabel extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-editable-label'; }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
rename to polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
index a226e30..5e36166 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
rename to polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.ts
index 61e8b24..ce475c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 039b95d..862c48c 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
@@ -83,7 +83,7 @@
 
     // Add new content.
     for (const node of this._computeNodes(this._computeBlocks(content))) {
-      container.appendChild(node);
+      if (node) container.appendChild(node);
     }
   }
 
@@ -276,7 +276,7 @@
       if (block.type === 'quote') {
         const bq = document.createElement('blockquote');
         for (const node of this._computeNodes(block.blocks)) {
-          bq.appendChild(node);
+          if (node) bq.appendChild(node);
         }
         return bq;
       }
@@ -300,6 +300,9 @@
         }
         return ul;
       }
+
+      console.warn('Unrecognized type.');
+      return;
     });
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
rename to polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts
index 5cb8670..468bbee 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 717a28e..a92ae4d 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
@@ -21,7 +21,6 @@
 import '../gr-button/gr-button.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import {hovercardBehaviorMixin} from '../gr-hovercard/gr-hovercard-behavior.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.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';
@@ -104,6 +103,20 @@
     return this.change.attention_set.hasOwnProperty(this.account._account_id);
   }
 
+  _computeReason(change) {
+    if (!change || !change.attention_set) return '';
+    const entry = change.attention_set[this.account._account_id];
+    if (!entry || !entry.reason) return '';
+    return entry.reason;
+  }
+
+  _computeLastUpdate(change) {
+    if (!change || !change.attention_set) return '';
+    const entry = change.attention_set[this.account._account_id];
+    if (!entry || !entry.last_update) return '';
+    return entry.last_update;
+  }
+
   _computeShowLabelNeedsAttention(config, highlightAttention, account, change) {
     return this.isAttentionSetEnabled && this.hasAttention;
   }
@@ -130,7 +143,8 @@
         this._reportingDetails());
     this.$.restAPI.addToAttentionSet(this.change._number,
         this.account._account_id, 'manually added').then(obj => {
-      GerritNav.navigateToChange(this.change);
+      this.dispatchEventThroughTarget('hide-alert');
+      this.dispatchEventThroughTarget('reload');
     });
     this.hide();
   }
@@ -148,7 +162,8 @@
         this._reportingDetails());
     this.$.restAPI.removeFromAttentionSet(this.change._number,
         this.account._account_id, 'manually removed').then(obj => {
-      GerritNav.navigateToChange(this.change);
+      this.dispatchEventThroughTarget('hide-alert');
+      this.dispatchEventThroughTarget('reload');
     });
     this.hide();
   }
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.ts
similarity index 82%
rename from polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
rename to polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.ts
index 262b089..0075881 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.ts
@@ -14,8 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../gr-hovercard/gr-hovercard-shared-style.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import '../gr-hovercard/gr-hovercard-shared-style';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-hovercard-shared-style">
@@ -63,6 +63,9 @@
       position: relative;
       top: 3px;
     }
+    .reason {
+      padding-top: var(--spacing-s);
+    }
   </style>
   <div id="container" role="tooltip" tabindex="-1">
     <template is="dom-if" if="[[_isShowing]]">
@@ -95,10 +98,20 @@
         if="[[_computeShowLabelNeedsAttention(_config, highlightAttention, account, change)]]"
       >
         <div class="attention">
-          <iron-icon icon="gr-icons:attention"></iron-icon>
-          <span>
-            [[_computeText(account, _selfAccount)]] turn to take action.
-          </span>
+          <div>
+            <iron-icon icon="gr-icons:attention"></iron-icon>
+            <span>
+              [[_computeText(account, _selfAccount)]] turn to take action.
+            </span>
+          </div>
+          <div class="reason">
+            <span class="title">Reason:</span>
+            <span class="value">[[_computeReason(change)]]</span>,
+            <gr-date-formatter
+              has-tooltip
+              date-str="[[_computeLastUpdate(change)]]"
+            ></gr-date-formatter>
+          </div>
         </div>
       </template>
       <template
@@ -107,6 +120,7 @@
       >
         <div class="action">
           <gr-button
+            class="addToAttentionSet"
             link=""
             no-uppercase=""
             on-click="_handleClickAddToAttentionSet"
@@ -121,6 +135,7 @@
       >
         <div class="action">
           <gr-button
+            class="removeFromAttentionSet"
             link=""
             no-uppercase=""
             on-click="_handleClickRemoveFromAttentionSet"
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
index ea7eb87..6e611d5 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -39,7 +39,13 @@
         new Promise(resolve => { '2'; })
     );
 
-    element.account = Object.assign({}, ACCOUNT);
+    element.account = {...ACCOUNT};
+    element._config = {
+      change: {enable_attention_set: true},
+    };
+    element.change = {
+      attention_set: {},
+    };
     element.show({});
     flushAsynchronousOperations();
   });
@@ -53,6 +59,29 @@
         'Kermit The Frog');
   });
 
+  test('_computeReason', () => {
+    const change = {
+      attention_set: {
+        31415926535: {
+          reason: 'a good reason',
+        },
+      },
+    };
+    assert.equal(element._computeReason(change), 'a good reason');
+  });
+
+  test('_computeLastUpdate', () => {
+    const last_update = '2019-07-17 19:39:02.000000000';
+    const change = {
+      attention_set: {
+        31415926535: {
+          last_update,
+        },
+      },
+    };
+    assert.equal(element._computeLastUpdate(change), last_update);
+  });
+
   test('_computeText', () => {
     let account = {_account_id: '1'};
     const selfAccount = {_account_id: '1'};
@@ -66,7 +95,7 @@
   });
 
   test('account status is displayed', () => {
-    element.account = Object.assign({status: 'OOO'}, ACCOUNT);
+    element.account = {status: 'OOO', ...ACCOUNT};
     flushAsynchronousOperations();
     assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
         'OOO');
@@ -82,5 +111,72 @@
     assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
         element.voteableText);
   });
+
+  test('add to attention set', done => {
+    let apiResolve;
+    const apiPromise = new Promise(r => {
+      apiResolve = r;
+    });
+    sinon.stub(element.$.restAPI, 'addToAttentionSet')
+        .callsFake(() => apiPromise);
+    element.highlightAttention = true;
+    element._target = document.createElement('div');
+    flushAsynchronousOperations();
+    const showAlertListener = sinon.spy();
+    const hideAlertListener = sinon.spy();
+    const reloadListener = sinon.spy();
+    element.addEventListener('show-alert', showAlertListener);
+    element._target.addEventListener('hide-alert', hideAlertListener);
+    element._target.addEventListener('reload', reloadListener);
+
+    const button = element.shadowRoot.querySelector('.addToAttentionSet');
+    assert.isOk(button);
+    assert.isTrue(element._isShowing, 'hovercard is showing');
+    MockInteractions.tap(button);
+
+    assert.isTrue(showAlertListener.called, 'showAlertListener was called');
+    assert.isFalse(element._isShowing, 'hovercard is hidden');
+
+    apiResolve({});
+    flush(() => {
+      assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+      assert.isTrue(reloadListener.called, 'reloadListener was called');
+      done();
+    });
+  });
+
+  test('remove from attention set', done => {
+    let apiResolve;
+    const apiPromise = new Promise(r => {
+      apiResolve = r;
+    });
+    sinon.stub(element.$.restAPI, 'removeFromAttentionSet')
+        .callsFake(() => apiPromise);
+    element.highlightAttention = true;
+    element.change = {attention_set: {31415926535: {}}};
+    element._target = document.createElement('div');
+    flushAsynchronousOperations();
+    const showAlertListener = sinon.spy();
+    const hideAlertListener = sinon.spy();
+    const reloadListener = sinon.spy();
+    element.addEventListener('show-alert', showAlertListener);
+    element._target.addEventListener('hide-alert', hideAlertListener);
+    element._target.addEventListener('reload', reloadListener);
+
+    const button = element.shadowRoot.querySelector('.removeFromAttentionSet');
+    assert.isOk(button);
+    assert.isTrue(element._isShowing, 'hovercard is showing');
+    MockInteractions.tap(button);
+
+    assert.isTrue(showAlertListener.called, 'showAlertListener was called');
+    assert.isFalse(element._isShowing, 'hovercard is hidden');
+
+    apiResolve({});
+    flush(() => {
+      assert.isTrue(hideAlertListener.called, 'hideAlertListener was called');
+      assert.isTrue(reloadListener.called, 'reloadListener was called');
+      done();
+    });
+  });
 });
 
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 0d351f6..04c3166 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
@@ -161,6 +161,17 @@
   }
 
   /**
+   * Hovercard elements are created outside of <gr-app>, so if you want to fire
+   * events, then you probably want to do that through the target element.
+   */
+  dispatchEventThroughTarget(eventName) {
+    this._target.dispatchEvent(new CustomEvent(eventName, {
+      bubbles: true,
+      composed: true,
+    }));
+  }
+
+  /**
    * Returns the target element that the hovercard is anchored to (the `id` of
    * the `for` property).
    *
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
similarity index 84%
rename from polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
rename to polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
index 5fb1add..4133fd1 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-shared-style.ts
@@ -15,10 +15,14 @@
  * limitations under the License.
  */
 
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 /** The shared styles for all hover cards. */
 const GrHoverCardSharedStyle = document.createElement('dom-module');
-GrHoverCardSharedStyle.innerHTML =
-  `<template>
+GrHoverCardSharedStyle.innerHTML = `<template>
     <style include="shared-styles">
       :host {
         position: absolute;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
similarity index 92%
rename from polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
rename to polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
index 67a3545..830cbd878 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-hovercard-shared-style">
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
similarity index 99%
rename from polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
rename to polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
index 5ffe028..ccaf40e 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -14,8 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-icon/iron-icon.js';
-import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
+import '@polymer/iron-icon/iron-icon';
+import '@polymer/iron-iconset-svg/iron-iconset-svg';
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index da0157f..2fa9acc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -19,14 +19,14 @@
  * GrChangeReplyInterface, provides a set of handy methods on reply dialog.
  */
 export class GrChangeReplyInterface {
-  constructor(plugin) {
+  constructor(plugin, sharedApiElement) {
     this.plugin = plugin;
-    this._sharedApiEl = Plugin._sharedAPIElement;
+    this.sharedApiElement = sharedApiElement;
   }
 
   get _el() {
-    return this._sharedApiEl.getElement(
-        this._sharedApiEl.Element.REPLY_DIALOG);
+    return this.sharedApiElement.getElement(
+        this.sharedApiElement.Element.REPLY_DIALOG);
   }
 
   getLabelValue(label) {
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 43d7378..6c785d4 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,12 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-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 {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {pluginLoader} from './gr-plugin-loader.js';
+import {patchNumEquals} from '../../../utils/patch-set-util.js';
 
 // Note: for new events, naming convention should be: `a-b`
 const EventType = {
@@ -46,11 +45,9 @@
 /**
  * @extends PolymerElement
  */
-class GrJsApiInterface extends mixinBehaviors( [
-  PatchSetBehavior,
-], GestureEventListeners(
+class GrJsApiInterface extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get is() { return 'gr-js-api-interface'; }
 
   constructor() {
@@ -158,19 +155,17 @@
     //
     // assign on getter with existing property will report error
     // see Issue: 12286
-    const change = Object.assign({}, detail.change, {
-      get mergeable() {
-        console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
+    const change = {...detail.change, get mergeable() {
+      console.warn('Accessing change.mergeable from SHOW_CHANGE is ' +
             'deprecated! Use info.mergeable instead.');
-        return detail.info && detail.info.mergeable;
-      },
-    });
+      return detail.info && detail.info.mergeable;
+    }};
     const patchNum = detail.patchNum;
     const info = detail.info;
 
     let revision;
     for (const rev of Object.values(change.revisions || {})) {
-      if (this.patchNumEquals(rev._number, patchNum)) {
+      if (patchNumEquals(rev._number, patchNum)) {
         revision = rev;
         break;
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 946325f..2a11f62 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -158,8 +158,11 @@
     const response = {status: 204};
     sendStub.returns(Promise.resolve(response));
     return plugin.delete('/url', r => {
-      assert.isTrue(sendStub.calledWithExactly(
-          'DELETE', 'http://test.com/plugins/testplugin/url'));
+      assert.equal(sendStub.lastCall.args[0], 'DELETE');
+      assert.equal(
+          sendStub.lastCall.args[1],
+          'http://test.com/plugins/testplugin/url'
+      );
       assert.strictEqual(r, response);
     });
   });
@@ -170,8 +173,11 @@
     return plugin.delete('/url', r => {
       throw new Error('Should not resolve');
     }).catch(err => {
-      assert.isTrue(sendStub.calledWith(
-          'DELETE', 'http://test.com/plugins/testplugin/url'));
+      assert.equal(sendStub.lastCall.args[0], 'DELETE');
+      assert.equal(
+          sendStub.lastCall.args[1],
+          'http://test.com/plugins/testplugin/url'
+      );
       assert.equal('text', err.message);
     });
   });
@@ -192,7 +198,7 @@
       _number: 42,
       revisions: {def: {_number: 2}, abc: {_number: 1}},
     };
-    const expectedChange = Object.assign({mergeable: false}, testChange);
+    const expectedChange = {mergeable: false, ...testChange};
     plugin.on(element.EventType.SHOW_CHANGE, throwErrFn);
     plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => {
       assert.deepEqual(change, expectedChange);
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 2c97df0..dae8d3e 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
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import {pluginLoader} from './gr-plugin-loader.js';
 import {importHref} from '../../../scripts/import-href.js';
 
 /** @constructor */
@@ -25,6 +24,11 @@
     this._callbacks = {};
     this._dynamicPlugins = {};
     this._importedUrls = new Set();
+    this._pluginLoaded = false;
+  }
+
+  setPluginsReady() {
+    this._pluginLoaded = true;
   }
 
   onNewEndpoint(endpoint, callback) {
@@ -89,7 +93,12 @@
       this._endpoints[endpoint] = [];
     }
     const moduleInfo = this._getOrCreateModuleInfo(plugin, opts);
-    if (pluginLoader.arePluginsLoaded() && this._callbacks[endpoint]) {
+    // TODO: the logic below seems wrong when:
+    // multiple plugins register to the same endpoint
+    // one register before plugins ready
+    // the other done after, then only the later one will have the callbacks
+    // invoked.
+    if (this._pluginLoaded && 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.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
index f1af433..5b931b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.js
@@ -19,7 +19,6 @@
 import {resetPlugins} from '../../../test/test-utils.js';
 import './gr-js-api-interface.js';
 import {GrPluginEndpoints} from './gr-plugin-endpoints.js';
-import {pluginLoader} from './gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 
 const pluginApi = _testOnly_initGerritPluginApi();
@@ -55,7 +54,6 @@
           domHook,
         }
     );
-    sinon.stub(pluginLoader, 'arePluginsLoaded').returns(true);
     sinon.spy(instance, 'importUrl');
   });
 
@@ -132,6 +130,7 @@
 
   test('onNewEndpoint', () => {
     const newModuleStub = sinon.stub();
+    instance.setPluginsReady();
     instance.onNewEndpoint('a-place', newModuleStub);
     instance.registerModule(
         pluginFoo,
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 0bf49c3..37aecf9 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
@@ -16,16 +16,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import './gr-api-utils.js';
 import {importHref} from '../../../scripts/import-href.js';
-
 import {
   PLUGIN_LOADING_TIMEOUT_MS,
   PRELOADED_PROTOCOL,
   getPluginNameFromUrl,
 } from './gr-api-utils.js';
-
+import {Plugin} from './gr-public-js-api.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
+import {pluginEndpoints} from './gr-plugin-endpoints.js';
 
 /**
  * @enum {string}
@@ -210,10 +209,13 @@
   }
 
   _checkIfCompleted() {
-    if (this.arePluginsLoaded() && this._loadingResolver) {
-      this._loadingResolver();
-      this._loadingResolver = null;
-      this._loadingPromise = null;
+    if (this.arePluginsLoaded()) {
+      pluginEndpoints.setPluginsReady();
+      if (this._loadingResolver) {
+        this._loadingResolver();
+        this._loadingResolver = null;
+        this._loadingPromise = null;
+      }
     }
   }
 
@@ -261,7 +263,7 @@
     const pluginObj = this._updatePluginState(url, PluginState.LOADED);
     pluginObj.plugin = plugin;
     this._getReporting().pluginLoaded(plugin.getPluginName() || url);
-    console.log(`Plugin ${plugin.getPluginName() || url} installed.`);
+    console.info(`Plugin ${plugin.getPluginName() || url} installed.`);
     this._checkIfCompleted();
   }
 
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 446ceb5..3611e8a 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
@@ -16,6 +16,7 @@
  */
 
 import {getBaseUrl} from '../../../utils/url-util.js';
+import {getSharedApiEl} from '../../../utils/dom-util.js';
 import {GrAttributeHelper} from '../../plugins/gr-attribute-helper/gr-attribute-helper.js';
 import {GrChangeActionsInterface} from './gr-change-actions-js-api.js';
 import {GrChangeReplyInterface} from './gr-change-reply-js-api.js';
@@ -33,43 +34,42 @@
 import {GrPluginActionContext} from './gr-plugin-action-context.js';
 import {pluginEndpoints} from './gr-plugin-endpoints.js';
 
-import {PRELOADED_PROTOCOL, getPluginNameFromUrl, send} from './gr-api-utils.js';
-import {deprecatedDelete} from './gr-gerrit.js';
+import {
+  PRELOADED_PROTOCOL,
+  getPluginNameFromUrl,
+  send,
+} from './gr-api-utils.js';
 
-(function(window) {
-  'use strict';
+const PANEL_ENDPOINTS_MAPPING = {
+  CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
+  CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
+};
 
-  const PANEL_ENDPOINTS_MAPPING = {
-    CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
-    CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
-  };
+/**
+ * Plugin-provided custom components can affect content in extension
+ * points using one of following methods:
+ * - DECORATE: custom component is set with `content` attribute and may
+ *   decorate (e.g. style) DOM element.
+ * - REPLACE: contents of extension point are replaced with the custom
+ *   component.
+ * - STYLE: custom component is a shared styles module that is inserted
+ *   into the extension point.
+ */
+const EndpointType = {
+  DECORATE: 'decorate',
+  REPLACE: 'replace',
+  STYLE: 'style',
+};
 
-  /**
-   * Plugin-provided custom components can affect content in extension
-   * points using one of following methods:
-   * - DECORATE: custom component is set with `content` attribute and may
-   *   decorate (e.g. style) DOM element.
-   * - REPLACE: contents of extension point are replaced with the custom
-   *   component.
-   * - STYLE: custom component is a shared styles module that is inserted
-   *   into the extension point.
-   */
-  const EndpointType = {
-    DECORATE: 'decorate',
-    REPLACE: 'replace',
-    STYLE: 'style',
-  };
-
-  /**
-   * @constructor
-   * @param {string=} opt_url
-   */
-  function Plugin(opt_url) {
+export class Plugin {
+  constructor(opt_url) {
     this._domHooks = new GrDomHooksManager(this);
 
     if (!opt_url) {
-      console.warn('Plugin not being loaded from /plugins base path.',
-          'Unable to determine name.');
+      console.warn(
+          'Plugin not being loaded from /plugins base path.',
+          'Unable to determine name.'
+      );
       return this;
     }
     this.deprecated = {
@@ -84,29 +84,31 @@
 
     this._url = new URL(opt_url);
     this._name = getPluginNameFromUrl(this._url);
+    this.sharedApiElement = getSharedApiEl();
   }
 
-  Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
-
-  Plugin.prototype._name = '';
-
-  Plugin.prototype.getPluginName = function() {
+  getPluginName() {
     return this._name;
-  };
+  }
 
-  Plugin.prototype.registerStyleModule = function(endpoint, moduleName) {
-    pluginEndpoints.registerModule(
-        this, {endpoint, type: EndpointType.STYLE, moduleName});
-  };
+  registerStyleModule(endpoint, moduleName) {
+    pluginEndpoints.registerModule(this, {
+      endpoint,
+      type: EndpointType.STYLE,
+      moduleName,
+    });
+  }
 
   /**
    * Registers an endpoint for the plugin.
    */
-  Plugin.prototype.registerCustomComponent = function(
-      endpointName, opt_moduleName, opt_options) {
-    return this._registerCustomComponent(endpointName, opt_moduleName,
-        opt_options);
-  };
+  registerCustomComponent(endpointName, opt_moduleName, opt_options) {
+    return this._registerCustomComponent(
+        endpointName,
+        opt_moduleName,
+        opt_options
+    );
+  }
 
   /**
    * Registers a dynamic endpoint for the plugin.
@@ -114,125 +116,151 @@
    * Dynamic plugins are registered by specific prefix, such as
    * 'change-list-header'.
    */
-  Plugin.prototype.registerDynamicCustomComponent = function(
-      endpointName, opt_moduleName, opt_options) {
+  registerDynamicCustomComponent(endpointName, opt_moduleName, opt_options) {
     const fullEndpointName = `${endpointName}-${this.getPluginName()}`;
-    return this._registerCustomComponent(fullEndpointName, opt_moduleName,
-        opt_options, endpointName);
-  };
+    return this._registerCustomComponent(
+        fullEndpointName,
+        opt_moduleName,
+        opt_options,
+        endpointName
+    );
+  }
 
-  Plugin.prototype._registerCustomComponent = function(
-      endpoint, opt_moduleName, opt_options, dynamicEndpoint) {
-    const type = opt_options && opt_options.replace ?
-      EndpointType.REPLACE : EndpointType.DECORATE;
-    const slot = opt_options && opt_options.slot || '';
+  _registerCustomComponent(
+      endpoint,
+      opt_moduleName,
+      opt_options,
+      dynamicEndpoint
+  ) {
+    const type =
+      opt_options && opt_options.replace
+        ? EndpointType.REPLACE
+        : EndpointType.DECORATE;
+    const slot = (opt_options && opt_options.slot) || '';
     const domHook = this._domHooks.getDomHook(endpoint, opt_moduleName);
     const moduleName = opt_moduleName || domHook.getModuleName();
-    pluginEndpoints.registerModule(
-        this, {slot, endpoint, type, moduleName, domHook, dynamicEndpoint});
+    pluginEndpoints.registerModule(this, {
+      slot,
+      endpoint,
+      type,
+      moduleName,
+      domHook,
+      dynamicEndpoint,
+    });
     return domHook.getPublicAPI();
-  };
+  }
 
   /**
    * Returns instance of DOM hook API for endpoint. Creates a placeholder
    * element for the first call.
    */
-  Plugin.prototype.hook = function(endpointName, opt_options) {
+  hook(endpointName, opt_options) {
     return this.registerCustomComponent(endpointName, undefined, opt_options);
-  };
+  }
 
-  Plugin.prototype.getServerInfo = function() {
+  getServerInfo() {
     return document.createElement('gr-rest-api-interface').getConfig();
-  };
+  }
 
-  Plugin.prototype.on = function(eventName, callback) {
-    Plugin._sharedAPIElement.addEventCallback(eventName, callback);
-  };
+  on(eventName, callback) {
+    this.sharedApiElement.addEventCallback(eventName, callback);
+  }
 
-  Plugin.prototype.url = function(opt_path) {
+  url(opt_path) {
     const relPath = '/plugins/' + this._name + (opt_path || '/');
-    const sameOriginPath = window.location.origin +
-      `${getBaseUrl()}${relPath}`;
+    const sameOriginPath = window.location.origin + `${getBaseUrl()}${relPath}`;
     if (window.location.origin === this._url.origin) {
       // Plugin loaded from the same origin as gr-app, getBaseUrl in effect.
       return sameOriginPath;
     } else if (this._url.protocol === PRELOADED_PROTOCOL) {
       // Plugin is preloaded, load plugin with ASSETS_PATH or location.origin
-      return window.ASSETS_PATH ? `${window.ASSETS_PATH}${relPath}` :
-        sameOriginPath;
+      return window.ASSETS_PATH
+        ? `${window.ASSETS_PATH}${relPath}`
+        : sameOriginPath;
     } else {
       // Plugin loaded from assets bundle, expect assets placed along with it.
       return this._url.href.split('/plugins/' + this._name)[0] + relPath;
     }
-  };
+  }
 
-  Plugin.prototype.screenUrl = function(opt_screenName) {
+  screenUrl(opt_screenName) {
     const origin = location.origin;
     const base = getBaseUrl();
     const tokenPart = opt_screenName ? '/' + opt_screenName : '';
     return `${origin}${base}/x/${this.getPluginName()}${tokenPart}`;
-  };
+  }
 
-  Plugin.prototype._send = function(method, url, opt_callback, opt_payload) {
+  _send(method, url, opt_callback, opt_payload) {
     return send(method, this.url(url), opt_callback, opt_payload);
-  };
+  }
 
-  Plugin.prototype.get = function(url, opt_callback) {
+  get(url, opt_callback) {
     console.warn('.get() is deprecated! Use .restApi().get()');
     return this._send('GET', url, opt_callback);
-  };
+  }
 
-  Plugin.prototype.post = function(url, payload, opt_callback) {
+  post(url, payload, opt_callback) {
     console.warn('.post() is deprecated! Use .restApi().post()');
     return this._send('POST', url, opt_callback, payload);
-  };
+  }
 
-  Plugin.prototype.put = function(url, payload, opt_callback) {
+  put(url, payload, opt_callback) {
     console.warn('.put() is deprecated! Use .restApi().put()');
     return this._send('PUT', url, opt_callback, payload);
-  };
+  }
 
-  Plugin.prototype.delete = function(url, opt_callback) {
-    return deprecatedDelete(this.url(url), opt_callback);
-  };
+  delete(url, opt_callback) {
+    console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
+    return this.restApi()
+        .delete(this.url(url))
+        .then(res => {
+          if (opt_callback) {
+            opt_callback(res);
+          }
+          return res;
+        });
+  }
 
-  Plugin.prototype.annotationApi = function() {
+  annotationApi() {
     return new GrAnnotationActionsInterface(this);
-  };
+  }
 
-  Plugin.prototype.changeActions = function() {
-    return new GrChangeActionsInterface(this,
-        Plugin._sharedAPIElement.getElement(
-            Plugin._sharedAPIElement.Element.CHANGE_ACTIONS));
-  };
+  changeActions() {
+    return new GrChangeActionsInterface(
+        this,
+        this.sharedApiElement.getElement(
+            this.sharedApiElement.Element.CHANGE_ACTIONS
+        )
+    );
+  }
 
-  Plugin.prototype.changeReply = function() {
-    return new GrChangeReplyInterface(this);
-  };
+  changeReply() {
+    return new GrChangeReplyInterface(this, this.sharedApiElement);
+  }
 
-  Plugin.prototype.theme = function() {
+  theme() {
     return new GrThemeApi(this);
-  };
+  }
 
-  Plugin.prototype.project = function() {
+  project() {
     return new GrRepoApi(this);
-  };
+  }
 
-  Plugin.prototype.changeMetadata = function() {
+  changeMetadata() {
     return new GrChangeMetadataApi(this);
-  };
+  }
 
-  Plugin.prototype.admin = function() {
+  admin() {
     return new GrAdminApi(this);
-  };
+  }
 
-  Plugin.prototype.settings = function() {
+  settings() {
     return new GrSettingsApi(this);
-  };
+  }
 
-  Plugin.prototype.styles = function() {
+  styles() {
     return new GrStylesApi();
-  };
+  }
 
   /**
    * To make REST requests for plugin-provided endpoints, use
@@ -242,158 +270,172 @@
    *
    * @param {string=} opt_prefix url for subsequent .get(), .post() etc requests.
    */
-  Plugin.prototype.restApi = function(opt_prefix) {
+  restApi(opt_prefix) {
     return new GrPluginRestApi(opt_prefix);
-  };
+  }
 
-  Plugin.prototype.attributeHelper = function(element) {
+  attributeHelper(element) {
     return new GrAttributeHelper(element);
-  };
+  }
 
-  Plugin.prototype.eventHelper = function(element) {
+  eventHelper(element) {
     return new GrEventHelper(element);
-  };
+  }
 
-  Plugin.prototype.popup = function(moduleName) {
+  popup(moduleName) {
     if (typeof moduleName !== 'string') {
       console.error('.popup(element) deprecated, use .popup(moduleName)!');
       return;
     }
     const api = new GrPopupInterface(this, moduleName);
     return api.open();
-  };
+  }
 
-  Plugin.prototype.panel = function() {
-    console.error('.panel() is deprecated! ' +
-        'Use registerCustomComponent() instead.');
-  };
+  panel() {
+    console.error(
+        '.panel() is deprecated! ' + 'Use registerCustomComponent() instead.'
+    );
+  }
 
-  Plugin.prototype.settingsScreen = function() {
-    console.error('.settingsScreen() is deprecated! ' +
-        'Use .settings() instead.');
-  };
+  settingsScreen() {
+    console.error(
+        '.settingsScreen() is deprecated! ' + 'Use .settings() instead.'
+    );
+  }
 
-  Plugin.prototype.screen = function(screenName, opt_moduleName) {
+  screen(screenName, opt_moduleName) {
     if (opt_moduleName && typeof opt_moduleName !== 'string') {
-      console.error('.screen(pattern, callback) deprecated, use ' +
-          '.screen(screenName, opt_moduleName)!');
+      console.error(
+          '.screen(pattern, callback) deprecated, use ' +
+          '.screen(screenName, opt_moduleName)!'
+      );
       return;
     }
     return this.registerCustomComponent(
         this._getScreenName(screenName),
-        opt_moduleName);
-  };
+        opt_moduleName
+    );
+  }
 
-  Plugin.prototype._getScreenName = function(screenName) {
+  _getScreenName(screenName) {
     return `${this.getPluginName()}-screen-${screenName}`;
-  };
+  }
+}
 
-  const deprecatedAPI = {
-    _loadedGwt: ()=> {},
+// TODO: should be removed soon after all core plugins moved away from it.
+const deprecatedAPI = {
+  _loadedGwt: () => {},
 
-    install() {
-      console.log('Installing deprecated APIs is deprecated!');
-      for (const method in this.deprecated) {
-        if (method === 'install') continue;
-        this[method] = this.deprecated[method];
-      }
-    },
+  install() {
+    console.info('Installing deprecated APIs is deprecated!');
+    for (const method in this.deprecated) {
+      if (method === 'install') continue;
+      this[method] = this.deprecated[method];
+    }
+  },
 
-    popup(el) {
-      console.warn('plugin.deprecated.popup() is deprecated, ' +
-          'use plugin.popup() insted!');
-      if (!el) {
-        throw new Error('Popup contents not found');
-      }
-      const api = new GrPopupInterface(this);
-      api.open().then(api => api._getElement().appendChild(el));
-      return api;
-    },
+  popup(el) {
+    console.warn(
+        'plugin.deprecated.popup() is deprecated, '
+        + 'use plugin.popup() insted!'
+    );
+    if (!el) {
+      throw new Error('Popup contents not found');
+    }
+    const api = new GrPopupInterface(this);
+    api.open().then(api => api._getElement().appendChild(el));
+    return api;
+  },
 
-    onAction(type, action, callback) {
-      console.warn('plugin.deprecated.onAction() is deprecated,' +
-          ' use plugin.changeActions() instead!');
-      if (type !== 'change' && type !== 'revision') {
-        console.warn(`${type} actions are not supported.`);
+  onAction(type, action, callback) {
+    console.warn(
+        'plugin.deprecated.onAction() is deprecated,' +
+        ' use plugin.changeActions() instead!'
+    );
+    if (type !== 'change' && type !== 'revision') {
+      console.warn(`${type} actions are not supported.`);
+      return;
+    }
+    this.on('showchange', (change, revision) => {
+      const details = this.changeActions().getActionDetails(action);
+      if (!details) {
+        console.warn(
+            `${this.getPluginName()} onAction error: ${action} not found!`
+        );
         return;
       }
-      this.on('showchange', (change, revision) => {
-        const details = this.changeActions().getActionDetails(action);
-        if (!details) {
-          console.warn(
-              `${this.getPluginName()} onAction error: ${action} not found!`);
-          return;
-        }
-        this.changeActions().addTapListener(details.__key, () => {
-          callback(new GrPluginActionContext(this, details, change, revision));
-        });
+      this.changeActions().addTapListener(details.__key, () => {
+        callback(new GrPluginActionContext(this, details, change, revision));
       });
-    },
+    });
+  },
 
-    screen(pattern, callback) {
-      console.warn('plugin.deprecated.screen is deprecated,' +
-          ' use plugin.screen instead!');
-      if (pattern instanceof RegExp) {
-        console.error('deprecated.screen() does not support RegExp. ' +
-            'Please use strings for patterns.');
-        return;
-      }
-      this.hook(this._getScreenName(pattern))
-          .onAttached(el => {
-            el.style.display = 'none';
-            callback({
-              body: el,
-              token: el.token,
-              onUnload: () => {},
-              setTitle: () => {},
-              setWindowTitle: () => {},
-              show: () => {
-                el.style.display = 'initial';
-              },
-            });
-          });
-    },
-
-    settingsScreen(path, menu, callback) {
-      console.warn('.settingsScreen() is deprecated! Use .settings() instead.');
-      const hook = this.settings()
-          .title(menu)
-          .token(path)
-          .module('div')
-          .build();
-      hook.onAttached(el => {
-        el.style.display = 'none';
-        const body = el.querySelector('div');
-        callback({
-          body,
-          onUnload: () => {},
-          setTitle: () => {},
-          setWindowTitle: () => {},
-          show: () => {
-            el.style.display = 'initial';
-          },
-        });
+  screen(pattern, callback) {
+    console.warn(
+        'plugin.deprecated.screen is deprecated,'
+        + ' use plugin.screen instead!'
+    );
+    if (pattern instanceof RegExp) {
+      console.error(
+          'deprecated.screen() does not support RegExp. ' +
+          'Please use strings for patterns.'
+      );
+      return;
+    }
+    this.hook(this._getScreenName(pattern)).onAttached(el => {
+      el.style.display = 'none';
+      callback({
+        body: el,
+        token: el.token,
+        onUnload: () => {},
+        setTitle: () => {},
+        setWindowTitle: () => {},
+        show: () => {
+          el.style.display = 'initial';
+        },
       });
-    },
+    });
+  },
 
-    panel(extensionpoint, callback) {
-      console.warn('.panel() is deprecated! ' +
-          'Use registerCustomComponent() instead.');
-      const endpoint = PANEL_ENDPOINTS_MAPPING[extensionpoint];
-      if (!endpoint) {
-        console.warn(`.panel ${extensionpoint} not supported!`);
-        return;
-      }
-      this.hook(endpoint).onAttached(el => callback({
+  settingsScreen(path, menu, callback) {
+    console.warn('.settingsScreen() is deprecated! Use .settings() instead.');
+    const hook = this.settings().title(menu)
+        .token(path)
+        .module('div')
+        .build();
+    hook.onAttached(el => {
+      el.style.display = 'none';
+      const body = el.querySelector('div');
+      callback({
+        body,
+        onUnload: () => {},
+        setTitle: () => {},
+        setWindowTitle: () => {},
+        show: () => {
+          el.style.display = 'initial';
+        },
+      });
+    });
+  },
+
+  panel(extensionpoint, callback) {
+    console.warn(
+        '.panel() is deprecated! ' + 'Use registerCustomComponent() instead.'
+    );
+    const endpoint = PANEL_ENDPOINTS_MAPPING[extensionpoint];
+    if (!endpoint) {
+      console.warn(`.panel ${extensionpoint} not supported!`);
+      return;
+    }
+    this.hook(endpoint).onAttached(el =>
+      callback({
         body: el,
         p: {
           CHANGE_INFO: el.change,
           REVISION_INFO: el.revision,
         },
         onUnload: () => {},
-      }));
-    },
-  };
-
-  window.Plugin = Plugin;
-})(window);
+      })
+    );
+  },
+};
\ No newline at end of file
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
rename to polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index 1722d02..dbc3b5e 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.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="gr-voting-styles">
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 fa1f758..5df6b58 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -14,21 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-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 {htmlTemplate} from './gr-label_html.js';
-import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin.js';
 
 /**
  * @extends PolymerElement
  */
-class GrLabel extends mixinBehaviors( [
-  TooltipBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrLabel extends TooltipMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-label'; }
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
rename to polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
index c4310fc..94196df 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html` <slot></slot> `;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
rename to polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts
index 615a525..fa50624 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.ts
index 204aa87..f34f99e 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
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 1b5224f..803f802 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,12 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-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 {htmlTemplate} from './gr-limited-text_html.js';
-import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin.js';
 
 /**
  * The gr-limited-text element is for displaying text with a maximum length
@@ -29,11 +28,9 @@
  *
  * @extends PolymerElement
  */
-class GrLimitedText extends mixinBehaviors( [
-  TooltipBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrLimitedText extends TooltipMixin(
+    GestureEventListeners(
+        LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-limited-text'; }
@@ -41,7 +38,10 @@
   static get properties() {
     return {
     /** The un-truncated text to display. */
-      text: String,
+      text: {
+        type: String,
+        value: '',
+      },
 
       /** The maximum length for the text to display before truncating. */
       limit: {
@@ -49,7 +49,12 @@
         value: null,
       },
 
-      /** Boolean property used by TooltipBehavior. */
+      tooltip: {
+        type: String,
+        value: '',
+      },
+
+      /** Boolean property used by TooltipMixin. */
       hasTooltip: {
         type: Boolean,
         value: false,
@@ -63,20 +68,12 @@
         type: Boolean,
         value: false,
       },
-
-      /**
-       * The maximum number of characters to display in the tooltip.
-       */
-      tooltipLimit: {
-        type: Number,
-        value: 1024,
-      },
     };
   }
 
   static get observers() {
     return [
-      '_updateTitle(text, limit, tooltipLimit)',
+      '_updateTitle(text, tooltip, limit)',
     ];
   }
 
@@ -84,17 +81,22 @@
    * The text or limit have changed. Recompute whether a tooltip needs to be
    * enabled.
    */
-  _updateTitle(text, limit, tooltipLimit) {
+  _updateTitle(text, tooltip, limit) {
     // Polymer 2: check for undefined
-    if ([text, limit, tooltipLimit].some(arg => arg === undefined)) {
+    if ([text, limit, tooltip].includes(undefined)) {
       return;
     }
 
-    this.hasTooltip = !!limit && !!text && text.length > limit;
+    this.hasTooltip = !!tooltip || (!!limit && text.length > limit);
     if (this.hasTooltip && !this.disableTooltip) {
-      this.setAttribute('title', text.substr(0, tooltipLimit));
+      // Combine the text and title if over-length
+      if (limit && text.length > limit) {
+        this.title = `${text}${tooltip? ` (${tooltip})` : ''}`;
+      } else {
+        this.title = tooltip;
+      }
     } else {
-      this.removeAttribute('title');
+      this.title = '';
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
similarity index 91%
rename from polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
rename to polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
index 6bcce8c..b942d07 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.ts
@@ -14,6 +14,6 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html` [[_computeDisplayText(text, limit)]] `;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
index dda3324..6e27eeb 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.js
@@ -27,7 +27,7 @@
     element = basicFixture.instantiate();
   });
 
-  test('_updateTitle', () => {
+  test('tooltip without title input', () => {
     const updateSpy = sinon.spy(element, '_updateTitle');
     element.text = 'abc 123';
     flushAsynchronousOperations();
@@ -43,28 +43,44 @@
 
     element.limit = 3;
     flushAsynchronousOperations();
-    assert.isTrue(updateSpy.calledThrice);
+    assert.equal(updateSpy.callCount, 3);
     assert.equal(element.getAttribute('title'), 'abc 123');
+    assert.equal(element.title, 'abc 123');
     assert.isTrue(element.hasTooltip);
 
-    element.tooltipLimit = 3;
-    flushAsynchronousOperations();
-    assert.equal(element.getAttribute('title'), 'abc');
-
-    element.tooltipLimit = 1024;
     element.limit = 100;
     flushAsynchronousOperations();
-    assert.equal(updateSpy.callCount, 6);
-    assert.isNotOk(element.getAttribute('title'));
+    assert.equal(updateSpy.callCount, 4);
     assert.isFalse(element.hasTooltip);
 
     element.limit = null;
     flushAsynchronousOperations();
-    assert.equal(updateSpy.callCount, 7);
+    assert.equal(updateSpy.callCount, 5);
     assert.isNotOk(element.getAttribute('title'));
     assert.isFalse(element.hasTooltip);
   });
 
+  test('with tooltip input', () => {
+    const updateSpy = sinon.spy(element, '_updateTitle');
+    element.tooltip = 'abc 123';
+    flushAsynchronousOperations();
+    assert.isTrue(updateSpy.calledOnce);
+    assert.isTrue(element.hasTooltip);
+    assert.equal(element.getAttribute('title'), 'abc 123');
+    assert.equal(element.title, 'abc 123');
+
+    element.text = 'abc';
+    flushAsynchronousOperations();
+    assert.equal(element.getAttribute('title'), 'abc 123');
+    assert.isTrue(element.hasTooltip);
+
+    element.text = 'abcdef';
+    element.limit = 3;
+    flushAsynchronousOperations();
+    assert.equal(element.getAttribute('title'), 'abcdef (abc 123)');
+    assert.isTrue(element.hasTooltip);
+  });
+
   test('_computeDisplayText', () => {
     assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar');
     assert.equal(element._computeDisplayText('foo bar', 4), 'foo…');
@@ -77,7 +93,7 @@
     element.disableTooltip = true;
     element.limit = 10;
     flushAsynchronousOperations();
-    assert.equal(element.getAttribute('title'), null);
+    assert.equal(element.getAttribute('title'), '');
   });
 });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
rename to polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
index f1f5f46..a335db7 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
rename to polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
index 59bed1e..4bdc1ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 6e8b3a3..94fc135 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
@@ -18,13 +18,11 @@
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../gr-button/gr-button.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 {htmlTemplate} from './gr-list-view_html.js';
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {encodeURL, getBaseUrl} from '../../../utils/url-util.js';
 import page from 'page/page.mjs';
 
 const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
@@ -32,11 +30,9 @@
 /**
  * @extends PolymerElement
  */
-class GrListView extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrListView extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-list-view'; }
@@ -74,7 +70,7 @@
     this.debounce('reload', () => {
       if (filter) {
         return page.show(`${this.path}/q/filter:` +
-            this.encodeURL(filter, false));
+            encodeURL(filter, false));
       }
       page.show(this.path);
     }, REQUEST_DEBOUNCE_INTERVAL_MS);
@@ -92,7 +88,7 @@
     const newOffset = Math.max(0, offset + (itemsPerPage * direction));
     let href = getBaseUrl() + path;
     if (filter) {
-      href += '/q/filter:' + this.encodeURL(filter, false);
+      href += '/q/filter:' + encodeURL(filter, false);
     }
     if (newOffset > 0) {
       href += ',' + newOffset;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
rename to polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.ts
index ff73d4d8..75ee667 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 5437ca5..882c557 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,13 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {IronOverlayBehaviorImpl, IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
+import {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
 import '../../../styles/shared-styles.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 {htmlTemplate} from './gr-overlay_html.js';
+import {IronOverlayMixin} from '../../../mixins/iron-overlay-mixin/iron-overlay-mixin.js';
 
 const AWAIT_MAX_ITERS = 10;
 const AWAIT_STEP = 5;
@@ -29,11 +29,8 @@
 /**
  * @extends PolymerElement
  */
-class GrOverlay extends mixinBehaviors( [
-  IronOverlayBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrOverlay extends IronOverlayMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-overlay'; }
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
rename to polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.ts
index 7123adb..730eeac 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.ts
similarity index 94%
rename from polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
rename to polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.ts
index c5e9142..facc4f8 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 9439c08..746fcf8 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
@@ -19,12 +19,11 @@
 import '../gr-icons/gr-icons.js';
 import '../gr-labeled-autocomplete/gr-labeled-autocomplete.js';
 import '../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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-branch-picker_html.js';
-import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
+import {singleDecodeURL} from '../../../utils/url-util.js';
 
 const SUGGESTIONS_LIMIT = 15;
 const REF_PREFIX = 'refs/heads/';
@@ -32,11 +31,9 @@
 /**
  * @extends PolymerElement
  */
-class GrRepoBranchPicker extends mixinBehaviors( [
-  URLEncodingBehavior,
-], GestureEventListeners(
+class GrRepoBranchPicker extends GestureEventListeners(
     LegacyElementMixin(
-        PolymerElement))) {
+        PolymerElement)) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-repo-branch-picker'; }
@@ -100,7 +97,7 @@
     return res.map(repo => {
       return {
         name: repo.name,
-        value: this.singleDecodeURL(repo.id),
+        value: singleDecodeURL(repo.id),
       };
     });
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.ts
similarity index 95%
rename from polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
rename to polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.ts
index 0ce885a..934b3cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 a4adbac..b364caf 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
@@ -38,7 +38,7 @@
   if (!etag) {
     return opt_options;
   }
-  const options = Object.assign({}, opt_options);
+  const options = {...opt_options};
   options.headers = options.headers || new Headers();
   options.headers.set('If-None-Match', this._etags.get(url));
   return options;
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 60f1463..58d53bb 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
@@ -16,19 +16,22 @@
  */
 /* NB: Order is important, because of namespaced classes. */
 
-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 {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 {parseDate} from '../../../utils/date-util.js';
-import {authService} from './gr-auth.js';
 import {getBaseUrl} from '../../../utils/url-util.js';
+import {appContext} from '../../../services/app-context.js';
+import {
+  getParentIndex,
+  isMergeParent,
+  patchNumEquals,
+  SPECIAL_PATCH_SET_NUM,
+} from '../../../utils/patch-set-util.js';
+import {ListChangesOption, listChangesOptionsToHex} from '../../../utils/change-util.js';
 
 const DiffViewMode = {
   SIDE_BY_SIDE: 'SIDE_BY_SIDE',
@@ -38,7 +41,6 @@
 const MAX_PROJECT_RESULTS = 25;
 // This value is somewhat arbitrary and not based on research or calculations.
 const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
-const PARENT_PATCH_NUM = 'PARENT';
 
 const Requests = {
   SEND_DIFF_DRAFT: 'sendDiffDraft',
@@ -81,19 +83,14 @@
   pendingRequest = {};
   grEtagDecorator = new GrEtagDecorator;
   projectLookup = {};
-  authService.clearCache();
+  appContext.authService.clearCache();
 }
 
 /**
  * @extends PolymerElement
  */
-class GrRestApiInterface extends mixinBehaviors( [
-  PathListBehavior,
-  PatchSetBehavior,
-  RESTClientBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrRestApiInterface extends GestureEventListeners(
+    LegacyElementMixin(PolymerElement)) {
   static get is() { return 'gr-rest-api-interface'; }
   /**
    * Fired when an server error occurs.
@@ -149,7 +146,7 @@
   /** @override */
   created() {
     super.created();
-    this._auth = authService;
+    this.authService = appContext.authService;
     this._initRestApiHelper();
   }
 
@@ -157,8 +154,8 @@
     if (this._restApiHelper) {
       return;
     }
-    if (this._cache && this._auth && this._sharedFetchPromises) {
-      this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
+    if (this._cache && this.authService && this._sharedFetchPromises) {
+      this._restApiHelper = new GrRestApiHelper(this._cache, this.authService,
           this._sharedFetchPromises, this);
     }
   }
@@ -751,7 +748,7 @@
     if (cachedAccount) {
       // Replace object in cache with new object to force UI updates.
       this._cache.set('/accounts/self/detail',
-          Object.assign({}, cachedAccount, obj));
+          {...cachedAccount, ...obj});
     }
   }
 
@@ -870,7 +867,7 @@
   }
 
   getLoggedIn() {
-    return this._auth.authCheck();
+    return this.authService.authCheck();
   }
 
   getIsAdmin() {
@@ -1058,16 +1055,16 @@
 
   _getChangesOptionsHex(config) {
     const options = [
-      this.ListChangesOption.LABELS,
-      this.ListChangesOption.DETAILED_ACCOUNTS,
+      ListChangesOption.LABELS,
+      ListChangesOption.DETAILED_ACCOUNTS,
     ];
     if (config && config.change && config.change.enable_attention_set) {
-      options.push(this.ListChangesOption.DETAILED_LABELS);
+      options.push(ListChangesOption.DETAILED_LABELS);
     } else {
-      options.push(this.ListChangesOption.REVIEWED);
+      options.push(ListChangesOption.REVIEWED);
     }
 
-    return this.listChangesOptionsToHex(...options);
+    return listChangesOptionsToHex(...options);
   }
 
   _getChangeOptionsHex(config) {
@@ -1079,20 +1076,20 @@
     // This list MUST be kept in sync with
     // ChangeIT#changeDetailsDoesNotRequireIndex
     const options = [
-      this.ListChangesOption.ALL_COMMITS,
-      this.ListChangesOption.ALL_REVISIONS,
-      this.ListChangesOption.CHANGE_ACTIONS,
-      this.ListChangesOption.DETAILED_LABELS,
-      this.ListChangesOption.DOWNLOAD_COMMANDS,
-      this.ListChangesOption.MESSAGES,
-      this.ListChangesOption.SUBMITTABLE,
-      this.ListChangesOption.WEB_LINKS,
-      this.ListChangesOption.SKIP_DIFFSTAT,
+      ListChangesOption.ALL_COMMITS,
+      ListChangesOption.ALL_REVISIONS,
+      ListChangesOption.CHANGE_ACTIONS,
+      ListChangesOption.DETAILED_LABELS,
+      ListChangesOption.DOWNLOAD_COMMANDS,
+      ListChangesOption.MESSAGES,
+      ListChangesOption.SUBMITTABLE,
+      ListChangesOption.WEB_LINKS,
+      ListChangesOption.SKIP_DIFFSTAT,
     ];
     if (config.receive && config.receive.enable_signed_push) {
-      options.push(this.ListChangesOption.PUSH_CERTIFICATES);
+      options.push(ListChangesOption.PUSH_CERTIFICATES);
     }
-    return this.listChangesOptionsToHex(...options);
+    return listChangesOptionsToHex(...options);
   }
 
   /**
@@ -1105,10 +1102,10 @@
     if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) {
       optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
     } else {
-      optionsHex = this.listChangesOptionsToHex(
-          this.ListChangesOption.ALL_COMMITS,
-          this.ListChangesOption.ALL_REVISIONS,
-          this.ListChangesOption.SKIP_DIFFSTAT
+      optionsHex = listChangesOptionsToHex(
+          ListChangesOption.ALL_COMMITS,
+          ListChangesOption.ALL_REVISIONS,
+          ListChangesOption.SKIP_DIFFSTAT
       );
     }
     return this._getChangeDetail(changeNum, optionsHex, opt_errFn,
@@ -1187,9 +1184,10 @@
    */
   getChangeFiles(changeNum, patchRange, opt_parentIndex) {
     let params = undefined;
-    if (this.isMergeParent(patchRange.basePatchNum)) {
-      params = {parent: this.getParentIndex(patchRange.basePatchNum)};
-    } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) {
+    if (isMergeParent(patchRange.basePatchNum)) {
+      params = {parent: getParentIndex(patchRange.basePatchNum)};
+    } else if (!patchNumEquals(patchRange.basePatchNum,
+        SPECIAL_PATCH_SET_NUM.PARENT)) {
       params = {base: patchRange.basePatchNum};
     }
     return this._getChangeURLAndFetch({
@@ -1240,7 +1238,7 @@
    * @return {!Promise<!Array<!Object>>}
    */
   getChangeOrEditFiles(changeNum, patchRange) {
-    if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
+    if (patchNumEquals(patchRange.patchNum, SPECIAL_PATCH_SET_NUM.EDIT)) {
       return this.getChangeEditFiles(changeNum, patchRange).then(res =>
         res.files);
     }
@@ -1613,9 +1611,9 @@
   }
 
   getChangeConflicts(changeNum) {
-    const options = this.listChangesOptionsToHex(
-        this.ListChangesOption.CURRENT_REVISION,
-        this.ListChangesOption.CURRENT_COMMIT
+    const options = listChangesOptionsToHex(
+        ListChangesOption.CURRENT_REVISION,
+        ListChangesOption.CURRENT_COMMIT
     );
     const params = {
       O: options,
@@ -1629,9 +1627,9 @@
   }
 
   getChangeCherryPicks(project, changeID, changeNum) {
-    const options = this.listChangesOptionsToHex(
-        this.ListChangesOption.CURRENT_REVISION,
-        this.ListChangesOption.CURRENT_COMMIT
+    const options = listChangesOptionsToHex(
+        ListChangesOption.CURRENT_REVISION,
+        ListChangesOption.CURRENT_COMMIT
     );
     const query = [
       'project:' + project,
@@ -1651,11 +1649,11 @@
   }
 
   getChangesWithSameTopic(topic, changeNum) {
-    const options = this.listChangesOptionsToHex(
-        this.ListChangesOption.LABELS,
-        this.ListChangesOption.CURRENT_REVISION,
-        this.ListChangesOption.CURRENT_COMMIT,
-        this.ListChangesOption.DETAILED_LABELS
+    const options = listChangesOptionsToHex(
+        ListChangesOption.LABELS,
+        ListChangesOption.CURRENT_REVISION,
+        ListChangesOption.CURRENT_COMMIT,
+        ListChangesOption.DETAILED_LABELS
     );
     const query = [
       'status:open',
@@ -1779,7 +1777,7 @@
       }
       return res;
     };
-    const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
+    const promise = patchNumEquals(patchNum, SPECIAL_PATCH_SET_NUM.EDIT) ?
       this._getFileInChangeEdit(changeNum, path) :
       this._getFileInRevision(changeNum, path, patchNum, suppress404s);
 
@@ -2027,9 +2025,9 @@
       intraline: null,
       whitespace: opt_whitespace || 'IGNORE_NONE',
     };
-    if (this.isMergeParent(basePatchNum)) {
-      params.parent = this.getParentIndex(basePatchNum);
-    } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) {
+    if (isMergeParent(basePatchNum)) {
+      params.parent = getParentIndex(basePatchNum);
+    } else if (!patchNumEquals(basePatchNum, SPECIAL_PATCH_SET_NUM.PARENT)) {
       params.base = basePatchNum;
     }
     const endpoint = `/files/${encodeURIComponent(path)}/diff`;
@@ -2043,7 +2041,7 @@
     };
 
     // Invalidate the cache if its edit patch to make sure we always get latest.
-    if (patchNum === this.EDIT_NAME) {
+    if (patchNum === SPECIAL_PATCH_SET_NUM.EDIT) {
       if (!req.fetchOptions) req.fetchOptions = {};
       if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
       req.fetchOptions.headers.append('Cache-Control', 'no-cache');
@@ -2148,8 +2146,8 @@
     if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
       return fetchComments();
     }
-    function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
-    function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
+    function onlyParent(c) { return c.side == SPECIAL_PATCH_SET_NUM.PARENT; }
+    function withoutParent(c) { return c.side != SPECIAL_PATCH_SET_NUM.PARENT; }
     function setPath(c) { c.path = opt_path; }
 
     const promises = [];
@@ -2164,7 +2162,7 @@
       // in a single pass.
       comments = this._setRanges(comments);
 
-      if (opt_basePatchNum == PARENT_PATCH_NUM) {
+      if (opt_basePatchNum == SPECIAL_PATCH_SET_NUM.PARENT) {
         baseComments = comments.filter(onlyParent);
         baseComments.forEach(setPath);
       }
@@ -2174,7 +2172,7 @@
     });
     promises.push(fetchPromise);
 
-    if (opt_basePatchNum != PARENT_PATCH_NUM) {
+    if (opt_basePatchNum != SPECIAL_PATCH_SET_NUM.PARENT) {
       fetchPromise = fetchComments(opt_basePatchNum).then(response => {
         baseComments = (response[opt_path] || [])
             .filter(withoutParent);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
index 639b768..aadde88 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
@@ -19,7 +19,8 @@
 import './gr-rest-api-interface.js';
 import {mockPromise} from '../../../test/test-utils.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
-import {authService} from './gr-auth.js';
+import {ListChangesOption} from '../../../utils/change-util.js';
+import {appContext} from '../../../services/app-context.js';
 
 const basicFixture = fixtureFromElement('gr-rest-api-interface');
 
@@ -43,7 +44,8 @@
       },
     }));
     // fake auth
-    sinon.stub(authService, 'authCheck').returns(Promise.resolve(true));
+    sinon.stub(appContext.authService, 'authCheck')
+        .returns(Promise.resolve(true));
     element = basicFixture.instantiate();
     element._projectLookup = {};
   });
@@ -256,59 +258,6 @@
         });
   });
 
-  test('special file path sorting', () => {
-    assert.deepEqual(
-        ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
-            element.specialFilePathCompare),
-        ['/COMMIT_MSG', '.a', '.b', 'file']);
-
-    assert.deepEqual(
-        ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
-            element.specialFilePathCompare),
-        ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
-
-    assert.deepEqual(
-        ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
-            element.specialFilePathCompare),
-        ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
-
-    assert.deepEqual(
-        ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
-            element.specialFilePathCompare),
-        ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
-
-    assert.deepEqual(
-        ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
-            element.specialFilePathCompare),
-        ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
-
-    // Regression test for Issue 4448.
-    assert.deepEqual(
-        [
-          'minidump/minidump_memory_writer.cc',
-          'minidump/minidump_memory_writer.h',
-          'minidump/minidump_thread_writer.cc',
-          'minidump/minidump_thread_writer.h',
-        ].sort(element.specialFilePathCompare),
-        [
-          'minidump/minidump_memory_writer.h',
-          'minidump/minidump_memory_writer.cc',
-          'minidump/minidump_thread_writer.h',
-          'minidump/minidump_thread_writer.cc',
-        ]);
-
-    // Regression test for Issue 4545.
-    assert.deepEqual(
-        [
-          'task_test.go',
-          'task.go',
-        ].sort(element.specialFilePathCompare),
-        [
-          'task.go',
-          'task_test.go',
-        ]);
-  });
-
   test('server error', done => {
     const getResponseObjectStub = sinon.stub(element, 'getResponseObject');
     window.fetch.returns(Promise.resolve({ok: false}));
@@ -923,9 +872,9 @@
   });
 
   test('gerrit auth is used', () => {
-    sinon.stub(authService, 'fetch').returns(Promise.resolve());
+    sinon.stub(appContext.authService, 'fetch').returns(Promise.resolve());
     element._restApiHelper.fetchJSON({url: 'foo'});
-    assert(authService.fetch.called);
+    assert(appContext.authService.fetch.called);
   });
 
   test('getSuggestedAccounts does not return _fetchJSON', () => {
@@ -949,35 +898,27 @@
 
   suite('getChangeDetail', () => {
     suite('change detail options', () => {
-      let toHexStub;
-
       setup(() => {
-        toHexStub = sinon.stub(element, 'listChangesOptionsToHex').callsFake(
-            options => 'deadbeef');
         sinon.stub(element, '_getChangeDetail').callsFake(
             async (changeNum, options) => { return {changeNum, options}; });
       });
 
       test('signed pushes disabled', async () => {
-        const {PUSH_CERTIFICATES} = element.ListChangesOption;
         sinon.stub(element, 'getConfig').callsFake( async () => { return {}; });
         const {changeNum, options} = await element.getChangeDetail(123);
         assert.strictEqual(123, changeNum);
-        assert.strictEqual('deadbeef', options);
-        assert.isTrue(toHexStub.calledOnce);
-        assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+        assert.isNotOk(
+            parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES));
       });
 
       test('signed pushes enabled', async () => {
-        const {PUSH_CERTIFICATES} = element.ListChangesOption;
         sinon.stub(element, 'getConfig').callsFake( async () => {
           return {receive: {enable_signed_push: true}};
         });
         const {changeNum, options} = await element.getChangeDetail(123);
         assert.strictEqual(123, changeNum);
-        assert.strictEqual('deadbeef', options);
-        assert.isTrue(toHexStub.calledOnce);
-        assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES));
+        assert.ok(
+            parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES));
       });
     });
 
@@ -1355,7 +1296,7 @@
       done();
     };
     element.addEventListener('server-error', handler);
-    sinon.stub(authService, 'fetch').returns(Promise.resolve(res));
+    sinon.stub(appContext.authService, 'fetch').returns(Promise.resolve(res));
     sinon.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
     element.getFileContent('1', 'tst/path', '1').then(() => {
       flushAsynchronousOperations();
@@ -1387,7 +1328,7 @@
     const response = {status: 404, text: sinon.stub()};
     const url = 'my url';
     const fetchOptions = {method: 'DELETE'};
-    sinon.stub(element._auth, 'fetch').returns(Promise.resolve(response));
+    sinon.stub(element.authService, 'fetch').returns(Promise.resolve(response));
     const startTime = 123;
     sinon.stub(Date, 'now').returns(startTime);
     const req = {url, fetchOptions};
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
index d54d342..f0932b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
@@ -150,7 +150,7 @@
     const elapsed = (endTime - startTime);
     const startAt = new Date(startTime);
     const endAt = new Date(endTime);
-    console.log([
+    console.info([
       'HTTP',
       status,
       method,
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
index 9cac375..a50a9e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
@@ -18,7 +18,7 @@
 import '../../../../test/common-test-setup-karma.js';
 import {SiteBasedCache} from './gr-rest-api-helper.js';
 import {FetchPromisesCache, GrRestApiHelper} from './gr-rest-api-helper.js';
-import {authService} from '../gr-auth.js';
+import {appContext} from '../../../../services/app-context.js';
 
 suite('gr-rest-api-helper tests', () => {
   let helper;
@@ -46,8 +46,8 @@
       },
     }));
 
-    helper = new GrRestApiHelper(cache, authService, fetchPromisesCache,
-        mockRestApiInterface);
+    helper = new GrRestApiHelper(cache, appContext.authService,
+        fetchPromisesCache, mockRestApiInterface);
   });
 
   teardown(() => {
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 ed474d8..49887240 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
@@ -18,212 +18,220 @@
 import {parseDate} from '../../../utils/date-util.js';
 import {MessageTag} from '../../../constants/constants.js';
 
-/** @constructor */
-export function GrReviewerUpdatesParser(change) {
-  this.result = Object.assign({}, change);
-  this._lastState = {};
-}
+const MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500;
+const REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000;
 
-GrReviewerUpdatesParser.parse = function(change) {
-  if (!change ||
-      !change.messages ||
-      !change.reviewer_updates ||
-      !change.reviewer_updates.length) {
-    return change;
+export class GrReviewerUpdatesParser {
+  constructor(change) {
+    this.result = {...change};
+    this._lastState = {};
+    this._batch = null;
+    this._updateItems = null;
   }
-  const parser = new GrReviewerUpdatesParser(change);
-  parser._filterRemovedMessages();
-  parser._groupUpdates();
-  parser._formatUpdates();
-  parser._advanceUpdates();
-  return parser.result;
-};
 
-GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500;
-GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000;
-
-GrReviewerUpdatesParser.prototype.result = null;
-GrReviewerUpdatesParser.prototype._batch = null;
-GrReviewerUpdatesParser.prototype._updateItems = null;
-GrReviewerUpdatesParser.prototype._lastState = null;
-
-/**
- * Removes messages that describe removed reviewers, since reviewer_updates
- * are used.
- */
-GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() {
-  this.result.messages = this.result.messages
-      .filter(message => message.tag !== MessageTag.TAG_DELETE_REVIEWER);
-};
-
-/**
- * Is a part of _groupUpdates(). Creates a new batch of updates.
- *
- * @param {Object} update instance of ReviewerUpdateInfo
- */
-GrReviewerUpdatesParser.prototype._startBatch = function(update) {
-  this._updateItems = [];
-  return {
-    author: update.updated_by,
-    date: update.updated,
-    type: 'REVIEWER_UPDATE',
-    tag: MessageTag.TAG_REVIEWER_UPDATE,
-  };
-};
-
-/**
- * Is a part of _groupUpdates(). Validates current batch:
- * - filters out updates that don't change reviewer state.
- * - updates current reviewer state.
- *
- * @param {Object} update instance of ReviewerUpdateInfo
- */
-GrReviewerUpdatesParser.prototype._completeBatch = function(update) {
-  const items = [];
-  for (const accountId in this._updateItems) {
-    if (!this._updateItems.hasOwnProperty(accountId)) continue;
-    const updateItem = this._updateItems[accountId];
-    if (this._lastState[accountId] !== updateItem.state) {
-      this._lastState[accountId] = updateItem.state;
-      items.push(updateItem);
-    }
+  /**
+   * Removes messages that describe removed reviewers, since reviewer_updates
+   * are used.
+   */
+  _filterRemovedMessages() {
+    this.result.messages = this.result.messages.filter(
+        message => message.tag !== MessageTag.TAG_DELETE_REVIEWER
+    );
   }
-  if (items.length) {
-    this._batch.updates = items;
-  }
-};
 
-/**
- * Groups reviewer updates. Sequential updates are grouped if:
- * - They were performed within short timeframe (6 seconds)
- * - Made by the same person
- * - Non-change updates are discarded within a group
- * - Groups with no-change updates are discarded (eg CC -> CC)
- */
-GrReviewerUpdatesParser.prototype._groupUpdates = function() {
-  const updates = this.result.reviewer_updates;
-  const newUpdates = updates.reduce((newUpdates, update) => {
-    if (!this._batch) {
-      this._batch = this._startBatch(update);
-    }
-    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 ||
-        update.updated_by._account_id !== this._batch.author._account_id) {
-      // Next sequential update should form new group.
-      this._completeBatch();
-      if (this._batch.updates && this._batch.updates.length) {
-        newUpdates.push(this._batch);
-      }
-      this._batch = this._startBatch(update);
-    }
-    this._updateItems[reviewerId] = {
-      reviewer: update.reviewer,
-      state: update.state,
+  /**
+   * Is a part of _groupUpdates(). Creates a new batch of updates.
+   *
+   * @param {Object} update instance of ReviewerUpdateInfo
+   */
+  _startBatch(update) {
+    this._updateItems = [];
+    return {
+      author: update.updated_by,
+      date: update.updated,
+      type: 'REVIEWER_UPDATE',
+      tag: MessageTag.TAG_REVIEWER_UPDATE,
     };
-    if (this._lastState[reviewerId]) {
-      this._updateItems[reviewerId].prev_state = this._lastState[reviewerId];
-    }
-    return newUpdates;
-  }, []);
-  this._completeBatch();
-  if (this._batch.updates && this._batch.updates.length) {
-    newUpdates.push(this._batch);
   }
-  this.result.reviewer_updates = newUpdates;
-};
 
-/**
- * Generates update message for reviewer state change.
- *
- * @param {string} prev previous reviewer state.
- * @param {string} state current reviewer state.
- * @return {string}
- */
-GrReviewerUpdatesParser.prototype._getUpdateMessage = function(prev, state) {
-  if (prev === 'REMOVED' || !prev) {
-    return 'Added to ' + state.toLowerCase() + ': ';
-  } else if (state === 'REMOVED') {
-    if (prev) {
-      return 'Removed from ' + prev.toLowerCase() + ': ';
+  /**
+   * Is a part of _groupUpdates(). Validates current batch:
+   * - filters out updates that don't change reviewer state.
+   * - updates current reviewer state.
+   *
+   * @param {Object} update instance of ReviewerUpdateInfo
+   */
+  _completeBatch(update) {
+    const items = [];
+    for (const accountId in this._updateItems) {
+      if (!this._updateItems.hasOwnProperty(accountId)) continue;
+      const updateItem = this._updateItems[accountId];
+      if (this._lastState[accountId] !== updateItem.state) {
+        this._lastState[accountId] = updateItem.state;
+        items.push(updateItem);
+      }
+    }
+    if (items.length) {
+      this._batch.updates = items;
+    }
+  }
+
+  /**
+   * Groups reviewer updates. Sequential updates are grouped if:
+   * - They were performed within short timeframe (6 seconds)
+   * - Made by the same person
+   * - Non-change updates are discarded within a group
+   * - Groups with no-change updates are discarded (eg CC -> CC)
+   */
+  _groupUpdates() {
+    const updates = this.result.reviewer_updates;
+    const newUpdates = updates.reduce((newUpdates, update) => {
+      if (!this._batch) {
+        this._batch = this._startBatch(update);
+      }
+      const updateDate = parseDate(update.updated).getTime();
+      const batchUpdateDate = parseDate(this._batch.date).getTime();
+      const reviewerId = update.reviewer._account_id.toString();
+      if (
+        updateDate - batchUpdateDate >
+          REVIEWER_UPDATE_THRESHOLD_MILLIS ||
+        update.updated_by._account_id !== this._batch.author._account_id
+      ) {
+        // Next sequential update should form new group.
+        this._completeBatch();
+        if (this._batch.updates && this._batch.updates.length) {
+          newUpdates.push(this._batch);
+        }
+        this._batch = this._startBatch(update);
+      }
+      this._updateItems[reviewerId] = {
+        reviewer: update.reviewer,
+        state: update.state,
+      };
+      if (this._lastState[reviewerId]) {
+        this._updateItems[reviewerId].prev_state = this._lastState[reviewerId];
+      }
+      return newUpdates;
+    }, []);
+    this._completeBatch();
+    if (this._batch.updates && this._batch.updates.length) {
+      newUpdates.push(this._batch);
+    }
+    this.result.reviewer_updates = newUpdates;
+  }
+
+  /**
+   * Generates update message for reviewer state change.
+   *
+   * @param {string} prev previous reviewer state.
+   * @param {string} state current reviewer state.
+   * @return {string}
+   */
+  _getUpdateMessage(prev, state) {
+    if (prev === 'REMOVED' || !prev) {
+      return 'Added to ' + state.toLowerCase() + ': ';
+    } else if (state === 'REMOVED') {
+      if (prev) {
+        return 'Removed from ' + prev.toLowerCase() + ': ';
+      } else {
+        return 'Removed : ';
+      }
     } else {
-      return 'Removed : ';
+      return (
+        'Moved from ' + prev.toLowerCase() + ' to ' + state.toLowerCase() + ': '
+      );
     }
-  } else {
-    return 'Moved from ' + prev.toLowerCase() + ' to ' + state.toLowerCase() +
-        ': ';
   }
-};
 
-/**
- * Groups updates for same category (eg CC->CC) into a hash arrays of
- * reviewers.
- *
- * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo.
- * @return {!Object} Hash of arrays of AccountInfo, message as key.
- */
-GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) {
-  return updates.reduce((result, item) => {
-    const message = this._getUpdateMessage(item.prev_state, item.state);
-    if (!result[message]) {
-      result[message] = [];
-    }
-    result[message].push(item.reviewer);
-    return result;
-  }, {});
-};
-
-/**
- * Generates text messages for grouped reviewer updates.
- * Formats reviewer updates to a (not yet implemented) EventInfo instance.
- *
- * @see https://gerrit-review.googlesource.com/c/94490/
- */
-GrReviewerUpdatesParser.prototype._formatUpdates = function() {
-  for (const update of this.result.reviewer_updates) {
-    const grouppedReviewers = this._groupUpdatesByMessage(update.updates);
-    const newUpdates = [];
-    for (const message in grouppedReviewers) {
-      if (grouppedReviewers.hasOwnProperty(message)) {
-        newUpdates.push({
-          message,
-          reviewers: grouppedReviewers[message],
-        });
+  /**
+   * Groups updates for same category (eg CC->CC) into a hash arrays of
+   * reviewers.
+   *
+   * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo.
+   * @return {!Object} Hash of arrays of AccountInfo, message as key.
+   */
+  _groupUpdatesByMessage(updates) {
+    return updates.reduce((result, item) => {
+      const message = this._getUpdateMessage(item.prev_state, item.state);
+      if (!result[message]) {
+        result[message] = [];
       }
-    }
-    update.updates = newUpdates;
+      result[message].push(item.reviewer);
+      return result;
+    }, {});
   }
-};
 
-/**
- * Moves reviewer updates that are within short time frame of change messages
- * back in time so they would come before change messages.
- * TODO(viktard): Remove when server-side serves reviewer updates like so.
- */
-GrReviewerUpdatesParser.prototype._advanceUpdates = function() {
-  const updates = this.result.reviewer_updates;
-  const messages = this.result.messages;
-  messages.forEach((message, index) => {
-    const messageDate = parseDate(message.date).getTime();
-    const nextMessageDate = index === messages.length - 1 ? null :
-      parseDate(messages[index + 1].date).getTime();
-    for (const update of updates) {
-      const date = parseDate(update.date).getTime();
-      if (date >= messageDate &&
-          (!nextMessageDate || date < nextMessageDate)) {
-        const timestamp = parseDate(update.date).getTime() -
-            GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
-        update.date = new Date(timestamp)
-            .toISOString()
-            .replace('T', ' ')
-            .replace('Z', '000000');
+  /**
+   * Generates text messages for grouped reviewer updates.
+   * Formats reviewer updates to a (not yet implemented) EventInfo instance.
+   *
+   * @see https://gerrit-review.googlesource.com/c/94490/
+   */
+  _formatUpdates() {
+    for (const update of this.result.reviewer_updates) {
+      const grouppedReviewers = this._groupUpdatesByMessage(update.updates);
+      const newUpdates = [];
+      for (const message in grouppedReviewers) {
+        if (grouppedReviewers.hasOwnProperty(message)) {
+          newUpdates.push({
+            message,
+            reviewers: grouppedReviewers[message],
+          });
+        }
       }
-      if (nextMessageDate && date > nextMessageDate) {
-        break;
-      }
+      update.updates = newUpdates;
     }
-  });
-};
+  }
 
+  /**
+   * Moves reviewer updates that are within short time frame of change messages
+   * back in time so they would come before change messages.
+   * TODO(viktard): Remove when server-side serves reviewer updates like so.
+   */
+  _advanceUpdates() {
+    const updates = this.result.reviewer_updates;
+    const messages = this.result.messages;
+    messages.forEach((message, index) => {
+      const messageDate = parseDate(message.date).getTime();
+      const nextMessageDate =
+        index === messages.length - 1
+          ? null
+          : parseDate(messages[index + 1].date).getTime();
+      for (const update of updates) {
+        const date = parseDate(update.date).getTime();
+        if (
+          date >= messageDate &&
+          (!nextMessageDate || date < nextMessageDate)
+        ) {
+          const timestamp =
+            parseDate(update.date).getTime() -
+            MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
+          update.date = new Date(timestamp)
+              .toISOString()
+              .replace('T', ' ')
+              .replace('Z', '000000');
+        }
+        if (nextMessageDate && date > nextMessageDate) {
+          break;
+        }
+      }
+    });
+  }
+
+  static parse(change) {
+    if (
+      !change ||
+    !change.messages ||
+    !change.reviewer_updates ||
+    !change.reviewer_updates.length
+    ) {
+      return change;
+    }
+    const parser = new GrReviewerUpdatesParser(change);
+    parser._filterRemovedMessages();
+    parser._groupUpdates();
+    parser._formatUpdates();
+    parser._advanceUpdates();
+    return parser.result;
+  }
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
index f408a97..34fb709 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.js
@@ -22,10 +22,6 @@
 suite('gr-reviewer-updates-parser tests', () => {
   let instance;
 
-  setup(() => {
-
-  });
-
   test('ignores changes without messages', () => {
     const change = {};
     sinon.stub(
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
rename to polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.ts
index 4a4480e..ef76999 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 e98e11d..8f763f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -21,12 +21,11 @@
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 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 {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {appContext} from '../../../services/app-context.js';
 
 const MAX_ITEMS_DROPDOWN = 10;
@@ -67,11 +66,8 @@
 /**
  * @extends PolymerElement
  */
-class GrTextarea extends mixinBehaviors( [
-  KeyboardShortcutBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrTextarea extends KeyboardShortcutMixin(GestureEventListeners(
+    LegacyElementMixin(PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-textarea'; }
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.ts
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
rename to polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.ts
index 8454f62..1f777aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
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 502c82e..d2182e4 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
@@ -15,21 +15,19 @@
  * limitations under the License.
  */
 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';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-tooltip-content_html.js';
-import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
+import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin.js';
 
 /**
  * @extends PolymerElement
  */
-class GrTooltipContent extends mixinBehaviors( [
-  TooltipBehavior,
-], GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement))) {
+class GrTooltipContent extends TooltipMixin(
+    GestureEventListeners(
+        LegacyElementMixin(
+            PolymerElement))) {
   static get template() { return htmlTemplate; }
 
   static get is() { return 'gr-tooltip-content'; }
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
similarity index 93%
rename from polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
rename to polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
index e5a2813..952420d 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
similarity index 96%
rename from polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
rename to polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
index 3f02fc5..d59a6c3 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
@@ -14,7 +14,7 @@
  * 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 {html} from '@polymer/polymer/lib/utils/html-tag';
 
 export const htmlTemplate = html`
   <style include="shared-styles">
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
index 3d9c2bc..2f6bfea 100644
--- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
+++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
+import {patchNumEquals} from '../../../utils/patch-set-util.js';
 
 /**
  * @constructor
@@ -76,6 +76,6 @@
  */
 RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) {
   const rev = Object.values(this._change.revisions).find(rev =>
-    PatchSetBehavior.patchNumEquals(rev._number, patchNum));
+    patchNumEquals(rev._number, patchNum));
   return rev.commit.parents[parentIndex].commit;
 };
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.js b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
index 90110f0..933edba 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
@@ -31,6 +31,20 @@
   }
 }
 
+class MockAuthService {
+  clearCache() {
+
+  }
+
+  get isAuthed() {
+    return false;
+  }
+
+  authCheck() {
+    return Promise.resolve(false);
+  }
+}
+
 // Setup mocks for appContext.
 // This is a temporary solution
 // TODO(dmfilippov): find a better solution for gr-diff
@@ -44,4 +58,5 @@
   }
   setMock('flagsService', new MockFlagsService);
   setMock('reportingService', grReportingMock);
+  setMock('authService', new MockAuthService);
 }
diff --git a/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.js b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.js
new file mode 100644
index 0000000..e3b9f20
--- /dev/null
+++ b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin.js
@@ -0,0 +1,122 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin.js';
+
+/**
+ * @polymer
+ * @mixinFunction
+ */
+export const ChangeTableMixin = dedupingMixin(superClass => {
+  /**
+   * @polymer
+   * @mixinClass
+   */
+  class Mixin extends superClass {
+    static get properties() {
+      return {
+        columnNames: {
+          type: Array,
+          value: [
+            'Subject',
+            'Status',
+            'Owner',
+            'Assignee',
+            'Reviewers',
+            'Comments',
+            'Repo',
+            'Branch',
+            'Updated',
+            'Size',
+          ],
+          readOnly: true,
+        },
+      };
+    }
+
+    /**
+     * Returns the complement to the given column array
+     *
+     * @param {Array} columns
+     * @return {!Array}
+     */
+    getComplementColumns(columns) {
+      return this.columnNames.filter(column => !columns.includes(column));
+    }
+
+    /**
+     * @param {string} columnToCheck
+     * @param {!Array} columnsToDisplay
+     * @return {boolean}
+     */
+    isColumnHidden(columnToCheck, columnsToDisplay) {
+      if ([columnsToDisplay, columnToCheck].includes(undefined)) {
+        return false;
+      }
+      return !columnsToDisplay.includes(columnToCheck);
+    }
+
+    /**
+     * Is the column disabled by a server config or experiment? For example the
+     * assignee feature might be disabled and thus the corresponding column is
+     * also disabled.
+     *
+     * @param {string} column
+     * @param {Object} config
+     * @param {!Array<string>} experiments
+     * @return {boolean}
+     */
+    isColumnEnabled(column, config, experiments) {
+      if (!config || !config.change) return true;
+      if (column === 'Assignee') return !!config.change.enable_assignee;
+      if (column === 'Comments') return experiments.includes('comments-column');
+      if (column === 'Reviewers') return !!config.change.enable_attention_set;
+      return true;
+    }
+
+    /**
+     * @param {!Array<string>} columns
+     * @param {Object} config
+     * @param {!Array<string>} experiments
+     * @return {!Array<string>} enabled columns, see isColumnEnabled().
+     */
+    getEnabledColumns(columns, config, experiments) {
+      return columns.filter(
+          col => this.isColumnEnabled(col, config, experiments));
+    }
+
+    /**
+     * The Project column was renamed to Repo, but some users may have
+     * preferences that use its old name. If that column is found, rename it
+     * before use.
+     *
+     * @param {!Array<string>} columns
+     * @return {!Array<string>} If the column was renamed, returns a new array
+     *     with the corrected name. Otherwise, it returns the original param.
+     */
+    getVisibleColumns(columns) {
+      const projectIndex = columns.indexOf('Project');
+      if (projectIndex === -1) { return columns; }
+      const newColumns = columns.slice(0);
+      newColumns[projectIndex] = 'Repo';
+      return newColumns;
+    }
+  }
+
+  return Mixin;
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.js b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
similarity index 73%
rename from polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.js
rename to polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
index dfca358..daf10ce 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.js
+++ b/polygerrit-ui/app/mixins/gr-change-table-mixin/gr-change-table-mixin_test.js
@@ -16,36 +16,25 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {ChangeTableBehavior} from './gr-change-table-behavior.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {ChangeTableMixin} from './gr-change-table-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+
+class GrChangeTableMixinTestElement extends
+  ChangeTableMixin(PolymerElement) {
+  static get is() { return 'gr-change-table-mixin-test-element'; }
+}
+
+customElements.define(GrChangeTableMixinTestElement.is,
+    GrChangeTableMixinTestElement);
 
 const basicFixture = fixtureFromElement(
-    'gr-change-table-behavior-test-element');
+    'gr-change-table-mixin-test-element');
 
-const withinOverlayFixture = fixtureFromTemplate(html`
-  <gr-overlay>
-    <gr-change-table-behavior-test-element>
-    </gr-change-table-behavior-test-element>
-  </gr-overlay>
-`);
-
-suite('gr-change-table-behavior tests', () => {
+suite('gr-change-table-mixin tests', () => {
   let element;
-  // eslint-disable-next-line no-unused-vars
-  let overlay;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'gr-change-table-behavior-test-element',
-      behaviors: [ChangeTableBehavior],
-    });
-  });
 
   setup(() => {
     element = basicFixture.instantiate();
-    overlay = withinOverlayFixture.instantiate();
   });
 
   test('getComplementColumns', () => {
diff --git a/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.js b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.js
new file mode 100644
index 0000000..0b2c306
--- /dev/null
+++ b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin.js
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {encodeURL, getBaseUrl} from '../../utils/url-util.js';
+import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin.js';
+
+/**
+ * @polymer
+ * @mixinFunction
+ */
+export const ListViewMixin = dedupingMixin(superClass => {
+  /**
+   * @polymer
+   * @mixinClass
+   */
+  class Mixin extends superClass {
+    computeLoadingClass(loading) {
+      return loading ? 'loading' : '';
+    }
+
+    computeShownItems(items) {
+      return items.slice(0, 25);
+    }
+
+    getUrl(path, item) {
+      return getBaseUrl() + path + encodeURL(item, true);
+    }
+
+    /**
+     * @param {Object} params
+     * @return {string}
+     */
+    getFilterValue(params) {
+      if (!params) { return ''; }
+      return params.filter || '';
+    }
+
+    /**
+     * @param {Object} params
+     * @return {number}
+     */
+    getOffsetValue(params) {
+      if (params && params.offset) {
+        return params.offset;
+      }
+      return 0;
+    }
+  }
+
+  return Mixin;
+});
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.js b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
similarity index 79%
rename from polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.js
rename to polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
index bb54672..407f29f 100644
--- a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.js
+++ b/polygerrit-ui/app/mixins/gr-list-view-mixin/gr-list-view-mixin_test.js
@@ -16,24 +16,22 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {ListViewBehavior} from './gr-list-view-behavior.js';
+import {ListViewMixin} from './gr-list-view-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
 const basicFixture = fixtureFromElement(
-    'gr-list-view-behavior-test-element');
+    'gr-list-view-mixin-test-element');
 
-suite('gr-list-view-behavior tests', () => {
+class GrListViewMixinTestElement extends
+  ListViewMixin(PolymerElement) {
+  static get is() { return 'gr-list-view-mixin-test-element'; }
+}
+
+customElements.define(GrListViewMixinTestElement.is,
+    GrListViewMixinTestElement);
+
+suite('gr-list-view-mixin tests', () => {
   let element;
-  // eslint-disable-next-line no-unused-vars
-  let overlay;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'gr-list-view-behavior-test-element',
-      behaviors: [ListViewBehavior],
-    });
-  });
 
   setup(() => {
     element = basicFixture.instantiate();
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.js b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.js
new file mode 100644
index 0000000..68d594d
--- /dev/null
+++ b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.js
@@ -0,0 +1,179 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../elements/shared/gr-tooltip/gr-tooltip.js';
+import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {getRootElement} from '../../scripts/rootElement.js';
+import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin.js';
+
+const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
+
+/**
+ * @polymer
+ * @mixinFunction
+ */
+export const TooltipMixin = dedupingMixin(superClass => {
+  /**
+   * @polymer
+   * @mixinClass
+   */
+  class Mixin extends superClass {
+    static get properties() {
+      return {
+        hasTooltip: {
+          type: Boolean,
+          observer: '_setupTooltipListeners',
+        },
+        positionBelow: {
+          type: Boolean,
+          value: false,
+          reflectToAttribute: true,
+        },
+
+        _isTouchDevice: {
+          type: Boolean,
+          value() {
+            return 'ontouchstart' in document.documentElement;
+          },
+        },
+        _tooltip: Object,
+        _titleText: String,
+        _hasSetupTooltipListeners: {
+          type: Boolean,
+          value: false,
+        },
+      };
+    }
+
+    /** @override */
+    disconnectedCallback() {
+      super.disconnectedCallback();
+      // NOTE: if you define your own `detached` in your component
+      // then this won't take affect (as its not a class yet)
+      this._handleHideTooltip();
+      this.removeEventListener('mouseenter', this._mouseenterHandler);
+    }
+
+    _setupTooltipListeners() {
+      if (!this._mouseenterHandler) {
+        this._mouseenterHandler = this._handleShowTooltip.bind(this);
+      }
+
+      if (!this.hasTooltip) {
+        // if attribute set to false, remove the listener
+        this.removeEventListener('mouseenter', this._mouseenterHandler);
+        this._hasSetupTooltipListeners = false;
+        return;
+      }
+
+      if (this._hasSetupTooltipListeners) {
+        return;
+      }
+      this._hasSetupTooltipListeners = true;
+
+      this.addEventListener('mouseenter', this._mouseenterHandler);
+    }
+
+    _handleShowTooltip(e) {
+      if (this._isTouchDevice) { return; }
+
+      if (!this.hasAttribute('title') ||
+          this.getAttribute('title') === '' ||
+          this._tooltip) {
+        return;
+      }
+
+      // Store the title attribute text then set it to an empty string to
+      // prevent it from showing natively.
+      this._titleText = this.getAttribute('title');
+      this.setAttribute('title', '');
+
+      const tooltip = document.createElement('gr-tooltip');
+      tooltip.text = this._titleText;
+      tooltip.maxWidth = this.getAttribute('max-width');
+      tooltip.positionBelow = this.getAttribute('position-below');
+
+      // Set visibility to hidden before appending to the DOM so that
+      // calculations can be made based on the element’s size.
+      tooltip.style.visibility = 'hidden';
+      getRootElement().appendChild(tooltip);
+      this._positionTooltip(tooltip);
+      tooltip.style.visibility = null;
+
+      this._tooltip = tooltip;
+      this.listen(window, 'scroll', '_handleWindowScroll');
+      this.listen(this, 'mouseleave', '_handleHideTooltip');
+      this.listen(this, 'click', '_handleHideTooltip');
+    }
+
+    _handleHideTooltip(e) {
+      if (this._isTouchDevice) { return; }
+      if (!this.hasAttribute('title') ||
+          this._titleText == null) {
+        return;
+      }
+
+      this.unlisten(window, 'scroll', '_handleWindowScroll');
+      this.unlisten(this, 'mouseleave', '_handleHideTooltip');
+      this.unlisten(this, 'click', '_handleHideTooltip');
+      this.setAttribute('title', this._titleText);
+      if (this._tooltip && this._tooltip.parentNode) {
+        this._tooltip.parentNode.removeChild(this._tooltip);
+      }
+      this._tooltip = null;
+    }
+
+    _handleWindowScroll(e) {
+      if (!this._tooltip) { return; }
+
+      this._positionTooltip(this._tooltip);
+    }
+
+    _positionTooltip(tooltip) {
+      // This flush is needed for tooltips to be positioned correctly in Firefox
+      // and Safari.
+      flush();
+      const rect = this.getBoundingClientRect();
+      const boxRect = tooltip.getBoundingClientRect();
+      const parentRect = tooltip.parentElement.getBoundingClientRect();
+      const top = rect.top - parentRect.top;
+      const left =
+          rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
+      const right = parentRect.width - left - boxRect.width;
+      if (left < 0) {
+        tooltip.updateStyles({
+          '--gr-tooltip-arrow-center-offset': left + 'px',
+        });
+      } else if (right < 0) {
+        tooltip.updateStyles({
+          '--gr-tooltip-arrow-center-offset': (-0.5 * right) + 'px',
+        });
+      }
+      tooltip.style.left = Math.max(0, left) + 'px';
+
+      if (!this.positionBelow) {
+        tooltip.style.top = Math.max(0, top) + 'px';
+        tooltip.style.transform = 'translateY(calc(-100% - ' + BOTTOM_OFFSET +
+            'px))';
+      } else {
+        tooltip.style.top = top + rect.height + BOTTOM_OFFSET + 'px';
+      }
+    }
+  }
+
+  return Mixin;
+});
+
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.js b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
similarity index 89%
rename from polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.js
rename to polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
index 1f144a0..589307d 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.js
+++ b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
@@ -16,12 +16,21 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {TooltipBehavior} from './gr-tooltip-behavior.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {TooltipMixin} from './gr-tooltip-mixin.js';
 
-const basicFixture = fixtureFromElement('tooltip-behavior-element');
+const basicFixture = fixtureFromElement('gr-tooltip-mixin-element');
 
-suite('gr-tooltip-behavior tests', () => {
+class GrTooltipMixinTestElement extends TooltipMixin(PolymerElement) {
+  static get is() {
+    return 'gr-tooltip-mixin-element';
+  }
+}
+
+customElements.define(GrTooltipMixinTestElement.is,
+    GrTooltipMixinTestElement);
+
+suite('gr-tooltip-mixin tests', () => {
   let element;
 
   function makeTooltip(tooltipRect, parentRect) {
@@ -35,14 +44,6 @@
     };
   }
 
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'tooltip-behavior-element',
-      behaviors: [TooltipBehavior],
-    });
-  });
-
   setup(() => {
     element = basicFixture.instantiate();
   });
diff --git a/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.js b/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.js
new file mode 100644
index 0000000..db482cf
--- /dev/null
+++ b/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.js
@@ -0,0 +1,25 @@
+/**
+ * @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 {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+
+// In .d.ts, the mixinBehaviors clears all type information about superClass.
+// As a workaround, we define IronFitMixin manually here and after conversion
+// to typescript we can define correct typing here as well.
+export const IronFitMixin = superClass => mixinBehaviors(
+    [IronFitBehavior], superClass);
diff --git a/polygerrit-ui/app/mixins/iron-overlay-mixin/iron-overlay-mixin.js b/polygerrit-ui/app/mixins/iron-overlay-mixin/iron-overlay-mixin.js
new file mode 100644
index 0000000..bdd5a4e
--- /dev/null
+++ b/polygerrit-ui/app/mixins/iron-overlay-mixin/iron-overlay-mixin.js
@@ -0,0 +1,25 @@
+/**
+ * @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 {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+
+// In .d.ts, the mixinBehaviors clears all type information about superClass.
+// As a workaround, we define IronOverlayMixin manually here and after
+// conversion to typescript we can define correct typing here as well.
+export const IronOverlayMixin = superClass => mixinBehaviors(
+    [IronOverlayBehavior], superClass);
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js
similarity index 88%
rename from polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
rename to polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js
index b525a82..75b0b00 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js
@@ -52,23 +52,23 @@
 
   // Ordinary shortcut with a single binding.
   this.bindShortcut(
-      this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
+      Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
 
   // Ordinary shortcut with multiple bindings.
   this.bindShortcut(
-      this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
+      Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
 
   // A "go-key" keyboard shortcut, which is combined with a previously and
   // continuously pressed "go" key (the go-key is hard-coded as 'g').
   this.bindShortcut(
-      this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
+      Shortcut.GO_TO_OPENED_CHANGES, SPECIAL_SHORTCUT.GO_KEY, 'o');
 
   // A "doc-only" keyboard shortcut. This declares the key-binding for help
   // dialog purposes, but doesn't actually implement the binding. It is up
   // to some element to implement this binding using iron-a11y-keys-behavior's
   // keyBindings property.
   this.bindShortcut(
-      this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
+      Shortcut.EXPAND_ALL_COMMENT_THREADS, SPECIAL_SHORTCUT.DOC_ONLY, 'e');
 
 Part (4), the listener definitions, are declared by the view or element that
 implements the shortcut behavior. This is done by implementing a method named
@@ -78,7 +78,7 @@
 
   keyboardShortcuts() {
     return {
-      [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
+      [Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
     };
   },
 
@@ -98,10 +98,14 @@
 
 import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {dedupingMixin} from '@polymer/polymer/lib/utils/mixin.js';
 
-const DOC_ONLY = 'DOC_ONLY';
-const GO_KEY = 'GO_KEY';
-const V_KEY = 'V_KEY';
+export const SPECIAL_SHORTCUT = {
+  DOC_ONLY: 'DOC_ONLY',
+  GO_KEY: 'GO_KEY',
+  V_KEY: 'V_KEY',
+};
 
 // The maximum age of a keydown event to be used in a jump navigation. This
 // is only for cases when the keyup event is lost.
@@ -109,7 +113,7 @@
 
 const V_KEY_TIMEOUT_MS = 1000;
 
-const ShortcutSection = {
+export const ShortcutSection = {
   ACTIONS: 'Actions',
   DIFFS: 'Diffs',
   EVERYWHERE: 'Everywhere',
@@ -118,7 +122,7 @@
   REPLY_DIALOG: 'Reply dialog',
 };
 
-const Shortcut = {
+export const Shortcut = {
   OPEN_SHORTCUT_HELP_DIALOG: 'OPEN_SHORTCUT_HELP_DIALOG',
   GO_TO_USER_DASHBOARD: 'GO_TO_USER_DASHBOARD',
   GO_TO_OPENED_CHANGES: 'GO_TO_OPENED_CHANGES',
@@ -341,7 +345,7 @@
   return e;
 };
 
-class ShortcutManager {
+export class ShortcutManager {
   constructor() {
     this.activeHosts = new Map();
     this.bindings = new Map();
@@ -462,20 +466,20 @@
   describeBindings(shortcut) {
     const bindings = this.bindings.get(shortcut);
     if (!bindings) { return null; }
-    if (bindings[0] === GO_KEY) {
+    if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
       return bindings.slice(1).map(
           binding => this._describeKey(binding)
       )
           .map(binding => ['g'].concat(binding));
     }
-    if (bindings[0] === V_KEY) {
+    if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
       return bindings.slice(1).map(
           binding => this._describeKey(binding)
       )
           .map(binding => ['v'].concat(binding));
     }
     return bindings
-        .filter(binding => binding !== DOC_ONLY)
+        .filter(binding => binding !== SPECIAL_SHORTCUT.DOC_ONLY)
         .map(binding => this.describeBinding(binding));
   }
 
@@ -519,47 +523,54 @@
 
 const shortcutManager = new ShortcutManager();
 
-/** @polymerBehavior Gerrit.KeyboardShortcutBehavior*/
-export const KeyboardShortcutBehavior = [
-  IronA11yKeysBehavior,
-  {
-    // Exports for convenience. Note: Closure compiler crashes when
-    // object-shorthand syntax is used here.
-    // eslint-disable-next-line object-shorthand
-    DOC_ONLY: DOC_ONLY,
-    // eslint-disable-next-line object-shorthand
-    GO_KEY: GO_KEY,
-    // eslint-disable-next-line object-shorthand
-    V_KEY: V_KEY,
-    // eslint-disable-next-line object-shorthand
-    Shortcut: Shortcut,
-    // eslint-disable-next-line object-shorthand
-    ShortcutSection: ShortcutSection,
-
-    properties: {
-      _shortcut_go_key_last_pressed: {
-        type: Number,
-        value: null,
-      },
-      _shortcut_go_table: {
-        type: Array,
-        value() { return new Map(); },
-      },
-      _shortcut_v_table: {
-        type: Array,
-        value() { return new Map(); },
-      },
-    },
+/**
+ * @polymer
+ * @mixinFunction
+ */
+const InternalKeyboardShortcutMixin = dedupingMixin(superClass => {
+  /**
+   * @polymer
+   * @mixinClass
+   */
+  class Mixin extends superClass {
+    static get properties() {
+      return {
+        _shortcut_go_key_last_pressed: {
+          type: Number,
+          value: null,
+        },
+        _shortcut_v_key_last_pressed: {
+          type: Number,
+          value: null,
+        },
+        _shortcut_go_table: {
+          type: Array,
+          value() {
+            return new Map();
+          },
+        },
+        _shortcut_v_table: {
+          type: Array,
+          value() {
+            return new Map();
+          },
+        },
+      };
+    }
 
     modifierPressed(e) {
+      /* We are checking for g/v as modifiers pressed. There are cases such as
+       * pressing v and then /, where we want the handler for / to be triggered.
+       * TODO(dhruvsri): find a way to support that keyboard combination
+       */
       e = getKeyboardEvent(e);
       return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey ||
         !!this._inGoKeyMode() || !!this._inVKeyMode();
-    },
+    }
 
     isModifierPressed(e, modifier) {
       return getKeyboardEvent(e)[modifier];
-    },
+    }
 
     shouldSuppressKeyboardShortcut(e) {
       e = getKeyboardEvent(e);
@@ -582,40 +593,40 @@
         composed: true, bubbles: true,
       }));
       return false;
-    },
+    }
 
     // Alias for getKeyboardEvent.
     /** @return {!Event} */
     getKeyboardEvent(e) {
       return getKeyboardEvent(e);
-    },
+    }
 
     getRootTarget(e) {
       return dom(getKeyboardEvent(e)).rootTarget;
-    },
+    }
 
     bindShortcut(shortcut, ...bindings) {
       shortcutManager.bindShortcut(shortcut, ...bindings);
-    },
+    }
 
     createTitle(shortcutName, section) {
       const desc = shortcutManager.getDescription(section, shortcutName);
       const shortcut = shortcutManager.getShortcut(shortcutName);
       return (desc && shortcut) ? `${desc} (shortcut: ${shortcut})` : '';
-    },
+    }
 
     _addOwnKeyBindings(shortcut, handler) {
       const bindings = shortcutManager.getBindingsForShortcut(shortcut);
       if (!bindings) {
         return;
       }
-      if (bindings[0] === DOC_ONLY) {
+      if (bindings[0] === SPECIAL_SHORTCUT.DOC_ONLY) {
         return;
       }
-      if (bindings[0] === GO_KEY) {
+      if (bindings[0] === SPECIAL_SHORTCUT.GO_KEY) {
         bindings.slice(1).forEach(binding =>
           this._shortcut_go_table.set(binding, handler));
-      } else if (bindings[0] === V_KEY) {
+      } else if (bindings[0] === SPECIAL_SHORTCUT.V_KEY) {
         // for each binding added with the go/v key, we set the handler to be
         // handleVKeyAction. handleVKeyAction then looks up in th
         // shortcut_table to see what the relevant handler should be
@@ -624,10 +635,15 @@
       } else {
         this.addOwnKeyBinding(bindings.join(' '), handler);
       }
-    },
+    }
+
+    ready() {
+      super.ready();
+    }
 
     /** @override */
-    attached() {
+    connectedCallback() {
+      super.connectedCallback();
       const shortcuts = shortcutManager.attachHost(this);
       if (!shortcuts) { return; }
 
@@ -655,42 +671,44 @@
           this.addOwnKeyBinding(key, '_handleVAction');
         });
       }
-    },
+    }
 
     /** @override */
-    detached() {
+    disconnectedCallback() {
+      super.disconnectedCallback();
       if (shortcutManager.detachHost(this)) {
         this.removeOwnKeyBindings();
       }
-    },
+    }
 
     keyboardShortcuts() {
       return {};
-    },
+    }
 
     addKeyboardShortcutDirectoryListener(listener) {
       shortcutManager.addListener(listener);
-    },
+    }
 
     removeKeyboardShortcutDirectoryListener(listener) {
       shortcutManager.removeListener(listener);
-    },
+    }
 
     _handleVKeyDown(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) return;
       this._shortcut_v_key_last_pressed = Date.now();
-    },
+    }
 
     _handleVKeyUp(e) {
       setTimeout(() => {
         this._shortcut_v_key_last_pressed = null;
       }, V_KEY_TIMEOUT_MS);
-    },
+    }
 
     _inVKeyMode() {
       return this._shortcut_v_key_last_pressed &&
           (Date.now() - this._shortcut_v_key_last_pressed <=
               V_KEY_TIMEOUT_MS);
-    },
+    }
 
     _handleVAction(e) {
       if (!this._inVKeyMode() ||
@@ -701,11 +719,12 @@
       e.preventDefault();
       const handler = this._shortcut_v_table.get(e.detail.key);
       this[handler](e);
-    },
+    }
 
     _handleGoKeyDown(e) {
+      if (this.shouldSuppressKeyboardShortcut(e)) return;
       this._shortcut_go_key_last_pressed = Date.now();
-    },
+    }
 
     _handleGoKeyUp(e) {
       // Set go_key_last_pressed to null `GO_KEY_TIMEOUT_MS` after keyup event
@@ -713,13 +732,13 @@
       setTimeout(() => {
         this._shortcut_go_key_last_pressed = null;
       }, GO_KEY_TIMEOUT_MS);
-    },
+    }
 
     _inGoKeyMode() {
       return this._shortcut_go_key_last_pressed &&
           (Date.now() - this._shortcut_go_key_last_pressed <=
               GO_KEY_TIMEOUT_MS);
-    },
+    }
 
     _handleGoAction(e) {
       if (!this._inGoKeyMode() ||
@@ -730,31 +749,24 @@
       e.preventDefault();
       const handler = this._shortcut_go_table.get(e.detail.key);
       this[handler](e);
-    },
-  },
-];
+    }
+  }
 
-export const KeyboardShortcutBinder = {
-  DOC_ONLY,
-  GO_KEY,
-  V_KEY,
-  Shortcut,
-  ShortcutManager,
-  ShortcutSection,
+  return Mixin;
+});
 
-  bindShortcut(shortcut, ...bindings) {
-    shortcutManager.bindShortcut(shortcut, ...bindings);
-  },
-};
+// The following doesn't work (IronA11yKeysBehavior crashes):
+// const KeyboardShortcutMixin = dedupingMixin(superClass => {
+//    class Mixin extends mixinBehaviors([IronA11yKeysBehavior], superClass) {
+//    ...
+//    }
+//    return Mixin;
+// }
+// This is a workaround
+export const KeyboardShortcutMixin = superClass =>
+  InternalKeyboardShortcutMixin(
+      mixinBehaviors([IronA11yKeysBehavior], superClass));
 
 export function _testOnly_getShortcutManagerInstance() {
   return shortcutManager;
 }
-
-// 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.KeyboardShortcutBehavior = KeyboardShortcutBehavior;
-window.Gerrit.KeyboardShortcutBinder = KeyboardShortcutBinder;
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.js b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
similarity index 74%
rename from polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.js
rename to polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
index 34cdf86..3ab7b6d 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.js
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin_test.js
@@ -16,39 +16,46 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {KeyboardShortcutBehavior, KeyboardShortcutBinder} from './keyboard-shortcut-behavior.js';
+import {
+  KeyboardShortcutMixin, Shortcut,
+  ShortcutManager, ShortcutSection, SPECIAL_SHORTCUT,
+} from './keyboard-shortcut-mixin.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
 const basicFixture =
-    fixtureFromElement('keyboard-shortcut-behavior-test-element');
+    fixtureFromElement('keyboard-shortcut-mixin-test-element');
 
 const withinOverlayFixture = fixtureFromTemplate(html`
 <gr-overlay>
-  <keyboard-shortcut-behavior-test-element>      
-  </keyboard-shortcut-behavior-test-element>
+  <keyboard-shortcut-mixin-test-element>      
+  </keyboard-shortcut-mixin-test-element>
 </gr-overlay>
 `);
 
-suite('keyboard-shortcut-behavior tests', () => {
-  const kb = KeyboardShortcutBinder;
+class GrKeyboardShortcutMixinTestElement extends
+  KeyboardShortcutMixin(PolymerElement) {
+  static get is() {
+    return 'keyboard-shortcut-mixin-test-element';
+  }
 
+  get keyBindings() {
+    return {
+      k: '_handleKey',
+      enter: '_handleKey',
+    };
+  }
+
+  _handleKey() {}
+}
+
+customElements.define(GrKeyboardShortcutMixinTestElement.is,
+    GrKeyboardShortcutMixinTestElement);
+
+suite('keyboard-shortcut-mixin tests', () => {
   let element;
   let overlay;
 
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'keyboard-shortcut-behavior-test-element',
-      behaviors: [KeyboardShortcutBehavior],
-      keyBindings: {
-        k: '_handleKey',
-        enter: '_handleKey',
-      },
-      _handleKey() {},
-    });
-  });
-
   setup(() => {
     element = basicFixture.instantiate();
     overlay = withinOverlayFixture.instantiate();
@@ -56,8 +63,8 @@
 
   suite('ShortcutManager', () => {
     test('bindings management', () => {
-      const mgr = new kb.ShortcutManager();
-      const {NEXT_FILE} = kb.Shortcut;
+      const mgr = new ShortcutManager();
+      const NEXT_FILE = Shortcut.NEXT_FILE;
 
       assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
       mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
@@ -74,7 +81,7 @@
       }
 
       test('single combo description', () => {
-        const mgr = new kb.ShortcutManager();
+        const mgr = new ShortcutManager();
         assert.deepEqual(mgr.describeBinding('a'), ['a']);
         assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
         assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
@@ -84,28 +91,27 @@
       });
 
       test('combo set description', () => {
-        const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
-        const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
-
         const mgr = new ShortcutManager();
-        assert.isNull(mgr.describeBindings(NEXT_FILE));
+        assert.isNull(mgr.describeBindings(Shortcut.NEXT_FILE));
 
-        mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
+        mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
+            SPECIAL_SHORTCUT.GO_KEY, 'o');
         assert.deepEqual(
-            mgr.describeBindings(GO_TO_OPENED_CHANGES),
+            mgr.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
             [['g', 'o']]);
 
-        mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
+        mgr.bindShortcut(Shortcut.NEXT_FILE, SPECIAL_SHORTCUT.DOC_ONLY,
+            ']', 'ctrl+shift+right:keyup');
         assert.deepEqual(
-            mgr.describeBindings(NEXT_FILE),
+            mgr.describeBindings(Shortcut.NEXT_FILE),
             [[']'], ['Ctrl', 'Shift', '→']]);
 
-        mgr.bindShortcut(PREV_FILE, '[');
-        assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
+        mgr.bindShortcut(Shortcut.PREV_FILE, '[');
+        assert.deepEqual(mgr.describeBindings(Shortcut.PREV_FILE), [['[']]);
       });
 
       test('combo set description width', () => {
-        const mgr = new kb.ShortcutManager();
+        const mgr = new ShortcutManager();
         assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
         assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
         assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
@@ -116,7 +122,7 @@
       });
 
       test('distribute shortcut help', () => {
-        const mgr = new kb.ShortcutManager();
+        const mgr = new ShortcutManager();
         assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
         assert.deepEqual(
             mgr.distributeBindingDesc([['g', 'o']]),
@@ -147,15 +153,11 @@
       });
 
       test('active shortcuts by section', () => {
-        const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
-            kb.Shortcut;
-        const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
-
-        const mgr = new kb.ShortcutManager();
-        mgr.bindShortcut(NEXT_FILE, ']');
-        mgr.bindShortcut(NEXT_LINE, 'j');
-        mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
-        mgr.bindShortcut(SEARCH, '/');
+        const mgr = new ShortcutManager();
+        mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
+        mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
+        mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES, 'g+o');
+        mgr.bindShortcut(Shortcut.SEARCH, '/');
 
         assert.deepEqual(
             mapToObject(mgr.activeShortcutsBySection()),
@@ -164,96 +166,91 @@
         mgr.attachHost({
           keyboardShortcuts() {
             return {
-              [NEXT_FILE]: null,
+              [Shortcut.NEXT_FILE]: null,
             };
           },
         });
         assert.deepEqual(
             mapToObject(mgr.activeShortcutsBySection()),
             {
-              [NAVIGATION]: [
-                {shortcut: NEXT_FILE, text: 'Go to next file'},
+              [ShortcutSection.NAVIGATION]: [
+                {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
               ],
             });
 
         mgr.attachHost({
           keyboardShortcuts() {
             return {
-              [NEXT_LINE]: null,
+              [Shortcut.NEXT_LINE]: null,
             };
           },
         });
         assert.deepEqual(
             mapToObject(mgr.activeShortcutsBySection()),
             {
-              [DIFFS]: [
-                {shortcut: NEXT_LINE, text: 'Go to next line'},
+              [ShortcutSection.DIFFS]: [
+                {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
               ],
-              [NAVIGATION]: [
-                {shortcut: NEXT_FILE, text: 'Go to next file'},
+              [ShortcutSection.NAVIGATION]: [
+                {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
               ],
             });
 
         mgr.attachHost({
           keyboardShortcuts() {
             return {
-              [SEARCH]: null,
-              [GO_TO_OPENED_CHANGES]: null,
+              [Shortcut.SEARCH]: null,
+              [Shortcut.GO_TO_OPENED_CHANGES]: null,
             };
           },
         });
         assert.deepEqual(
             mapToObject(mgr.activeShortcutsBySection()),
             {
-              [DIFFS]: [
-                {shortcut: NEXT_LINE, text: 'Go to next line'},
+              [ShortcutSection.DIFFS]: [
+                {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
               ],
-              [EVERYWHERE]: [
-                {shortcut: SEARCH, text: 'Search'},
+              [ShortcutSection.EVERYWHERE]: [
+                {shortcut: Shortcut.SEARCH, text: 'Search'},
                 {
-                  shortcut: GO_TO_OPENED_CHANGES,
+                  shortcut: Shortcut.GO_TO_OPENED_CHANGES,
                   text: 'Go to Opened Changes',
                 },
               ],
-              [NAVIGATION]: [
-                {shortcut: NEXT_FILE, text: 'Go to next file'},
+              [ShortcutSection.NAVIGATION]: [
+                {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
               ],
             });
       });
 
       test('directory view', () => {
-        const {
-          NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
-          SAVE_COMMENT,
-        } = kb.Shortcut;
-        const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
-        const {GO_KEY, ShortcutManager} = kb;
-
         const mgr = new ShortcutManager();
-        mgr.bindShortcut(NEXT_FILE, ']');
-        mgr.bindShortcut(NEXT_LINE, 'j');
-        mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
-        mgr.bindShortcut(SEARCH, '/');
+        mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
+        mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
+        mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
+            SPECIAL_SHORTCUT.GO_KEY, 'o');
+        mgr.bindShortcut(Shortcut.SEARCH, '/');
         mgr.bindShortcut(
-            SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
+            Shortcut.SAVE_COMMENT, 'ctrl+enter', 'meta+enter',
+            'ctrl+s', 'meta+s');
 
         assert.deepEqual(mapToObject(mgr.directoryView()), {});
 
         mgr.attachHost({
           keyboardShortcuts() {
             return {
-              [GO_TO_OPENED_CHANGES]: null,
-              [NEXT_FILE]: null,
-              [NEXT_LINE]: null,
-              [SAVE_COMMENT]: null,
-              [SEARCH]: null,
+              [Shortcut.GO_TO_OPENED_CHANGES]: null,
+              [Shortcut.NEXT_FILE]: null,
+              [Shortcut.NEXT_LINE]: null,
+              [Shortcut.SAVE_COMMENT]: null,
+              [Shortcut.SEARCH]: null,
             };
           },
         });
         assert.deepEqual(
             mapToObject(mgr.directoryView()),
             {
-              [DIFFS]: [
+              [ShortcutSection.DIFFS]: [
                 {binding: [['j']], text: 'Go to next line'},
                 {
                   binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
@@ -264,11 +261,11 @@
                   text: 'Save comment',
                 },
               ],
-              [EVERYWHERE]: [
+              [ShortcutSection.EVERYWHERE]: [
                 {binding: [['/']], text: 'Search'},
                 {binding: [['g', 'o']], text: 'Go to Opened Changes'},
               ],
-              [NAVIGATION]: [
+              [ShortcutSection.NAVIGATION]: [
                 {binding: [[']']], text: 'Go to next file'},
               ],
             });
@@ -309,7 +306,7 @@
   test('blocks kb shortcuts for anything in a gr-overlay', done => {
     const divEl = document.createElement('div');
     const element =
-        overlay.querySelector('keyboard-shortcut-behavior-test-element');
+        overlay.querySelector('keyboard-shortcut-mixin-test-element');
     element.appendChild(divEl);
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
@@ -321,7 +318,7 @@
   test('blocks enter shortcut on an anchor', done => {
     const anchorEl = document.createElement('a');
     const element =
-        overlay.querySelector('keyboard-shortcut-behavior-test-element');
+        overlay.querySelector('keyboard-shortcut-mixin-test-element');
     element.appendChild(anchorEl);
     element._handleKey = e => {
       assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
diff --git a/polygerrit-ui/app/node_modules_licenses/BUILD b/polygerrit-ui/app/node_modules_licenses/BUILD
index 92a3db8..7652ddc 100644
--- a/polygerrit-ui/app/node_modules_licenses/BUILD
+++ b/polygerrit-ui/app/node_modules_licenses/BUILD
@@ -1,4 +1,4 @@
-load("@npm_bazel_typescript//:index.bzl", "ts_library")
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
 load("//tools/node_tools/node_modules_licenses:node_modules_licenses.bzl", "node_modules_licenses")
 
 filegroup(
diff --git a/polygerrit-ui/app/polymer.json b/polygerrit-ui/app/polymer.json
index af927a0..11793e2 100644
--- a/polygerrit-ui/app/polymer.json
+++ b/polygerrit-ui/app/polymer.json
@@ -1,8 +1,8 @@
 {
   "shell": "elements/gr-app.js",
   "sources": [
-    "behaviors/**/*",
     "elements/**/*",
+    "mixins/**/*",
     "scripts/**/*",
     "styles/*",
     "types/**/*"
diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
index d83f24f..db0e2f7 100644
--- a/polygerrit-ui/app/rollup.config.js
+++ b/polygerrit-ui/app/rollup.config.js
@@ -60,12 +60,6 @@
 export default {
   treeshake: false,
   onwarn: warning => {
-    if(warning.code === 'CIRCULAR_DEPENDENCY') {
-      // Temporary allow CIRCULAR_DEPENDENCY.
-      // See https://bugs.chromium.org/p/gerrit/issues/detail?id=12090
-      // Delete this code after bug is fixed.
-      return;
-    }
     // No warnings from rollupjs are allowed.
     // Most of the warnings are real error in our code (for example,
     // if some import couldn't be resolved we can't continue, but rollup
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 74b9ac1..8d9be62 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -1,5 +1,5 @@
 load("//tools/bzl:genrule2.bzl", "genrule2")
-load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
+load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
 
 def _get_ts_compiled_path(outdir, file_name):
     """Calculates the typescript output path for a file_name.
diff --git a/polygerrit-ui/app/samples/bind-parameters.js b/polygerrit-ui/app/samples/bind-parameters.js
index 2c89064..30c7c3d 100644
--- a/polygerrit-ui/app/samples/bind-parameters.js
+++ b/polygerrit-ui/app/samples/bind-parameters.js
@@ -51,7 +51,7 @@
   }
 
   _onRevisionChanged(value) {
-    console.log(`(attributeHelper.bind) revision number: ${value._number}`);
+    console.info(`(attributeHelper.bind) revision number: ${value._number}`);
   }
 }
 
diff --git a/polygerrit-ui/app/samples/custom-wip-requirement.js b/polygerrit-ui/app/samples/custom-wip-requirement.js
new file mode 100644
index 0000000..1d2663c
--- /dev/null
+++ b/polygerrit-ui/app/samples/custom-wip-requirement.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.
+ */
+
+/**
+ * This plugin will add a text next to WIP requirement if shown.
+ */
+class WipRequirementValue extends Polymer.Element {
+  static get is() {
+    return 'wip-requirement-value';
+  }
+
+  static get template() {
+    return Polymer.html`
+        <style include="shared-styles">
+        :host {
+          color: var(--deemphasized-text-color);
+        }
+        </style>
+        <span>Will be removed once active.</span>
+      `;
+  }
+
+  static get properties() {
+    return {
+      change: Object,
+      requirement: Object,
+    };
+  }
+}
+
+customElements.define(WipRequirementValue.is, WipRequirementValue);
+
+Gerrit.install(plugin => {
+  plugin.registerCustomComponent(
+      'submit-requirement-item-wip', WipRequirementValue.is, {slot: 'value'});
+});
\ 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 5aaea30..4f64059 100644
--- a/polygerrit-ui/app/samples/repo-command.js
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -50,8 +50,8 @@
 
   connectedCallback() {
     super.connectedCallback();
-    console.log(this.repoName);
-    console.log(this.config);
+    console.info(this.repoName);
+    console.info(this.config);
     this.hidden = this.repoName !== 'All-Projects';
   }
 
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js b/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
deleted file mode 100644
index 62dc3ee..0000000
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils.js
+++ /dev/null
@@ -1,71 +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.
- */
-const ANONYMOUS_NAME = 'Anonymous';
-
-export class GrDisplayNameUtils {
-  static getUserName(config, account) {
-    if (account && account.name) {
-      return account.name;
-    } else if (account && account.username) {
-      return account.username;
-    } else if (account && account.email) {
-      return account.email;
-    } else if (config && config.user &&
-        config.user.anonymous_coward_name !== 'Anonymous Coward') {
-      return config.user.anonymous_coward_name;
-    }
-
-    return ANONYMOUS_NAME;
-  }
-
-  static getDisplayName(config, account) {
-    if (account && account.display_name) {
-      return account.display_name;
-    }
-    if (!account || !account.name || !config || !config.accounts) {
-      return this.getUserName(config, account);
-    }
-    if (config.accounts.default_display_name === 'USERNAME'
-        && account.username) {
-      return account.username;
-    }
-    if (config.accounts.default_display_name === 'FIRST_NAME') {
-      return account.name.trim().split(' ')[0];
-    }
-    // Treat every other value as FULL_NAME.
-    return account.name;
-  }
-
-  static getAccountDisplayName(config, account) {
-    const reviewerName = this.getUserName(config, account);
-    const reviewerEmail = this._accountEmail(account.email);
-    const reviewerStatus = account.status ? '(' + account.status + ')' : '';
-    return [reviewerName, reviewerEmail, reviewerStatus]
-        .filter(p => p.length > 0).join(' ');
-  }
-
-  static _accountEmail(email) {
-    if (typeof email !== 'undefined') {
-      return '<' + email + '>';
-    }
-    return '';
-  }
-
-  static getGroupDisplayName(group) {
-    return group.name + ' (group)';
-  }
-}
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
index 2d2deac..248217c 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider.js
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GrDisplayNameUtils} from '../gr-display-name-utils/gr-display-name-utils.js';
+import {getAccountDisplayName} from '../../utils/display-name-util.js';
 
 export class GrEmailSuggestionsProvider {
   constructor(restAPI) {
@@ -31,7 +31,7 @@
 
   makeSuggestionItem(account) {
     return {
-      name: GrDisplayNameUtils.getAccountDisplayName(null, account),
+      name: getAccountDisplayName(null, account),
       value: {account, count: 1},
     };
   }
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
index 16b6aae..ae63c56 100644
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider.js
@@ -25,7 +25,7 @@
         .then(groups => {
           if (!groups) { return []; }
           const keys = Object.keys(groups);
-          return keys.map(key => Object.assign({}, groups[key], {name: key}));
+          return keys.map(key => { return {...groups[key], name: key}; });
         });
   }
 
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
index 3a47ed3..1bbf1b0 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {GrDisplayNameUtils} from '../gr-display-name-utils/gr-display-name-utils.js';
+import {getAccountDisplayName, getGroupDisplayName} from '../../utils/display-name-util.js';
 
 /**
  * @enum {string}
@@ -80,7 +80,7 @@
     if (suggestion.account) {
       // Reviewer is an account suggestion from getChangeSuggestedReviewers.
       return {
-        name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+        name: getAccountDisplayName(this._config,
             suggestion.account),
         value: suggestion,
       };
@@ -89,7 +89,7 @@
     if (suggestion.group) {
       // Reviewer is a group suggestion from getChangeSuggestedReviewers.
       return {
-        name: GrDisplayNameUtils.getGroupDisplayName(suggestion.group),
+        name: getGroupDisplayName(suggestion.group),
         value: suggestion,
       };
     }
@@ -97,7 +97,7 @@
     if (suggestion._account_id) {
       // Reviewer is an account suggestion from getSuggestedAccounts.
       return {
-        name: GrDisplayNameUtils.getAccountDisplayName(this._config,
+        name: getAccountDisplayName(this._config,
             suggestion),
         value: {account: suggestion, count: 1},
       };
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
index fba4b26..fe13c1c 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
@@ -17,7 +17,6 @@
 
 import '../../test/common-test-setup-karma.js';
 import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {GrDisplayNameUtils} from '../gr-display-name-utils/gr-display-name-utils.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from './gr-reviewer-suggestions-provider.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
@@ -164,8 +163,7 @@
           value: {account, count: 1},
         });
 
-        sinon.stub(GrDisplayNameUtils, '_accountEmail').callsFake(
-            () => '');
+        account3.email = undefined;
 
         suggestion = provider.makeSuggestionItem(account3);
         assert.deepEqual(suggestion, {
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
index fa6a44bf..531c361 100644
--- a/polygerrit-ui/app/services/app-context-init.js
+++ b/polygerrit-ui/app/services/app-context-init.js
@@ -18,6 +18,7 @@
 import {FlagsService} from './flags.js';
 import {GrReporting} from './gr-reporting/gr-reporting.js';
 import {EventEmitter} from './gr-event-interface/gr-event-interface.js';
+import {Auth} from './gr-auth.js';
 
 const initializedServices = new Map();
 
@@ -48,5 +49,6 @@
   addService('reportingService',
       () => new GrReporting(appContext.flagsService));
   addService('eventEmitter', () => new EventEmitter());
+  addService('authService', () => new Auth(appContext.eventEmitter));
   Object.defineProperties(appContext, registeredServices);
 }
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.js
index d82750e..3f86003 100644
--- a/polygerrit-ui/app/services/app-context.js
+++ b/polygerrit-ui/app/services/app-context.js
@@ -25,4 +25,5 @@
   flagsService: null,
   reportingService: null,
   eventEmitter: null,
+  authService: null,
 };
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js b/polygerrit-ui/app/services/gr-auth.js
similarity index 89%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
rename to polygerrit-ui/app/services/gr-auth.js
index 5fcd1a4..21081cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth.js
+++ b/polygerrit-ui/app/services/gr-auth.js
@@ -14,8 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {getBaseUrl} from '../../../utils/url-util.js';
-import {appContext} from '../../../services/app-context.js';
+import {getBaseUrl} from '../utils/url-util.js';
 
 const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s
 const MAX_GET_TOKEN_RETRIES = 2;
@@ -24,9 +23,7 @@
  * Auth class.
  */
 export class Auth {
-  // TODO(taoalpha): this whole thing should be moved to a service
-
-  constructor() {
+  constructor(eventEmitter) {
     this._type = null;
     this._cachedTokenPromise = null;
     this._defaultOptions = {};
@@ -34,7 +31,7 @@
     this._status = Auth.STATUS.UNDETERMINED;
     this._authCheckPromise = null;
     this._last_auth_check_time = Date.now();
-    this.eventEmitter = appContext.eventEmitter;
+    this.eventEmitter = eventEmitter;
   }
 
   get baseUrl() {
@@ -137,9 +134,11 @@
    * @return {!Promise<!Response>}
    */
   fetch(url, opt_options) {
-    const options = Object.assign({
+    const options = {
       headers: new Headers(),
-    }, this._defaultOptions, opt_options);
+      ...this._defaultOptions,
+      ...opt_options,
+    };
     if (this._type === Auth.TYPE.ACCESS_TOKEN) {
       return this._getAccessToken().then(
           accessToken =>
@@ -159,6 +158,7 @@
         result = c.substring(key.length);
         return true;
       }
+      return false;
     });
     return result;
   }
@@ -263,12 +263,3 @@
 };
 
 Auth.CREDS_EXPIRED_MSG = 'Credentials expired.';
-// TODO(dmfilippov) move to appContext
-export const authService = new Auth();
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use global Auth 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.Auth = authService;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js b/polygerrit-ui/app/services/gr-auth_test.js
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
rename to polygerrit-ui/app/services/gr-auth_test.js
index efdd4d1..541cd42 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.js
+++ b/polygerrit-ui/app/services/gr-auth_test.js
@@ -15,22 +15,22 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import {Auth, authService} from './gr-auth.js';
-import {appContext} from '../../../services/app-context.js';
-import {stubBaseUrl} from '../../../test/test-utils.js';
+import '../test/common-test-setup-karma.js';
+import {Auth} from './gr-auth.js';
+import {appContext} from './app-context.js';
+import {stubBaseUrl} from '../test/test-utils.js';
 
 suite('gr-auth', () => {
   let auth;
 
   setup(() => {
-    auth = authService;
+    auth = appContext.authService;
   });
 
   suite('Auth class methods', () => {
     let fakeFetch;
     setup(() => {
-      auth = new Auth();
+      auth = new Auth(appContext.eventEmitter);
       fakeFetch = sinon.stub(window, 'fetch');
     });
 
@@ -75,7 +75,7 @@
     let fakeFetch;
     let clock;
     setup(() => {
-      auth = new Auth();
+      auth = new Auth(appContext.eventEmitter);
       clock = sinon.useFakeTimers();
       fakeFetch = sinon.stub(window, 'fetch');
     });
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
index 42d112e..ea69d5f 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
@@ -286,9 +286,9 @@
     if (opt_noLog) { return; }
     if (type !== ERROR.TYPE) {
       if (value !== undefined) {
-        console.log(`Reporting: ${name}: ${value}`);
+        console.info(`Reporting: ${name}: ${value}`);
       } else {
-        console.log(`Reporting: ${name}`);
+        console.info(`Reporting: ${name}`);
       }
     }
   }
@@ -651,4 +651,4 @@
   }
 }
 
-export const DEFAULT_STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
+export const DEFAULT_STARTUP_TIMERS = {...STARTUP_TIMERS};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
index 01ba3cb..1e50766 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
@@ -29,7 +29,7 @@
   setup(() => {
     clock = sinon.useFakeTimers(NOW_TIME);
     service = new GrReporting(appContext.flagsService);
-    service._baselines = Object.assign({}, DEFAULT_STARTUP_TIMERS);
+    service._baselines = {...DEFAULT_STARTUP_TIMERS};
     sinon.stub(service, 'reporter');
   });
 
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.js b/polygerrit-ui/app/styles/dashboard-header-styles.ts
similarity index 88%
rename from polygerrit-ui/app/styles/dashboard-header-styles.js
rename to polygerrit-ui/app/styles/dashboard-header-styles.ts
index 683202e..2354f65 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.js
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="dashboard-header-styles">
@@ -55,4 +61,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.js b/polygerrit-ui/app/styles/gr-change-list-styles.ts
similarity index 95%
rename from polygerrit-ui/app/styles/gr-change-list-styles.js
rename to polygerrit-ui/app/styles/gr-change-list-styles.ts
index a7f231b..25d7f52 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-list-styles">
@@ -190,4 +196,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
similarity index 89%
rename from polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
rename to polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
index aabdde5..3d07d2e 100644
--- a/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-metadata-shared-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-shared-styles">
@@ -55,4 +61,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
similarity index 91%
rename from polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
rename to polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
index 4bfb742..57c8d78 100644
--- a/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.js
+++ b/polygerrit-ui/app/styles/gr-change-view-integration-shared-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-view-integration-shared-styles">
@@ -70,4 +76,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-form-styles.js b/polygerrit-ui/app/styles/gr-form-styles.ts
similarity index 94%
rename from polygerrit-ui/app/styles/gr-form-styles.js
rename to polygerrit-ui/app/styles/gr-form-styles.ts
index 91763c5..3284ad5 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.js
+++ b/polygerrit-ui/app/styles/gr-form-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-form-styles">
@@ -124,4 +130,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.js b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
similarity index 91%
rename from polygerrit-ui/app/styles/gr-menu-page-styles.js
rename to polygerrit-ui/app/styles/gr-menu-page-styles.ts
index e52a895..8e8b264 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.js
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-menu-page-styles">
@@ -79,4 +85,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.js b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
similarity index 91%
rename from polygerrit-ui/app/styles/gr-page-nav-styles.js
rename to polygerrit-ui/app/styles/gr-page-nav-styles.ts
index 97f1a03..9010b2d 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.js
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-page-nav-styles">
@@ -72,4 +78,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.js b/polygerrit-ui/app/styles/gr-subpage-styles.ts
similarity index 85%
rename from polygerrit-ui/app/styles/gr-subpage-styles.js
rename to polygerrit-ui/app/styles/gr-subpage-styles.ts
index f94cc9c..640da66 100644
--- a/polygerrit-ui/app/styles/gr-subpage-styles.js
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-subpage-styles">
@@ -42,4 +48,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-table-styles.js b/polygerrit-ui/app/styles/gr-table-styles.ts
similarity index 94%
rename from polygerrit-ui/app/styles/gr-table-styles.js
rename to polygerrit-ui/app/styles/gr-table-styles.ts
index ceac675..52fdc67 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.js
+++ b/polygerrit-ui/app/styles/gr-table-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-table-styles">
@@ -116,4 +122,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/gr-voting-styles.js b/polygerrit-ui/app/styles/gr-voting-styles.ts
similarity index 86%
rename from polygerrit-ui/app/styles/gr-voting-styles.js
rename to polygerrit-ui/app/styles/gr-voting-styles.ts
index 60bf623..d4e6d52 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.js
+++ b/polygerrit-ui/app/styles/gr-voting-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-voting-styles">
@@ -40,4 +46,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/shared-styles.js b/polygerrit-ui/app/styles/shared-styles.ts
similarity index 95%
rename from polygerrit-ui/app/styles/shared-styles.js
rename to polygerrit-ui/app/styles/shared-styles.ts
index 3e81761..04dca9c 100644
--- a/polygerrit-ui/app/styles/shared-styles.js
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="shared-styles">
@@ -127,7 +133,7 @@
         --iron-icon-width: 20px;
       }
 
-      /* Stopgap solution until we remove hidden\$ attributes. */
+      /* Stopgap solution until we remove hidden$ attributes. */
 
       [hidden] {
         display: none !important;
@@ -196,4 +202,3 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
diff --git a/polygerrit-ui/app/styles/themes/app-theme.js b/polygerrit-ui/app/styles/themes/app-theme.ts
similarity index 97%
rename from polygerrit-ui/app/styles/themes/app-theme.js
rename to polygerrit-ui/app/styles/themes/app-theme.ts
index 1a8296f..f48e43f 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.js
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// Mark the file as a module. Otherwise typescript assumes this is a script
+// and $_documentContainer is a global variable.
+// See: https://www.typescriptlang.org/docs/handbook/modules.html
+export {};
+
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `
@@ -223,4 +229,4 @@
   }
 </style></custom-style>`;
 
-document.head.appendChild($_documentContainer.content);
\ No newline at end of file
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.js b/polygerrit-ui/app/styles/themes/dark-theme.ts
similarity index 100%
rename from polygerrit-ui/app/styles/themes/dark-theme.js
rename to polygerrit-ui/app/styles/themes/dark-theme.ts
diff --git a/polygerrit-ui/app/template_test.sh b/polygerrit-ui/app/template_test.sh
deleted file mode 100755
index d42f23f..0000000
--- a/polygerrit-ui/app/template_test.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-# TODO(dmfilippov): Update template_test to support Polymer 2/Polymer 3 or delete it completely
-# The following line temporary disable template tests. Existing implementation doesn't compatible
-# with Polymer 2 & 3 class-based components. Polymer linter makes some checks regarding
-# templates and binding, but not all.
-exit 0
-
-set -ex
-
-node_bin=$(which node) && true
-if [ -z "$node_bin" ]; then
-    echo "node must be on the path."
-    exit 1
-fi
-
-npm_bin=$(which npm) && true
-if [[ -z "$npm_bin" ]]; then
-    echo "NPM must be on the path. (https://www.npmjs.com/)"
-    exit 1
-fi
-
-# Have to find where node_modules are installed and set the NODE_PATH
-
-get_node_path() {
-    cd $(dirname $node_bin)
-    cd ../lib/node_modules
-    pwd
-}
-
-export NODE_PATH=$(get_node_path)
-
-unzip -o polygerrit-ui/polygerrit_components.bower_components.zip -d polygerrit-ui/app
-python $TEST_SRCDIR/gerrit/polygerrit-ui/app/template_test_srcs/convert_for_template_tests.py
-# Pass a file name argument from the --test_args (example: --test_arg=gr-list-view)
-${node_bin} $TEST_SRCDIR/gerrit/polygerrit-ui/app/template_test_srcs/template_test.js $1 $2
diff --git a/polygerrit-ui/app/template_test_srcs/convert_for_template_tests.py b/polygerrit-ui/app/template_test_srcs/convert_for_template_tests.py
deleted file mode 100644
index 579e783..0000000
--- a/polygerrit-ui/app/template_test_srcs/convert_for_template_tests.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import json
-import os
-import re
-
-polymerRegex = r"Polymer\({"
-polymerCompiledRegex = re.compile(polymerRegex)
-
-removeSelfInvokeRegex = r"\(function\(\) {\n(.+)}\)\(\);"
-fnCompiledRegex = re.compile(removeSelfInvokeRegex, re.DOTALL)
-
-regexBehavior = r"<script>(.+)<\/script>"
-behaviorCompiledRegex = re.compile(regexBehavior, re.DOTALL)
-
-
-def _open(filename, mode="r"):
-    try:
-        return open(filename, mode, encoding="utf-8")
-    except TypeError:
-        return open(filename, mode)
-
-
-def replaceBehaviorLikeHTML(fileIn, fileOut):
-    with _open(fileIn) as f:
-        file_str = f.read()
-        match = behaviorCompiledRegex.search(file_str)
-        if match:
-            with _open("polygerrit-ui/temp/behaviors/" +
-                       fileOut.replace("html", "js"), "w+") as f:
-                f.write(match.group(1))
-
-
-def replaceBehaviorLikeJS(fileIn, fileOut):
-    with _open(fileIn) as f:
-        file_str = f.read()
-        with _open("polygerrit-ui/temp/behaviors/" + fileOut, "w+") as f:
-            f.write(file_str)
-
-
-def generateStubBehavior(behaviorName):
-    with _open("polygerrit-ui/temp/behaviors/" +
-               behaviorName + ".js", "w+") as f:
-        f.write("/** @polymerBehavior **/\n" + behaviorName + "= {};")
-
-
-def replacePolymerElement(fileIn, fileOut, root):
-    with _open(fileIn) as f:
-        key = fileOut.split('.')[0]
-        # Removed self invoked function
-        file_str = f.read()
-        file_str_no_fn = fnCompiledRegex.search(file_str)
-
-        if file_str_no_fn:
-            package = root.replace("/", ".") + "." + fileOut
-
-            with _open("polygerrit-ui/temp/" + fileOut, "w+") as f:
-                mainFileContents = re.sub(
-                    polymerCompiledRegex,
-                    "exports = Polymer({",
-                    file_str_no_fn.group(1)).replace("'use strict';", "")
-                f.write("/** \n"
-                        "* @fileoverview \n"
-                        "* @suppress {missingProperties} \n"
-                        "*/ \n\n"
-                        "goog.module('polygerrit." + package + "')\n\n" +
-                        mainFileContents)
-
-            # Add package and javascript to files object.
-            elements[key]["js"] = "polygerrit-ui/temp/" + fileOut
-            elements[key]["package"] = package
-
-
-def writeTempFile(file, root):
-    # This is included in an extern because it is directly on the window object
-    # (for now at least).
-    if "gr-reporting" in file:
-        return
-    key = file.split('.')[0]
-    if key not in elements:
-        # gr-app doesn't have an additional level
-        elements[key] = {
-            "directory":
-                'gr-app' if len(root.split("/")) < 4 else root.split("/")[3]
-        }
-    if file.endswith(".html") and not file.endswith("_test.html"):
-        # gr-navigation is treated like a behavior rather than a standard
-        # element because of the way it added to the Gerrit object.
-        if file.endswith("gr-navigation.html"):
-            replaceBehaviorLikeHTML(os.path.join(root, file), file)
-        else:
-            elements[key]["html"] = os.path.join(root, file)
-    if file.endswith(".js"):
-        replacePolymerElement(os.path.join(root, file), file, root)
-
-
-if __name__ == "__main__":
-    # Create temp directory.
-    if not os.path.exists("polygerrit-ui/temp"):
-        os.makedirs("polygerrit-ui/temp")
-
-    # Within temp directory create behavior directory.
-    if not os.path.exists("polygerrit-ui/temp/behaviors"):
-        os.makedirs("polygerrit-ui/temp/behaviors")
-
-    elements = {}
-
-    # Go through every file in app/elements, and re-write accordingly to temp
-    # directory, and also added to elements object, which is used to generate a
-    # map of html files, package names, and javascript files.
-    for root, dirs, files in os.walk("polygerrit-ui/app/elements"):
-        for file in files:
-            writeTempFile(file, root)
-
-    # Special case for polymer behaviors we are using.
-    replaceBehaviorLikeHTML("polygerrit-ui/app/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html", "iron-a11y-keys-behavior.html")
-    generateStubBehavior("Polymer.IronOverlayBehavior")
-    generateStubBehavior("Polymer.IronFitBehavior")
-
-    # TODO figure out something to do with iron-overlay-behavior.
-    # it is hard-coded reformatted.
-
-    with _open("polygerrit-ui/temp/map.json", "w+") as f:
-        f.write(json.dumps(elements))
-
-    for root, dirs, files in os.walk("polygerrit-ui/app/behaviors"):
-        for file in files:
-            if file.endswith("behavior.html"):
-                replaceBehaviorLikeHTML(os.path.join(root, file), file)
-            elif file.endswith("behavior.js"):
-                replaceBehaviorLikeJS(os.path.join(root, file), file)
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
deleted file mode 100644
index 5592825..0000000
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ /dev/null
@@ -1,87 +0,0 @@
-const fs = require('fs');
-const twinkie = require('fried-twinkie');
-
-fs.readdir('./polygerrit-ui/temp/behaviors/', (err, data) => {
-  if (err) {
-    console.log('error /polygerrit-ui/temp/behaviors/ directory');
-  }
-  const behaviors = data;
-  const additionalSources = [];
-  const externMap = {};
-
-  for (const behavior of behaviors) {
-    if (!externMap[behavior]) {
-      additionalSources.push({
-        path: `./polygerrit-ui/temp/behaviors/${behavior}`,
-        src: fs.readFileSync(
-            `./polygerrit-ui/temp/behaviors/${behavior}`, 'utf-8'),
-      });
-      externMap[behavior] = true;
-    }
-  }
-
-  let mappings = JSON.parse(fs.readFileSync(
-      `./polygerrit-ui/temp/map.json`, 'utf-8'));
-
-  // The directory is passed as arg2 by the test target.
-  const directory = process.argv[2];
-  if (directory) {
-    const mappingSpecificDirectory = {};
-
-    for (key of Object.keys(mappings)) {
-      if (directory === mappings[key].directory) {
-        mappingSpecificDirectory[key] = mappings[key];
-      }
-    }
-    mappings = mappingSpecificDirectory;
-  }
-
-  // If a particular file was passed by the user, don't test everything.
-  const file = process.argv[3];
-  if (file) {
-    const mappingSpecificFile = {};
-    for (key of Object.keys(mappings)) {
-      if (key.includes(file)) {
-        mappingSpecificFile[key] = mappings[key];
-      }
-    }
-    mappings = mappingSpecificFile;
-  }
-
-  /**
-   * Types in Gerrit.
-   * All types should be under `./polygerrit-ui/app/types` folder and end with `js`.
-   */
-  fs.readdir('./polygerrit-ui/app/types/', (err, typeFiles) => {
-    for (const typeFile of typeFiles) {
-      if (!typeFile.endsWith('.js')) continue;
-      additionalSources.push({
-        path: `./polygerrit-ui/app/types/${typeFile}`,
-        src: fs.readFileSync(
-            `./polygerrit-ui/app/types/${typeFile}`, 'utf-8'),
-      });
-    }
-
-    const toCheck = [];
-    for (key of Object.keys(mappings)) {
-      if (mappings[key].html && mappings[key].js) {
-        toCheck.push({
-          htmlSrcPath: mappings[key].html,
-          jsSrcPath: mappings[key].js,
-          jsModule: 'polygerrit.' + mappings[key].package,
-        });
-      }
-    }
-
-    twinkie.checkTemplate(toCheck, additionalSources)
-        .then(() => {}, joinedErrors => {
-          if (joinedErrors) {
-            process.exit(1);
-          }
-        })
-        .catch(e => {
-          console.error(e);
-          process.exit(1);
-        });
-  });
-});
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 9ac05f9..19465a3 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -22,14 +22,14 @@
 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 {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api.js';
 import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {cleanupTestUtils, TestKeyboardShortcutBinder} from './test-utils.js';
 import {flushDebouncers} from '@polymer/polymer/lib/utils/debounce';
-import {_testOnly_getShortcutManagerInstance} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import sinon from 'sinon/pkg/sinon-esm.js';
+import {safeTypesBridge} from '../utils/safe-types-util.js';
 window.sinon = sinon;
 
 security.polymer_resin.install({
@@ -45,7 +45,7 @@
         JSON.stringify(args));
     }
   },
-  safeTypesBridge: SafeTypes.safeTypesBridge,
+  safeTypesBridge,
 });
 
 const cleanups = [];
diff --git a/polygerrit-ui/app/test/test-utils.js b/polygerrit-ui/app/test/test-utils.js
index 5be9850..e1eadef 100644
--- a/polygerrit-ui/app/test/test-utils.js
+++ b/polygerrit-ui/app/test/test-utils.js
@@ -18,7 +18,7 @@
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
 import {testOnly_resetInternalState} from '../elements/shared/gr-js-api-interface/gr-api-utils.js';
 import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {KeyboardShortcutBinder, _testOnly_getShortcutManagerInstance} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 
 export const mockPromise = () => {
   let res;
@@ -40,7 +40,7 @@
     }
     const testBinder = new TestKeyboardShortcutBinder();
     this.stack.push(testBinder);
-    return KeyboardShortcutBinder;
+    return _testOnly_getShortcutManagerInstance();
   }
 
   static pop() {
@@ -78,6 +78,7 @@
 
 export function cleanupTestUtils() {
   cleanups.forEach(cleanup => cleanup());
+  cleanups.splice(0);
 }
 
 export function stubBaseUrl(newUrl) {
@@ -85,3 +86,40 @@
   window.CANONICAL_PATH = newUrl;
   registerTestCleanup(() => window.CANONICAL_PATH = originalCanonicalPath);
 }
+
+export function generateChange(options) {
+  const change = {
+    _number: 42,
+  };
+  const revisionIdStart = 1;
+  const messageIdStart = 1000;
+  // We want to distinguish between empty arrays/objects and undefined
+  // If an option is not set - the appropriate property is not set
+  // If an options is set - the property always set
+  if (typeof options.revisionsCount !== 'undefined') {
+    const revisions = {};
+    for (let i = 0; i < options.revisionsCount; i++) {
+      const revisionId = (i + revisionIdStart).toString(16);
+      revisions[revisionId] = {
+        _number: i+1,
+        commit: {parents: []},
+      };
+    }
+    change.revisions = revisions;
+  }
+  if (typeof options.messagesCount !== 'undefined') {
+    const messages = [];
+    for (let i = 0; i < options.messagesCount; i++) {
+      messages.push({
+        id: (i + messageIdStart).toString(16),
+        date: new Date(2020, 1, 1),
+        message: `This is a message N${i + 1}`,
+      });
+    }
+    change.messages = messages;
+  }
+  if (options.status) {
+    change.status = options.status;
+  }
+  return change;
+}
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index e45cbad..efe575a 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -44,11 +44,11 @@
   // Note: gerrit doesn't have .tsx and .jsx files
   "include": [
     // This items below must be in sync with the src_dirs list in the BUILD file
-    "behaviors/**/*",
     "constants/**/*",
     "elements/**/*",
     "embed/**/*",
     "gr-diff/**/*",
+    "mixins/**/*",
     "samples/**/*",
     "scripts/**/*",
     "services/**/*",
diff --git a/polygerrit-ui/app/utils/access-util.js b/polygerrit-ui/app/utils/access-util.js
new file mode 100644
index 0000000..2578cfa
--- /dev/null
+++ b/polygerrit-ui/app/utils/access-util.js
@@ -0,0 +1,143 @@
+/**
+ * @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.
+ */
+
+export const AccessPermissions = {
+  abandon: {
+    id: 'abandon',
+    name: 'Abandon',
+  },
+  addPatchSet: {
+    id: 'addPatchSet',
+    name: 'Add Patch Set',
+  },
+  create: {
+    id: 'create',
+    name: 'Create Reference',
+  },
+  createTag: {
+    id: 'createTag',
+    name: 'Create Annotated Tag',
+  },
+  createSignedTag: {
+    id: 'createSignedTag',
+    name: 'Create Signed Tag',
+  },
+  delete: {
+    id: 'delete',
+    name: 'Delete Reference',
+  },
+  deleteChanges: {
+    id: 'deleteChanges',
+    name: 'Delete Changes',
+  },
+  deleteOwnChanges: {
+    id: 'deleteOwnChanges',
+    name: 'Delete Own Changes',
+  },
+  editAssignee: {
+    id: 'editAssignee',
+    name: 'Edit Assignee',
+  },
+  editHashtags: {
+    id: 'editHashtags',
+    name: 'Edit Hashtags',
+  },
+  editTopicName: {
+    id: 'editTopicName',
+    name: 'Edit Topic Name',
+  },
+  forgeAuthor: {
+    id: 'forgeAuthor',
+    name: 'Forge Author Identity',
+  },
+  forgeCommitter: {
+    id: 'forgeCommitter',
+    name: 'Forge Committer Identity',
+  },
+  forgeServerAsCommitter: {
+    id: 'forgeServerAsCommitter',
+    name: 'Forge Server Identity',
+  },
+  owner: {
+    id: 'owner',
+    name: 'Owner',
+  },
+  publishDrafts: {
+    id: 'publishDrafts',
+    name: 'Publish Drafts',
+  },
+  push: {
+    id: 'push',
+    name: 'Push',
+  },
+  pushMerge: {
+    id: 'pushMerge',
+    name: 'Push Merge Commit',
+  },
+  read: {
+    id: 'read',
+    name: 'Read',
+  },
+  rebase: {
+    id: 'rebase',
+    name: 'Rebase',
+  },
+  revert: {
+    id: 'revert',
+    name: 'Revert',
+  },
+  removeReviewer: {
+    id: 'removeReviewer',
+    name: 'Remove Reviewer',
+  },
+  submit: {
+    id: 'submit',
+    name: 'Submit',
+  },
+  submitAs: {
+    id: 'submitAs',
+    name: 'Submit (On Behalf Of)',
+  },
+  toggleWipState: {
+    id: 'toggleWipState',
+    name: 'Toggle Work In Progress State',
+  },
+  viewPrivateChanges: {
+    id: 'viewPrivateChanges',
+    name: 'View Private Changes',
+  },
+};
+
+/**
+ * @param {!Object} obj
+ * @return {!Array} returns a sorted array sorted by the id of the original
+ *    object.
+ */
+export function toSortedPermissionsArray(obj) {
+  if (!obj) { return []; }
+  return Object.keys(obj)
+      .map(key => {
+        return {
+          id: key,
+          value: obj[key],
+        };
+      })
+      .sort((a, b) =>
+        // Since IDs are strings, use localeCompare.
+        a.id.localeCompare(b.id)
+      );
+}
diff --git a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.js b/polygerrit-ui/app/utils/access-util_test.js
similarity index 64%
rename from polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.js
rename to polygerrit-ui/app/utils/access-util_test.js
index b29505f..d4f5669 100644
--- a/polygerrit-ui/app/behaviors/gr-access-behavior/gr-access-behavior_test.js
+++ b/polygerrit-ui/app/utils/access-util_test.js
@@ -15,28 +15,11 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {AccessBehavior} from './gr-access-behavior.js';
-
-const basicFixture = fixtureFromElement('gr-access-behavior-test-element');
+import '../test/common-test-setup-karma.js';
+import {toSortedPermissionsArray} from './access-util.js';
 
 suite('gr-access-behavior tests', () => {
-  let element;
-
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'gr-access-behavior-test-element',
-      behaviors: [AccessBehavior],
-    });
-  });
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('toSortedArray', () => {
+  test('toSortedPermissionsArray', () => {
     const rules = {
       'global:Project-Owners': {
         action: 'ALLOW', force: false,
@@ -53,7 +36,7 @@
         action: 'ALLOW', force: false,
       }},
     ];
-    assert.deepEqual(element.toSortedArray(rules), expectedResult);
+    assert.deepEqual(toSortedPermissionsArray(rules), expectedResult);
   });
 });
 
diff --git a/polygerrit-ui/app/utils/admin-nav-util.js b/polygerrit-ui/app/utils/admin-nav-util.js
new file mode 100644
index 0000000..8356db1
--- /dev/null
+++ b/polygerrit-ui/app/utils/admin-nav-util.js
@@ -0,0 +1,197 @@
+/**
+ * @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 {GerritNav} from '../elements/core/gr-navigation/gr-navigation.js';
+
+const ADMIN_LINKS = [{
+  name: 'Repositories',
+  noBaseUrl: true,
+  url: '/admin/repos',
+  view: 'gr-repo-list',
+  viewableToAll: true,
+}, {
+  name: 'Groups',
+  section: 'Groups',
+  noBaseUrl: true,
+  url: '/admin/groups',
+  view: 'gr-admin-group-list',
+}, {
+  name: 'Plugins',
+  capability: 'viewPlugins',
+  section: 'Plugins',
+  noBaseUrl: true,
+  url: '/admin/plugins',
+  view: 'gr-plugin-list',
+}];
+
+/**
+ * @param {!Object} account
+ * @param {!Function} getAccountCapabilities
+ * @param {!Function} getAdminMenuLinks
+ *  Possible aguments in options:
+ *    repoName?: string
+ *    groupId?: string,
+ *    groupName?: string,
+ *    groupIsInternal?: boolean,
+ *    isAdmin?: boolean,
+ *    groupOwner?: boolean,
+ * @param {!Object=} opt_options
+ * @return {Promise<!Object>}
+ */
+export function getAdminLinks(account, getAccountCapabilities,
+    getAdminMenuLinks, opt_options) {
+  if (!account) {
+    return Promise.resolve(_filterLinks(link => link.viewableToAll,
+        getAdminMenuLinks, opt_options));
+  }
+  return getAccountCapabilities()
+      .then(capabilities => _filterLinks(
+          link => !link.capability
+          || capabilities.hasOwnProperty(link.capability),
+          getAdminMenuLinks,
+          opt_options));
+}
+
+/**
+ * @param {!Function} filterFn
+ * @param {!Function} getAdminMenuLinks
+ *  Possible aguments in options:
+ *    repoName?: string
+ *    groupId?: string,
+ *    groupName?: string,
+ *    groupIsInternal?: boolean,
+ *    isAdmin?: boolean,
+ *    groupOwner?: boolean,
+ * @param {!Object|undefined} opt_options
+ * @return {Promise<!Object>}
+ */
+function _filterLinks(filterFn, getAdminMenuLinks, opt_options) {
+  let links = ADMIN_LINKS.slice(0);
+  let expandedSection;
+
+  const isExternalLink = link => link.url[0] !== '/';
+
+  // Append top-level links that are defined by plugins.
+  links.push(...getAdminMenuLinks().map(link => {
+    return {
+      url: link.url,
+      name: link.text,
+      capability: link.capability || null,
+      noBaseUrl: !isExternalLink(link),
+      view: null,
+      viewableToAll: !link.capability,
+      target: isExternalLink(link) ? '_blank' : null,
+    };
+  }));
+
+  links = links.filter(filterFn);
+
+  const filteredLinks = [];
+  const repoName = opt_options && opt_options.repoName;
+  const groupId = opt_options && opt_options.groupId;
+  const groupName = opt_options && opt_options.groupName;
+  const groupIsInternal = opt_options && opt_options.groupIsInternal;
+  const isAdmin = opt_options && opt_options.isAdmin;
+  const groupOwner = opt_options && opt_options.groupOwner;
+
+  // Don't bother to get sub-navigation items if only the top level links
+  // are needed. This is used by the main header dropdown.
+  if (!repoName && !groupId) { return {links, expandedSection}; }
+
+  // Otherwise determine the full set of links and return both the full
+  // set in addition to the subsection that should be displayed if it
+  // exists.
+  for (const link of links) {
+    const linkCopy = {...link};
+    if (linkCopy.name === 'Repositories' && repoName) {
+      linkCopy.subsection = getRepoSubsections(repoName);
+      expandedSection = linkCopy.subsection;
+    } else if (linkCopy.name === 'Groups' && groupId && groupName) {
+      linkCopy.subsection = getGroupSubsections(groupId, groupName,
+          groupIsInternal, isAdmin, groupOwner);
+      expandedSection = linkCopy.subsection;
+    }
+    filteredLinks.push(linkCopy);
+  }
+  return {links: filteredLinks, expandedSection};
+}
+
+export function getGroupSubsections(groupId, groupName, groupIsInternal,
+    isAdmin, groupOwner) {
+  const subsection = {
+    name: groupName,
+    view: GerritNav.View.GROUP,
+    url: GerritNav.getUrlForGroup(groupId),
+    children: [],
+  };
+  if (groupIsInternal) {
+    subsection.children.push({
+      name: 'Members',
+      detailType: GerritNav.GroupDetailView.MEMBERS,
+      view: GerritNav.View.GROUP,
+      url: GerritNav.getUrlForGroupMembers(groupId),
+    });
+  }
+  if (groupIsInternal && (isAdmin || groupOwner)) {
+    subsection.children.push(
+        {
+          name: 'Audit Log',
+          detailType: GerritNav.GroupDetailView.LOG,
+          view: GerritNav.View.GROUP,
+          url: GerritNav.getUrlForGroupLog(groupId),
+        }
+    );
+  }
+  return subsection;
+}
+
+export function getRepoSubsections(repoName) {
+  return {
+    name: repoName,
+    view: GerritNav.View.REPO,
+    url: GerritNav.getUrlForRepo(repoName),
+    children: [{
+      name: 'Access',
+      view: GerritNav.View.REPO,
+      detailType: GerritNav.RepoDetailView.ACCESS,
+      url: GerritNav.getUrlForRepoAccess(repoName),
+    },
+    {
+      name: 'Commands',
+      view: GerritNav.View.REPO,
+      detailType: GerritNav.RepoDetailView.COMMANDS,
+      url: GerritNav.getUrlForRepoCommands(repoName),
+    },
+    {
+      name: 'Branches',
+      view: GerritNav.View.REPO,
+      detailType: GerritNav.RepoDetailView.BRANCHES,
+      url: GerritNav.getUrlForRepoBranches(repoName),
+    },
+    {
+      name: 'Tags',
+      view: GerritNav.View.REPO,
+      detailType: GerritNav.RepoDetailView.TAGS,
+      url: GerritNav.getUrlForRepoTags(repoName),
+    },
+    {
+      name: 'Dashboards',
+      view: GerritNav.View.REPO,
+      detailType: GerritNav.RepoDetailView.DASHBOARDS,
+      url: GerritNav.getUrlForRepoDashboards(repoName),
+    }],
+  };
+}
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.js b/polygerrit-ui/app/utils/admin-nav-util_test.js
similarity index 93%
rename from polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.js
rename to polygerrit-ui/app/utils/admin-nav-util_test.js
index 72109f2..a3dc87a 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.js
+++ b/polygerrit-ui/app/utils/admin-nav-util_test.js
@@ -15,35 +15,20 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {AdminNavBehavior} from './gr-admin-nav-behavior.js';
-
-const basicFixture = fixtureFromElement('gr-admin-nav-behavior-test-element');
+import '../test/common-test-setup-karma.js';
+import {getAdminLinks} from './admin-nav-util.js';
 
 suite('gr-admin-nav-behavior tests', () => {
-  let element;
   let capabilityStub;
   let menuLinkStub;
 
-  suiteSetup(() => {
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'gr-admin-nav-behavior-test-element',
-      behaviors: [
-        AdminNavBehavior,
-      ],
-    });
-  });
-
   setup(() => {
-    element = basicFixture.instantiate();
     capabilityStub = sinon.stub();
     menuLinkStub = sinon.stub().returns([]);
   });
 
   const testAdminLinks = (account, options, expected, done) => {
-    element.getAdminLinks(account,
+    getAdminLinks(account,
         capabilityStub,
         menuLinkStub,
         options)
diff --git a/polygerrit-ui/app/utils/change-util.ts b/polygerrit-ui/app/utils/change-util.ts
new file mode 100644
index 0000000..5a39b23
--- /dev/null
+++ b/polygerrit-ui/app/utils/change-util.ts
@@ -0,0 +1,200 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {getBaseUrl} from './url-util';
+import {ChangeStatus} from '../constants/constants';
+
+// WARNING: The types below can be completely wrong!
+// The types was added to avoid eslinter and typescript errors.
+// Correct typing requires more analysis and (probably) code changes.
+// This will be done later.
+type ChangeNum = string; // This can be wrong! See WARNING above
+type PatchNum = string; // This can be wrong! See WARNING above
+
+// This can be wrong! See WARNING above
+interface Change {
+  status: string; // This can be wrong! See WARNING above
+  mergeable: boolean; // This can be wrong! See WARNING above
+  work_in_progress: boolean; // This can be wrong! See WARNING above
+  is_private: boolean; // This can be wrong! See WARNING above
+  submittable: boolean; // This can be wrong! See WARNING above
+}
+
+// This can be wrong! See WARNING above
+interface ChangeStatusesOptions {
+  mergeable: boolean; // This can be wrong! See WARNING above
+  submitEnabled: boolean; // This can be wrong! See WARNING above
+}
+
+export const ChangeDiffType = {
+  ADDED: 'ADDED',
+  COPIED: 'COPIED',
+  DELETED: 'DELETED',
+  MODIFIED: 'MODIFIED',
+  RENAMED: 'RENAMED',
+  REWRITE: 'REWRITE',
+};
+
+// Must be kept in sync with the ListChangesOption enum and protobuf.
+export const ListChangesOption = {
+  LABELS: 0,
+  DETAILED_LABELS: 8,
+
+  // Return information on the current patch set of the change.
+  CURRENT_REVISION: 1,
+  ALL_REVISIONS: 2,
+
+  // If revisions are included, parse the commit object.
+  CURRENT_COMMIT: 3,
+  ALL_COMMITS: 4,
+
+  // If a patch set is included, include the files of the patch set.
+  CURRENT_FILES: 5,
+  ALL_FILES: 6,
+
+  // If accounts are included, include detailed account info.
+  DETAILED_ACCOUNTS: 7,
+
+  // Include messages associated with the change.
+  MESSAGES: 9,
+
+  // Include allowed actions client could perform.
+  CURRENT_ACTIONS: 10,
+
+  // Set the reviewed boolean for the caller.
+  REVIEWED: 11,
+
+  // Include download commands for the caller.
+  DOWNLOAD_COMMANDS: 13,
+
+  // Include patch set weblinks.
+  WEB_LINKS: 14,
+
+  // Include consistency check results.
+  CHECK: 15,
+
+  // Include allowed change actions client could perform.
+  CHANGE_ACTIONS: 16,
+
+  // Include a copy of commit messages including review footers.
+  COMMIT_FOOTERS: 17,
+
+  // Include push certificate information along with any patch sets.
+  PUSH_CERTIFICATES: 18,
+
+  // Include change's reviewer updates.
+  REVIEWER_UPDATES: 19,
+
+  // Set the submittable boolean.
+  SUBMITTABLE: 20,
+
+  // If tracking ids are included, include detailed tracking ids info.
+  TRACKING_IDS: 21,
+
+  // Skip mergeability data.
+  SKIP_MERGEABLE: 22,
+
+  /**
+   * Skip diffstat computation that compute the insertions field (number of lines inserted) and
+   * deletions field (number of lines deleted)
+   */
+  SKIP_DIFFSTAT: 23,
+};
+
+export function listChangesOptionsToHex(...args: number[]) {
+  let v = 0;
+  for (let i = 0; i < args.length; i++) {
+    v |= 1 << args[i];
+  }
+  return v.toString(16);
+}
+
+/**
+ *  @return {string}
+ */
+export function changeBaseURL(
+  project: string,
+  changeNum: ChangeNum,
+  patchNum: PatchNum
+): string {
+  let v =
+    getBaseUrl() + '/changes/' + encodeURIComponent(project) + '~' + changeNum;
+  if (patchNum) {
+    v += '/revisions/' + patchNum;
+  }
+  return v;
+}
+
+export function changePath(changeNum: ChangeNum) {
+  return getBaseUrl() + '/c/' + changeNum;
+}
+
+export function changeIsOpen(change?: Change) {
+  return change && change.status === ChangeStatus.NEW;
+}
+
+/**
+ * @param {!Object} change
+ * @param {!Object=} opt_options
+ *
+ * @return {!Array}
+ */
+export function changeStatuses(
+  change: Change,
+  opt_options?: ChangeStatusesOptions
+) {
+  const states = [];
+  if (change.status === ChangeStatus.MERGED) {
+    states.push('Merged');
+  } else if (change.status === ChangeStatus.ABANDONED) {
+    states.push('Abandoned');
+  } else if (
+    change.mergeable === false ||
+    (opt_options && opt_options.mergeable === false)
+  ) {
+    // 'mergeable' prop may not always exist (@see Issue 6819)
+    states.push('Merge Conflict');
+  }
+  if (change.work_in_progress) {
+    states.push('WIP');
+  }
+  if (change.is_private) {
+    states.push('Private');
+  }
+
+  // If there are any pre-defined statuses, only return those. Otherwise,
+  // will determine the derived status.
+  if (states.length || !opt_options) {
+    return states;
+  }
+
+  // If no missing requirements, either active or ready to submit.
+  if (change.submittable && opt_options.submitEnabled) {
+    states.push('Ready to submit');
+  } else {
+    // Otherwise it is active.
+    states.push('Active');
+  }
+  return states;
+}
+
+/**
+ * @param {!Object} change
+ * @return {string}
+ */
+export function changeStatusString(change: Change) {
+  return changeStatuses(change).join(', ');
+}
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js b/polygerrit-ui/app/utils/change-util_test.js
similarity index 68%
rename from polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
rename to polygerrit-ui/app/utils/change-util_test.js
index 16f6111..20b9578 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.js
+++ b/polygerrit-ui/app/utils/change-util_test.js
@@ -15,55 +15,35 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {RESTClientBehavior} from './rest-client-behavior.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import '../test/common-test-setup-karma.js';
+import {
+  changeBaseURL,
+  changePath,
+  changeStatuses,
+  changeStatusString,
+} from './change-util.js';
 
-const basicFixture = fixtureFromElement('rest-client-behavior-test-element');
-
-const withinOverlayFixture = fixtureFromTemplate(html`
-<gr-overlay>
-  <rest-client-behavior-test-element></rest-client-behavior-test-element>
-</gr-overlay>
-`);
-
-suite('rest-client-behavior tests', () => {
-  let element;
-  // eslint-disable-next-line no-unused-vars
-  let overlay;
+suite('change-util tests', () => {
   let originalCanonicalPath;
 
   suiteSetup(() => {
     originalCanonicalPath = window.CANONICAL_PATH;
     window.CANONICAL_PATH = '/r';
-    // Define a Polymer element that uses this behavior.
-    Polymer({
-      is: 'rest-client-behavior-test-element',
-      behaviors: [
-        RESTClientBehavior,
-      ],
-    });
   });
 
   suiteTeardown(() => {
     window.CANONICAL_PATH = originalCanonicalPath;
   });
 
-  setup(() => {
-    element = basicFixture.instantiate();
-    overlay = withinOverlayFixture.instantiate();
-  });
-
   test('changeBaseURL', () => {
     assert.deepEqual(
-        element.changeBaseURL('test/project', '1', '2'),
+        changeBaseURL('test/project', '1', '2'),
         '/r/changes/test%2Fproject~1/revisions/2'
     );
   });
 
   test('changePath', () => {
-    assert.deepEqual(element.changePath('1'), '/r/c/1');
+    assert.deepEqual(changePath('1'), '/r/c/1');
   });
 
   test('Open status', () => {
@@ -77,41 +57,41 @@
       labels: {},
       mergeable: true,
     };
-    let statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    let statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, []);
     assert.equal(statusString, '');
 
     change.submittable = false;
-    statuses = element.changeStatuses(change,
+    statuses = changeStatuses(change,
         {includeDerived: true});
     assert.deepEqual(statuses, ['Active']);
 
     // With no missing labels but no submitEnabled option.
     change.submittable = true;
-    statuses = element.changeStatuses(change,
+    statuses = changeStatuses(change,
         {includeDerived: true});
     assert.deepEqual(statuses, ['Active']);
 
     // Without missing labels and enabled submit
-    statuses = element.changeStatuses(change,
+    statuses = changeStatuses(change,
         {includeDerived: true, submitEnabled: true});
     assert.deepEqual(statuses, ['Ready to submit']);
 
     change.mergeable = false;
     change.submittable = true;
-    statuses = element.changeStatuses(change,
+    statuses = changeStatuses(change,
         {includeDerived: true});
     assert.deepEqual(statuses, ['Merge Conflict']);
 
     delete change.mergeable;
     change.submittable = true;
-    statuses = element.changeStatuses(change,
+    statuses = changeStatuses(change,
         {includeDerived: true, mergeable: true, submitEnabled: true});
     assert.deepEqual(statuses, ['Ready to submit']);
 
     change.submittable = true;
-    statuses = element.changeStatuses(change,
+    statuses = changeStatuses(change,
         {includeDerived: true, mergeable: false});
     assert.deepEqual(statuses, ['Merge Conflict']);
   });
@@ -127,8 +107,8 @@
       labels: {},
       mergeable: false,
     };
-    const statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    const statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, ['Merge Conflict']);
     assert.equal(statusString, 'Merge Conflict');
   });
@@ -143,8 +123,8 @@
       status: 'NEW',
       labels: {},
     };
-    const statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    const statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, []);
     assert.equal(statusString, '');
   });
@@ -159,8 +139,8 @@
       status: 'MERGED',
       labels: {},
     };
-    const statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    const statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, ['Merged']);
     assert.equal(statusString, 'Merged');
   });
@@ -175,8 +155,8 @@
       status: 'ABANDONED',
       labels: {},
     };
-    const statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    const statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, ['Abandoned']);
     assert.equal(statusString, 'Abandoned');
   });
@@ -194,8 +174,8 @@
       labels: {},
       mergeable: true,
     };
-    const statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    const statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, ['WIP', 'Private']);
     assert.equal(statusString, 'WIP, Private');
   });
@@ -213,8 +193,8 @@
       labels: {},
       mergeable: false,
     };
-    const statuses = element.changeStatuses(change);
-    const statusString = element.changeStatusString(change);
+    const statuses = changeStatuses(change);
+    const statusString = changeStatusString(change);
     assert.deepEqual(statuses, ['Merge Conflict', 'WIP', 'Private']);
     assert.equal(statusString, 'Merge Conflict, WIP, Private');
   });
diff --git a/polygerrit-ui/app/utils/display-name-util.js b/polygerrit-ui/app/utils/display-name-util.js
new file mode 100644
index 0000000..9868932
--- /dev/null
+++ b/polygerrit-ui/app/utils/display-name-util.js
@@ -0,0 +1,71 @@
+/**
+ * @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.
+ */
+const ANONYMOUS_NAME = 'Anonymous';
+
+export function getUserName(config, account) {
+  if (account && account.name) {
+    return account.name;
+  } else if (account && account.username) {
+    return account.username;
+  } else if (account && account.email) {
+    return account.email;
+  } else if (config && config.user &&
+      config.user.anonymous_coward_name !== 'Anonymous Coward') {
+    return config.user.anonymous_coward_name;
+  }
+
+  return ANONYMOUS_NAME;
+}
+
+export function getDisplayName(config, account) {
+  if (account && account.display_name) {
+    return account.display_name;
+  }
+  if (!account || !account.name || !config || !config.accounts) {
+    return getUserName(config, account);
+  }
+  if (config.accounts.default_display_name === 'USERNAME'
+      && account.username) {
+    return account.username;
+  }
+  if (config.accounts.default_display_name === 'FIRST_NAME') {
+    return account.name.trim().split(' ')[0];
+  }
+  // Treat every other value as FULL_NAME.
+  return account.name;
+}
+
+export function getAccountDisplayName(config, account) {
+  const reviewerName = getUserName(config, account);
+  const reviewerEmail = _accountEmail(account.email);
+  const reviewerStatus = account.status ? '(' + account.status + ')' : '';
+  return [reviewerName, reviewerEmail, reviewerStatus]
+      .filter(p => p.length > 0).join(' ');
+}
+
+function _accountEmail(email) {
+  if (typeof email !== 'undefined') {
+    return '<' + email + '>';
+  }
+  return '';
+}
+
+export const _testOnly_accountEmail = _accountEmail;
+
+export function getGroupDisplayName(group) {
+  return group.name + ' (group)';
+}
diff --git a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.js b/polygerrit-ui/app/utils/display-name-util_test.js
similarity index 73%
rename from polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.js
rename to polygerrit-ui/app/utils/display-name-util_test.js
index 94d96ad..68dc2e5 100644
--- a/polygerrit-ui/app/scripts/gr-display-name-utils/gr-display-name-utils_test.js
+++ b/polygerrit-ui/app/utils/display-name-util_test.js
@@ -15,10 +15,10 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {GrDisplayNameUtils} from './gr-display-name-utils.js';
+import '../test/common-test-setup-karma.js';
+import {getDisplayName, getUserName, getGroupDisplayName, getAccountDisplayName, _testOnly_accountEmail} from './display-name-util.js';
 
-suite('gr-display-name-utils tests', () => {
+suite('display-name-utils tests', () => {
   // eslint-disable-next-line no-unused-vars
   const config = {
     user: {
@@ -30,7 +30,7 @@
     const account = {
       name: 'test-name',
     };
-    assert.equal(GrDisplayNameUtils.getDisplayName(config, account),
+    assert.equal(getDisplayName(config, account),
         'test-name');
   });
 
@@ -39,7 +39,7 @@
       name: 'test-name',
       display_name: 'better-name',
     };
-    assert.equal(GrDisplayNameUtils.getDisplayName(config, account),
+    assert.equal(getDisplayName(config, account),
         'better-name');
   });
 
@@ -53,7 +53,7 @@
         default_display_name: 'USERNAME',
       },
     };
-    assert.equal(GrDisplayNameUtils.getDisplayName(config, account),
+    assert.equal(getDisplayName(config, account),
         'user-name');
   });
 
@@ -66,7 +66,7 @@
         default_display_name: 'FIRST_NAME',
       },
     };
-    assert.equal(GrDisplayNameUtils.getDisplayName(config, account),
+    assert.equal(getDisplayName(config, account),
         'firstname');
   });
 
@@ -79,7 +79,7 @@
         default_display_name: 'FIRST_NAME',
       },
     };
-    assert.equal(GrDisplayNameUtils.getDisplayName(config, account),
+    assert.equal(getDisplayName(config, account),
         'firstname');
   });
 
@@ -92,7 +92,7 @@
         default_display_name: 'FULL_NAME',
       },
     };
-    assert.equal(GrDisplayNameUtils.getDisplayName(config, account),
+    assert.equal(getDisplayName(config, account),
         'firstname lastname');
   });
 
@@ -100,7 +100,7 @@
     const account = {
       name: 'test-name',
     };
-    assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
+    assert.deepEqual(getUserName(config, account),
         'test-name');
   });
 
@@ -108,7 +108,7 @@
     const account = {
       username: 'test-user',
     };
-    assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
+    assert.deepEqual(getUserName(config, account),
         'test-user');
   });
 
@@ -116,12 +116,12 @@
     const account = {
       email: 'test-user@test-url.com',
     };
-    assert.deepEqual(GrDisplayNameUtils.getUserName(config, account),
+    assert.deepEqual(getUserName(config, account),
         'test-user@test-url.com');
   });
 
   test('getUserName returns not Anonymous Coward as the anon name', () => {
-    assert.deepEqual(GrDisplayNameUtils.getUserName(config, null),
+    assert.deepEqual(getUserName(config, null),
         'Anonymous');
   });
 
@@ -131,27 +131,27 @@
         anonymous_coward_name: 'Test Anon',
       },
     };
-    assert.deepEqual(GrDisplayNameUtils.getUserName(config, null),
+    assert.deepEqual(getUserName(config, null),
         'Test Anon');
   });
 
   test('getAccountDisplayName - account with name only', () => {
     assert.equal(
-        GrDisplayNameUtils.getAccountDisplayName(config,
+        getAccountDisplayName(config,
             {name: 'Some user name'}),
         'Some user name');
   });
 
   test('getAccountDisplayName - account with email only', () => {
     assert.equal(
-        GrDisplayNameUtils.getAccountDisplayName(config,
+        getAccountDisplayName(config,
             {email: 'my@example.com'}),
         'my@example.com <my@example.com>');
   });
 
   test('getAccountDisplayName - account with name and status', () => {
     assert.equal(
-        GrDisplayNameUtils.getAccountDisplayName(config, {
+        getAccountDisplayName(config, {
           name: 'Some name',
           status: 'OOO',
         }),
@@ -160,7 +160,7 @@
 
   test('getAccountDisplayName - account with name and email', () => {
     assert.equal(
-        GrDisplayNameUtils.getAccountDisplayName(config, {
+        getAccountDisplayName(config, {
           name: 'Some name',
           email: 'my@example.com',
         }),
@@ -169,7 +169,7 @@
 
   test('getAccountDisplayName - account with name, email and status', () => {
     assert.equal(
-        GrDisplayNameUtils.getAccountDisplayName(config, {
+        getAccountDisplayName(config, {
           name: 'Some name',
           email: 'my@example.com',
           status: 'OOO',
@@ -179,15 +179,15 @@
 
   test('getGroupDisplayName', () => {
     assert.equal(
-        GrDisplayNameUtils.getGroupDisplayName({name: 'Some user name'}),
+        getGroupDisplayName({name: 'Some user name'}),
         'Some user name (group)');
   });
 
   test('_accountEmail', () => {
     assert.equal(
-        GrDisplayNameUtils._accountEmail('email@gerritreview.com'),
+        _testOnly_accountEmail('email@gerritreview.com'),
         '<email@gerritreview.com>');
-    assert.equal(GrDisplayNameUtils._accountEmail(undefined), '');
+    assert.equal(_testOnly_accountEmail(undefined), '');
   });
 });
 
diff --git a/polygerrit-ui/app/utils/dom-util.js b/polygerrit-ui/app/utils/dom-util.js
index e26bf74..16d9e00 100644
--- a/polygerrit-ui/app/utils/dom-util.js
+++ b/polygerrit-ui/app/utils/dom-util.js
@@ -189,4 +189,14 @@
  */
 export function strToClassName(str = '', prefix = 'generated_') {
   return `${prefix}${str.replace(/[^a-zA-Z0-9-_]/g, '_')}`;
+}
+
+// shared API element
+let _sharedApiEl;
+
+export function getSharedApiEl() {
+  if (!_sharedApiEl) {
+    _sharedApiEl = document.createElement('gr-js-api-interface');
+  }
+  return _sharedApiEl;
 }
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/patch-set-util.js b/polygerrit-ui/app/utils/patch-set-util.js
new file mode 100644
index 0000000..c27a8cd
--- /dev/null
+++ b/polygerrit-ui/app/utils/patch-set-util.js
@@ -0,0 +1,265 @@
+/**
+ * @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.
+ */
+
+// Tags identifying ChangeMessages that move change into WIP state.
+const WIP_TAGS = [
+  'autogenerated:gerrit:newWipPatchSet',
+  'autogenerated:gerrit:setWorkInProgress',
+];
+
+// Tags identifying ChangeMessages that move change out of WIP state.
+const READY_TAGS = [
+  'autogenerated:gerrit:setReadyForReview',
+];
+
+export const SPECIAL_PATCH_SET_NUM = {
+  EDIT: 'edit',
+  PARENT: 'PARENT',
+};
+
+/**
+ * As patchNum can be either a string (e.g. 'edit', 'PARENT') OR a number,
+ * this function checks for patchNum equality.
+ *
+ * @param {string|number} a
+ * @param {string|number|undefined} b Undefined sometimes because
+ *    computeLatestPatchNum can return undefined.
+ * @return {boolean}
+ */
+export function patchNumEquals(a, b) {
+  return a + '' === b + '';
+}
+
+/**
+ * Whether the given patch is a numbered parent of a merge (i.e. a negative
+ * number).
+ *
+ * @param  {string|number} n
+ * @return {boolean}
+ */
+export function isMergeParent(n) {
+  return (n + '')[0] === '-';
+}
+
+/**
+ * Given an object of revisions, get a particular revision based on patch
+ * num.
+ *
+ * @param {Object} revisions The object of revisions given by the API
+ * @param {number|string} patchNum The number index of the revision
+ * @return {Object} The correspondent revision obj from {revisions}
+ */
+export function getRevisionByPatchNum(revisions, patchNum) {
+  for (const rev of Object.values(revisions || {})) {
+    if (patchNumEquals(rev._number, patchNum)) {
+      return rev;
+    }
+  }
+}
+
+/**
+ * Find change edit base revision if change edit exists.
+ *
+ * @param {!Array<!Object>} revisions The revisions array.
+ * @return {Object} change edit parent revision or null if change edit
+ *     doesn't exist.
+ */
+export function findEditParentRevision(revisions) {
+  const editInfo =
+      revisions.find(info => info._number === SPECIAL_PATCH_SET_NUM.EDIT);
+
+  if (!editInfo) { return null; }
+
+  return revisions.find(info => info._number === editInfo.basePatchNum) ||
+      null;
+}
+
+/**
+ * Find change edit base patch set number if change edit exists.
+ *
+ * @param {!Array<!Object>} revisions The revisions array.
+ * @return {number} Change edit patch set number or -1.
+ */
+export function findEditParentPatchNum(revisions) {
+  const revisionInfo = findEditParentRevision(revisions);
+  return revisionInfo ? revisionInfo._number : -1;
+}
+
+/**
+ * Sort given revisions array according to the patch set number, in
+ * descending order.
+ * The sort algorithm is change edit aware. Change edit has patch set number
+ * equals 'edit', but must appear after the patch set it was based on.
+ * Example: change edit is based on patch set 2, and another patch set was
+ * uploaded after change edit creation, the sorted order should be:
+ * 3, edit, 2, 1.
+ *
+ * @param {!Array<!Object>} revisions The revisions array
+ * @return {!Array<!Object>} The sorted {revisions} array
+ */
+export function sortRevisions(revisions) {
+  const editParent = findEditParentPatchNum(revisions);
+  // Map a normal patchNum to 2 * (patchNum - 1) + 1... I.e. 1 -> 1,
+  // 2 -> 3, 3 -> 5, etc.
+  // Map an edit to the patchNum of parent*2... I.e. edit on 2 -> 4.
+  const num = r => (r._number === SPECIAL_PATCH_SET_NUM.EDIT ?
+    2 * editParent :
+    2 * (r._number - 1) + 1);
+  return revisions.sort((a, b) => num(b) - num(a));
+}
+
+/**
+ * Construct a chronological list of patch sets derived from change details.
+ * Each element of this list is an object with the following properties:
+ *
+ *   * num {number} The number identifying the patch set
+ *   * desc {!string} Optional patch set description
+ *   * wip {boolean} If true, this patch set was never subject to review.
+ *   * sha {string} hash of the commit
+ *
+ * The wip property is determined by the change's current work_in_progress
+ * property and its log of change messages.
+ *
+ * @param {!Object} change The change details
+ * @return {!Array<!Object>} Sorted list of patch set objects, as described
+ *     above
+ */
+export function computeAllPatchSets(change) {
+  if (!change) { return []; }
+  let patchNums = [];
+  if (change.revisions && Object.keys(change.revisions).length) {
+    const revisions = Object.keys(change.revisions)
+        .map(sha => { return {sha, ...change.revisions[sha]}; });
+    patchNums = sortRevisions(revisions)
+        .map(e => {
+          // TODO(kaspern): Mark which patchset an edit was made on, if an
+          // edit exists -- perhaps with a temporary description.
+          return {
+            num: e._number,
+            desc: e.description,
+            sha: e.sha,
+          };
+        });
+  }
+  return _computeWipForPatchSets(change, patchNums);
+}
+
+/**
+ * Populate the wip properties of the given list of patch sets.
+ *
+ * @param {!Object} change The change details
+ * @param {!Array<!Object>} patchNums Sorted list of patch set objects, as
+ *     generated by computeAllPatchSets
+ * @return {!Array<!Object>} The given list of patch set objects, with the
+ *     wip property set on each of them
+ */
+function _computeWipForPatchSets(change, patchNums) {
+  if (!change.messages || !change.messages.length) {
+    return patchNums;
+  }
+  const psWip = {};
+  let wip = change.work_in_progress;
+  for (let i = 0; i < change.messages.length; i++) {
+    const msg = change.messages[i];
+    if (WIP_TAGS.includes(msg.tag)) {
+      wip = true;
+    } else if (READY_TAGS.includes(msg.tag)) {
+      wip = false;
+    }
+    if (psWip[msg._revision_number] !== false) {
+      psWip[msg._revision_number] = wip;
+    }
+  }
+
+  for (let i = 0; i < patchNums.length; i++) {
+    patchNums[i].wip = psWip[patchNums[i].num];
+  }
+  return patchNums;
+}
+
+export const _testOnly_computeWipForPatchSets = _computeWipForPatchSets;
+
+/** @return {number|undefined} */
+export function computeLatestPatchNum(allPatchSets) {
+  if (!allPatchSets || !allPatchSets.length) { return undefined; }
+  if (allPatchSets[0].num === SPECIAL_PATCH_SET_NUM.EDIT) {
+    return allPatchSets[1].num;
+  }
+  return allPatchSets[0].num;
+}
+
+/** @return {boolean} */
+export function hasEditBasedOnCurrentPatchSet(allPatchSets) {
+  if (!allPatchSets || allPatchSets.length < 2) { return false; }
+  return allPatchSets[0].num === SPECIAL_PATCH_SET_NUM.EDIT;
+}
+
+/** @return {boolean} */
+export function hasEditPatchsetLoaded(patchRangeRecord) {
+  const patchRange = patchRangeRecord.base;
+  if (!patchRange) { return false; }
+  return patchRange.patchNum === SPECIAL_PATCH_SET_NUM.EDIT ||
+      patchRange.basePatchNum === SPECIAL_PATCH_SET_NUM.EDIT;
+}
+
+/**
+ * Check whether there is no newer patch than the latest patch that was
+ * available when this change was loaded.
+ *
+ * @return {Promise<!Object>} A promise that yields true if the latest patch
+ *     has been loaded, and false if a newer patch has been uploaded in the
+ *     meantime. The promise is rejected on network error.
+ */
+export function fetchChangeUpdates(change, restAPI) {
+  const knownLatest = computeLatestPatchNum(computeAllPatchSets(change));
+  return restAPI.getChangeDetail(change._number)
+      .then(detail => {
+        if (!detail) {
+          const error = new Error('Unable to check for latest patchset.');
+          return Promise.reject(error);
+        }
+        const actualLatest = computeLatestPatchNum(computeAllPatchSets(detail));
+        return {
+          isLatest: actualLatest <= knownLatest,
+          newStatus: change.status !== detail.status ? detail.status : null,
+          newMessages: change.messages.length < detail.messages.length,
+        };
+      });
+}
+
+/**
+ * @param {number|string} patchNum
+ * @param {!Array<!Object>} revisions A sorted array of revisions.
+ *
+ * @return {number} The index of the revision with the given patchNum.
+ */
+export function findSortedIndex(patchNum, revisions) {
+  revisions = revisions || [];
+  const findNum = rev => rev._number + '' === patchNum + '';
+  return revisions.findIndex(findNum);
+}
+
+/**
+ * Convert parent indexes from patch range expressions to numbers.
+ * For example, in a patch range expression `"-3"` becomes `3`.
+ *
+ * @param {number|string} rangeBase
+ * @return {number}
+ */
+export function getParentIndex(rangeBase) {
+  return -parseInt(rangeBase + '', 10);
+}
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.js b/polygerrit-ui/app/utils/patch-set-util_test.js
similarity index 73%
rename from polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.js
rename to polygerrit-ui/app/utils/patch-set-util_test.js
index b14c0bd..29cc370 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.js
+++ b/polygerrit-ui/app/utils/patch-set-util_test.js
@@ -15,19 +15,25 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {PatchSetBehavior} from './gr-patch-set-behavior.js';
-suite('gr-patch-set-behavior tests', () => {
+import '../test/common-test-setup-karma.js';
+import {
+  _testOnly_computeWipForPatchSets, computeAllPatchSets,
+  fetchChangeUpdates, findEditParentPatchNum, findEditParentRevision,
+  getParentIndex, getRevisionByPatchNum,
+  isMergeParent,
+  patchNumEquals, sortRevisions,
+} from './patch-set-util.js';
+
+suite('gr-patch-set-util tests', () => {
   test('getRevisionByPatchNum', () => {
-    const get = PatchSetBehavior.getRevisionByPatchNum;
     const revisions = [
       {_number: 0},
       {_number: 1},
       {_number: 2},
     ];
-    assert.deepEqual(get(revisions, '1'), revisions[1]);
-    assert.deepEqual(get(revisions, 2), revisions[2]);
-    assert.equal(get(revisions, '3'), undefined);
+    assert.deepEqual(getRevisionByPatchNum(revisions, '1'), revisions[1]);
+    assert.deepEqual(getRevisionByPatchNum(revisions, 2), revisions[2]);
+    assert.equal(getRevisionByPatchNum(revisions, '3'), undefined);
   });
 
   test('fetchChangeUpdates on latest', done => {
@@ -44,7 +50,7 @@
         return Promise.resolve(knownChange);
       },
     };
-    PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+    fetchChangeUpdates(knownChange, mockRestApi)
         .then(result => {
           assert.isTrue(result.isLatest);
           assert.isNotOk(result.newStatus);
@@ -76,7 +82,7 @@
         return Promise.resolve(actualChange);
       },
     };
-    PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+    fetchChangeUpdates(knownChange, mockRestApi)
         .then(result => {
           assert.isFalse(result.isLatest);
           assert.isNotOk(result.newStatus);
@@ -107,7 +113,7 @@
         return Promise.resolve(actualChange);
       },
     };
-    PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+    fetchChangeUpdates(knownChange, mockRestApi)
         .then(result => {
           assert.isTrue(result.isLatest);
           assert.equal(result.newStatus, 'MERGED');
@@ -138,7 +144,7 @@
         return Promise.resolve(actualChange);
       },
     };
-    PatchSetBehavior.fetchChangeUpdates(knownChange, mockRestApi)
+    fetchChangeUpdates(knownChange, mockRestApi)
         .then(result => {
           assert.isTrue(result.isLatest);
           assert.isNotOk(result.newStatus);
@@ -172,7 +178,7 @@
         }
       }
       let patchNums = revs.map(rev => { return {num: rev}; });
-      patchNums = PatchSetBehavior._computeWipForPatchSets(
+      patchNums = _testOnly_computeWipForPatchSets(
           change, patchNums);
       const actualWipsByRevision = {};
       for (const patchNum of patchNums) {
@@ -226,63 +232,58 @@
   });
 
   test('patchNumEquals', () => {
-    const equals = PatchSetBehavior.patchNumEquals;
-    assert.isFalse(equals('edit', 'PARENT'));
-    assert.isFalse(equals('edit', NaN));
-    assert.isFalse(equals(1, '2'));
+    assert.isFalse(patchNumEquals('edit', 'PARENT'));
+    assert.isFalse(patchNumEquals('edit', NaN));
+    assert.isFalse(patchNumEquals(1, '2'));
 
-    assert.isTrue(equals(1, '1'));
-    assert.isTrue(equals(1, 1));
-    assert.isTrue(equals('edit', 'edit'));
-    assert.isTrue(equals('PARENT', 'PARENT'));
+    assert.isTrue(patchNumEquals(1, '1'));
+    assert.isTrue(patchNumEquals(1, 1));
+    assert.isTrue(patchNumEquals('edit', 'edit'));
+    assert.isTrue(patchNumEquals('PARENT', 'PARENT'));
   });
 
   test('isMergeParent', () => {
-    const isParent = PatchSetBehavior.isMergeParent;
-    assert.isFalse(isParent(1));
-    assert.isFalse(isParent(4321));
-    assert.isFalse(isParent('52'));
-    assert.isFalse(isParent('edit'));
-    assert.isFalse(isParent('PARENT'));
-    assert.isFalse(isParent(0));
+    assert.isFalse(isMergeParent(1));
+    assert.isFalse(isMergeParent(4321));
+    assert.isFalse(isMergeParent('52'));
+    assert.isFalse(isMergeParent('edit'));
+    assert.isFalse(isMergeParent('PARENT'));
+    assert.isFalse(isMergeParent(0));
 
-    assert.isTrue(isParent(-23));
-    assert.isTrue(isParent(-1));
-    assert.isTrue(isParent('-42'));
+    assert.isTrue(isMergeParent(-23));
+    assert.isTrue(isMergeParent(-1));
+    assert.isTrue(isMergeParent('-42'));
   });
 
   test('findEditParentRevision', () => {
-    const findParent = PatchSetBehavior.findEditParentRevision;
     let revisions = [
       {_number: 0},
       {_number: 1},
       {_number: 2},
     ];
-    assert.strictEqual(findParent(revisions), null);
+    assert.strictEqual(findEditParentRevision(revisions), null);
 
     revisions = [...revisions, {_number: 'edit', basePatchNum: 3}];
-    assert.strictEqual(findParent(revisions), null);
+    assert.strictEqual(findEditParentRevision(revisions), null);
 
     revisions = [...revisions, {_number: 3}];
-    assert.deepEqual(findParent(revisions), {_number: 3});
+    assert.deepEqual(findEditParentRevision(revisions), {_number: 3});
   });
 
   test('findEditParentPatchNum', () => {
-    const findNum = PatchSetBehavior.findEditParentPatchNum;
     let revisions = [
       {_number: 0},
       {_number: 1},
       {_number: 2},
     ];
-    assert.equal(findNum(revisions), -1);
+    assert.equal(findEditParentPatchNum(revisions), -1);
 
     revisions =
         [...revisions, {_number: 'edit', basePatchNum: 3}, {_number: 3}];
-    assert.deepEqual(findNum(revisions), 3);
+    assert.deepEqual(findEditParentPatchNum(revisions), 3);
   });
 
   test('sortRevisions', () => {
-    const sort = PatchSetBehavior.sortRevisions;
     const revisions = [
       {_number: 0},
       {_number: 2},
@@ -294,24 +295,45 @@
       {_number: 0},
     ];
 
-    assert.deepEqual(sort(revisions), sorted);
+    assert.deepEqual(sortRevisions(revisions), sorted);
 
     // Edit patchset should follow directly after its basePatchNum.
     revisions.push({_number: 'edit', basePatchNum: 2});
     sorted.unshift({_number: 'edit', basePatchNum: 2});
-    assert.deepEqual(sort(revisions), sorted);
+    assert.deepEqual(sortRevisions(revisions), sorted);
 
     revisions[0].basePatchNum = 0;
     const edit = sorted.shift();
     edit.basePatchNum = 0;
     // Edit patchset should be at index 2.
     sorted.splice(2, 0, edit);
-    assert.deepEqual(sort(revisions), sorted);
+    assert.deepEqual(sortRevisions(revisions), sorted);
   });
 
   test('getParentIndex', () => {
-    assert.equal(PatchSetBehavior.getParentIndex('-13'), 13);
-    assert.equal(PatchSetBehavior.getParentIndex(-4), 4);
+    assert.equal(getParentIndex('-13'), 13);
+    assert.equal(getParentIndex(-4), 4);
+  });
+
+  test('computeAllPatchSets', () => {
+    const expected = [
+      {num: 4, desc: 'test', sha: 'rev4'},
+      {num: 3, desc: 'test', sha: 'rev3'},
+      {num: 2, desc: 'test', sha: 'rev2'},
+      {num: 1, desc: 'test', sha: 'rev1'},
+    ];
+    const patchNums = computeAllPatchSets({
+      revisions: {
+        rev3: {_number: 3, description: 'test', date: 3},
+        rev1: {_number: 1, description: 'test', date: 1},
+        rev4: {_number: 4, description: 'test', date: 4},
+        rev2: {_number: 2, description: 'test', date: 2},
+      },
+    });
+    assert.equal(patchNums.length, expected.length);
+    for (let i = 0; i < expected.length; i++) {
+      assert.deepEqual(patchNums[i], expected[i]);
+    }
   });
 });
 
diff --git a/polygerrit-ui/app/utils/path-list-util.js b/polygerrit-ui/app/utils/path-list-util.js
new file mode 100644
index 0000000..e408359
--- /dev/null
+++ b/polygerrit-ui/app/utils/path-list-util.js
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {SpecialFilePath} from '../constants/constants.js';
+
+/**
+ * @param {string} a
+ * @param {string} b
+ * @return {number}
+ */
+export function specialFilePathCompare(a, b) {
+  // The commit message always goes first.
+  if (a === SpecialFilePath.COMMIT_MESSAGE) {
+    return -1;
+  }
+  if (b === SpecialFilePath.COMMIT_MESSAGE) {
+    return 1;
+  }
+
+  // The merge list always comes next.
+  if (a === SpecialFilePath.MERGE_LIST) {
+    return -1;
+  }
+  if (b === SpecialFilePath.MERGE_LIST) {
+    return 1;
+  }
+
+  const aLastDotIndex = a.lastIndexOf('.');
+  const aExt = a.substr(aLastDotIndex + 1);
+  const aFile = a.substr(0, aLastDotIndex) || a;
+
+  const bLastDotIndex = b.lastIndexOf('.');
+  const bExt = b.substr(bLastDotIndex + 1);
+  const bFile = b.substr(0, bLastDotIndex) || b;
+
+  // Sort header files above others with the same base name.
+  const headerExts = ['h', 'hxx', 'hpp'];
+  if (aFile.length > 0 && aFile === bFile) {
+    if (headerExts.includes(aExt) && headerExts.includes(bExt)) {
+      return a.localeCompare(b);
+    }
+    if (headerExts.includes(aExt)) {
+      return -1;
+    }
+    if (headerExts.includes(bExt)) {
+      return 1;
+    }
+  }
+  return aFile.localeCompare(bFile) || a.localeCompare(b);
+}
+
+export function shouldHideFile(file) {
+  return file === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+}
+
+export function addUnmodifiedFiles(files, commentedPaths) {
+  Object.keys(commentedPaths).forEach(commentedPath => {
+    if (files.hasOwnProperty(commentedPath) ||
+      shouldHideFile(commentedPath)) { return; }
+    files[commentedPath] = {status: 'U'};
+  });
+}
+
+export function computeDisplayPath(path) {
+  if (path === SpecialFilePath.COMMIT_MESSAGE) {
+    return 'Commit message';
+  } else if (path === SpecialFilePath.MERGE_LIST) {
+    return 'Merge list';
+  }
+  return path;
+}
+
+export function isMagicPath(path) {
+  return !!path &&
+      (path === SpecialFilePath.COMMIT_MESSAGE || path ===
+          SpecialFilePath.MERGE_LIST);
+}
+
+export function computeTruncatedPath(path) {
+  return truncatePath(
+      computeDisplayPath(path));
+}
+
+/**
+ * Truncates URLs to display filename only
+ * Example
+ * // returns '.../text.html'
+ * util.truncatePath.('dir/text.html');
+ * Example
+ * // returns 'text.html'
+ * util.truncatePath.('text.html');
+ *
+ * @param {string} path
+ * @param {number=} opt_threshold
+ * @return {string} Returns the truncated value of a URL.
+ */
+export function truncatePath(path, opt_threshold) {
+  const threshold = opt_threshold || 1;
+  const pathPieces = path.split('/');
+
+  if (pathPieces.length <= threshold) { return path; }
+
+  const index = pathPieces.length - threshold;
+  // Character is an ellipsis.
+  return `\u2026/${pathPieces.slice(index).join('/')}`;
+}
diff --git a/polygerrit-ui/app/utils/path-list-util_test.js b/polygerrit-ui/app/utils/path-list-util_test.js
new file mode 100644
index 0000000..4d06344
--- /dev/null
+++ b/polygerrit-ui/app/utils/path-list-util_test.js
@@ -0,0 +1,161 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../test/common-test-setup-karma.js';
+import {SpecialFilePath} from '../constants/constants.js';
+import {
+  addUnmodifiedFiles,
+  computeDisplayPath,
+  isMagicPath,
+  specialFilePathCompare, truncatePath,
+} from './path-list-util.js';
+
+suite('path-list-utl tests', () => {
+  test('special sort', () => {
+    const testFiles = [
+      '/a.h',
+      '/MERGE_LIST',
+      '/a.cpp',
+      '/COMMIT_MSG',
+      '/asdasd',
+      '/mrPeanutbutter.py',
+    ];
+    assert.deepEqual(
+        testFiles.sort(specialFilePathCompare),
+        [
+          '/COMMIT_MSG',
+          '/MERGE_LIST',
+          '/a.h',
+          '/a.cpp',
+          '/asdasd',
+          '/mrPeanutbutter.py',
+        ]);
+  });
+
+  test('special file path sorting', () => {
+    assert.deepEqual(
+        ['.b', '/COMMIT_MSG', '.a', 'file'].sort(
+            specialFilePathCompare),
+        ['/COMMIT_MSG', '.a', '.b', 'file']);
+
+    assert.deepEqual(
+        ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
+            specialFilePathCompare),
+        ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
+
+    assert.deepEqual(
+        ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
+            specialFilePathCompare),
+        ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
+
+    assert.deepEqual(
+        ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
+            specialFilePathCompare),
+        ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
+
+    assert.deepEqual(
+        ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
+            specialFilePathCompare),
+        ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
+
+    // Regression test for Issue 4448.
+    assert.deepEqual(
+        [
+          'minidump/minidump_memory_writer.cc',
+          'minidump/minidump_memory_writer.h',
+          'minidump/minidump_thread_writer.cc',
+          'minidump/minidump_thread_writer.h',
+        ].sort(specialFilePathCompare),
+        [
+          'minidump/minidump_memory_writer.h',
+          'minidump/minidump_memory_writer.cc',
+          'minidump/minidump_thread_writer.h',
+          'minidump/minidump_thread_writer.cc',
+        ]);
+
+    // Regression test for Issue 4545.
+    assert.deepEqual(
+        [
+          'task_test.go',
+          'task.go',
+        ].sort(specialFilePathCompare),
+        [
+          'task.go',
+          'task_test.go',
+        ]);
+  });
+
+  test('file display name', () => {
+    assert.equal(computeDisplayPath('/foo/bar/baz'), '/foo/bar/baz');
+    assert.equal(computeDisplayPath('/foobarbaz'), '/foobarbaz');
+    assert.equal(computeDisplayPath('/COMMIT_MSG'), 'Commit message');
+    assert.equal(computeDisplayPath('/MERGE_LIST'), 'Merge list');
+  });
+
+  test('isMagicPath', () => {
+    assert.isFalse(isMagicPath(undefined));
+    assert.isFalse(isMagicPath('/foo.cc'));
+    assert.isTrue(isMagicPath('/COMMIT_MSG'));
+    assert.isTrue(isMagicPath('/MERGE_LIST'));
+  });
+
+  test('patchset level comments are hidden', () => {
+    const commentedPaths = {
+      [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: true,
+      'file1.txt': true,
+    };
+
+    const files = {'file2.txt': {status: 'M'}};
+    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', () => {
+    let path = 'level1/level2/level3/level4/file.js';
+    let shortenedPath = truncatePath(path);
+    // The expected path is truncated with an ellipsis.
+    const expectedPath = '\u2026/file.js';
+    assert.equal(shortenedPath, expectedPath);
+
+    path = 'level2/file.js';
+    shortenedPath = truncatePath(path);
+    assert.equal(shortenedPath, expectedPath);
+  });
+
+  test('truncatePath with opt_threshold', () => {
+    let path = 'level1/level2/level3/level4/file.js';
+    let shortenedPath = truncatePath(path, 2);
+    // The expected path is truncated with an ellipsis.
+    const expectedPath = '\u2026/level4/file.js';
+    assert.equal(shortenedPath, expectedPath);
+
+    path = 'level2/file.js';
+    shortenedPath = truncatePath(path, 2);
+    assert.equal(shortenedPath, path);
+  });
+
+  test('truncatePath with short path should not add ellipsis', () => {
+    const path = 'file.js';
+    const expectedPath = 'file.js';
+    const shortenedPath = truncatePath(path);
+    assert.equal(shortenedPath, expectedPath);
+  });
+});
+
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js b/polygerrit-ui/app/utils/safe-types-util.js
similarity index 66%
rename from polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
rename to polygerrit-ui/app/utils/safe-types-util.js
index ec7a9f4..c4181db 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior.js
+++ b/polygerrit-ui/app/utils/safe-types-util.js
@@ -17,9 +17,6 @@
 
 const SAFE_URL_PATTERN = /^(https?:\/\/|mailto:|[^:/?#]*(?:[/?#]|$))/i;
 
-/** @polymerBehavior Gerrit.SafeTypes */
-export const SafeTypes = {};
-
 /**
  * Wraps a string to be used as a URL. An error is thrown if the string cannot
  * be considered safe.
@@ -27,35 +24,39 @@
  * @constructor
  * @param {string} url the unwrapped, potentially unsafe URL.
  */
-SafeTypes.SafeUrl = function(url) {
-  if (!SAFE_URL_PATTERN.test(url)) {
-    throw new Error(`URL not marked as safe: ${url}`);
+class SafeUrl {
+  constructor(url) {
+    if (!SAFE_URL_PATTERN.test(url)) {
+      throw new Error(`URL not marked as safe: ${url}`);
+    }
+    this._url = url;
   }
-  this._url = url;
-};
+
+  toString() {
+    return this._url;
+  }
+}
+
+export const _testOnly_SafeUrl = SafeUrl;
 
 /**
  * Get the string representation of the safe URL.
  *
  * @returns {string}
  */
-SafeTypes.SafeUrl.prototype.asString = function() {
-  return this._url;
-};
-
-SafeTypes.safeTypesBridge = function(value, type) {
+export function safeTypesBridge(value, type) {
   // If the value is being bound to a URL, ensure the value is wrapped in the
   // SafeUrl type first. If the URL is not safe, allow the SafeUrl constructor
   // to surface the error.
   if (type === 'URL') {
     let safeValue = null;
-    if (value instanceof SafeTypes.SafeUrl) {
+    if (value instanceof SafeUrl) {
       safeValue = value;
     } else if (typeof value === 'string') {
-      safeValue = new SafeTypes.SafeUrl(value);
+      safeValue = new SafeUrl(value);
     }
     if (safeValue) {
-      return safeValue.asString();
+      return safeValue.toString();
     }
   }
 
@@ -67,12 +68,4 @@
 
   // Otherwise fail.
   throw new Error(`Refused to bind value as ${type}: ${value}`);
-};
-
-// 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.SafeTypes = SafeTypes;
-
+}
diff --git a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.js b/polygerrit-ui/app/utils/safe-types-util_test.js
similarity index 70%
rename from polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.js
rename to polygerrit-ui/app/utils/safe-types-util_test.js
index 0e0ff2e..e3968d0 100644
--- a/polygerrit-ui/app/behaviors/safe-types-behavior/safe-types-behavior_test.js
+++ b/polygerrit-ui/app/utils/safe-types-util_test.js
@@ -15,31 +15,15 @@
  * limitations under the License.
  */
 
-import '../../test/common-test-setup-karma.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {SafeTypes} from './safe-types-behavior.js';
+import '../test/common-test-setup-karma.js';
+import {safeTypesBridge, _testOnly_SafeUrl} from './safe-types-util.js';
 
-const basicFixture = fixtureFromElement('safe-types-element');
-
-suite('gr-tooltip-behavior tests', () => {
-  let element;
-
-  suiteSetup(() => {
-    Polymer({
-      is: 'safe-types-element',
-      behaviors: [SafeTypes],
-    });
-  });
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
+suite('safe-types-util tests', () => {
   test('SafeUrl accepts valid urls', () => {
     function accepts(url) {
-      const safeUrl = new element.SafeUrl(url);
+      const safeUrl = new _testOnly_SafeUrl(url);
       assert.isOk(safeUrl);
-      assert.equal(url, safeUrl.asString());
+      assert.equal(url, safeUrl.toString());
     }
     accepts('http://www.google.com/');
     accepts('https://www.google.com/');
@@ -52,7 +36,7 @@
 
   test('SafeUrl rejects invalid urls', () => {
     function rejects(url) {
-      assert.throws(() => { new element.SafeUrl(url); });
+      assert.throws(() => { new _testOnly_SafeUrl(url); });
     }
     rejects('javascript://alert("evil");');
     rejects('ftp:example.com');
@@ -61,12 +45,12 @@
 
   suite('safeTypesBridge', () => {
     function acceptsString(value, type) {
-      assert.equal(SafeTypes.safeTypesBridge(value, type),
+      assert.equal(safeTypesBridge(value, type),
           value);
     }
 
     function rejects(value, type) {
-      assert.throws(() => { SafeTypes.safeTypesBridge(value, type); });
+      assert.throws(() => { safeTypesBridge(value, type); });
     }
 
     test('accepts valid URL strings', () => {
@@ -80,8 +64,8 @@
 
     test('accepts SafeUrl values', () => {
       const url = '/abc/123';
-      const safeUrl = new element.SafeUrl(url);
-      assert.equal(SafeTypes.safeTypesBridge(safeUrl, 'URL'), url);
+      const safeUrl = new _testOnly_SafeUrl(url);
+      assert.equal(safeTypesBridge(safeUrl, 'URL'), url);
     });
 
     test('rejects non-string or non-SafeUrl types', () => {
@@ -101,4 +85,3 @@
     });
   });
 });
-
diff --git a/polygerrit-ui/app/utils/url-util.ts b/polygerrit-ui/app/utils/url-util.ts
index a823bb4..15ab75b 100644
--- a/polygerrit-ui/app/utils/url-util.ts
+++ b/polygerrit-ui/app/utils/url-util.ts
@@ -70,3 +70,29 @@
 export function _testOnly_clearDocsBaseUrlCache() {
   getDocsBaseUrlCachedPromise = undefined;
 }
+
+/**
+ * Pretty-encodes a URL. Double-encodes the string, and then replaces
+ *   benevolent characters for legibility.
+ */
+export function encodeURL(url: string, replaceSlashes?: boolean): string {
+  // @see Issue 4255 regarding double-encoding.
+  let output = encodeURIComponent(encodeURIComponent(url));
+  // @see Issue 4577 regarding more readable URLs.
+  output = output.replace(/%253A/g, ':');
+  output = output.replace(/%2520/g, '+');
+  if (replaceSlashes) {
+    output = output.replace(/%252F/g, '/');
+  }
+  return output;
+}
+
+/**
+ * Single decode for URL components. Will decode plus signs ('+') to spaces.
+ * Note: because this function decodes once, it is not the inverse of
+ * encodeURL.
+ */
+export function singleDecodeURL(url: string): string {
+  const withoutPlus = url.replace(/\+/g, '%20');
+  return decodeURIComponent(withoutPlus);
+}
diff --git a/polygerrit-ui/app/utils/url-util_test.js b/polygerrit-ui/app/utils/url-util_test.js
index d3d3f2f..0658be3 100644
--- a/polygerrit-ui/app/utils/url-util_test.js
+++ b/polygerrit-ui/app/utils/url-util_test.js
@@ -16,7 +16,12 @@
  */
 
 import '../test/common-test-setup-karma.js';
-import {getBaseUrl, getDocsBaseUrl, _testOnly_clearDocsBaseUrlCache} from './url-util.js';
+import {
+  getBaseUrl,
+  getDocsBaseUrl,
+  _testOnly_clearDocsBaseUrlCache,
+  encodeURL, singleDecodeURL,
+} from './url-util.js';
 
 suite('url-util tests', () => {
   suite('getBaseUrl tests', () => {
@@ -82,4 +87,41 @@
       assert.isNotOk(docsBaseUrl);
     });
   });
+
+  suite('url encoding and decoding tests', () => {
+    suite('encodeURL', () => {
+      test('double encodes', () => {
+        assert.equal(encodeURL('abc?123'), 'abc%253F123');
+        assert.equal(encodeURL('def/ghi'), 'def%252Fghi');
+        assert.equal(encodeURL('jkl'), 'jkl');
+        assert.equal(encodeURL(''), '');
+      });
+
+      test('does not convert colons', () => {
+        assert.equal(encodeURL('mno:pqr'), 'mno:pqr');
+      });
+
+      test('converts spaces to +', () => {
+        assert.equal(encodeURL('words with spaces'), 'words+with+spaces');
+      });
+
+      test('does not convert slashes when configured', () => {
+        assert.equal(encodeURL('stu/vwx', true), 'stu/vwx');
+      });
+
+      test('does not convert slashes when configured', () => {
+        assert.equal(encodeURL('stu/vwx', true), 'stu/vwx');
+      });
+    });
+
+    suite('singleDecodeUrl', () => {
+      test('single decodes', () => {
+        assert.equal(singleDecodeURL('abc%3Fdef'), 'abc?def');
+      });
+
+      test('converts + to space', () => {
+        assert.equal(singleDecodeURL('ghi+jkl'), 'ghi jkl');
+      });
+    });
+  });
 });
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index fc91632..d03dada 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -167,8 +167,18 @@
 		// with the import error, so we can catch this problem easily.
 		writer.Header().Set("Content-Type", "text/html")
 	} else if isJsFile {
-		moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*)';$")
-		data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2';"))
+	  // The following code updates import statements.
+	  // 1. Keep all imports started with '.' character unchanged (i.e. all relative
+	  // imports like import ... from './a.js' or import ... from '../b/c/d.js'
+	  // 2. For other imports it adds '/node_modules/' prefix. Additionally,
+	  //   if an in imported file has .js or .mjs extension, the code keeps
+	  //   the file extension unchanged. Otherwise, it adds .js extension.
+	  //   Examples:
+	  //   '@polymer/polymer.js' -> '/node_modules/@polymer/polymer.js'
+    //   'page/page.mjs' -> '/node_modules/page.mjs'
+    //   '@polymer/iron-icon' -> '/node_modules/@polymer/iron-icon.js'
+		moduleImportRegexp := regexp.MustCompile("(?m)^(import.*)'([^/.].*?)(\\.(m?)js)?';$")
+		data = moduleImportRegexp.ReplaceAll(data, []byte("$1 '/node_modules/$2.${4}js';"))
 		writer.Header().Set("Content-Type", "application/javascript")
 	} else if strings.HasSuffix(normalizedContentPath, ".css") {
 		writer.Header().Set("Content-Type", "text/css")
@@ -483,7 +493,11 @@
 var (
 	tsStartingCompilation     = "- Starting compilation in watch mode..."
 	tsFileChangeDetectedMsg   = "- File change detected. Starting incremental compilation..."
-	tsStartWatchingMsg        = regexp.MustCompile(`^.* - Found \d errors\. Watching for file changes\.$`)
+	// If there is only one error typescript outputs:
+	// Found 1 error
+	// In all other cases it outputs
+	// Found X errors
+	tsStartWatchingMsg        = regexp.MustCompile(`^.* - Found \d+ error(s)?\. Watching for file changes\.$`)
 	waitForNextChangeInterval = 1 * time.Second
 )
 
diff --git a/proto/cache.proto b/proto/cache.proto
index 4d24036..29b5870 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -338,3 +338,128 @@
   string local_default_dashboard = 9;
   string config_ref_state = 10;
 }
+
+// Serialized form of com.google.gerrit.common.data.GroupReference.
+// Next ID: 3
+message GroupReferenceProto {
+  string uuid = 1;
+  string name = 2;
+}
+
+// Serialized form of com.google.gerrit.common.data.PermissionRule.
+// Next ID: 6
+message PermissionRuleProto {
+  string action = 1; // ENUM as String
+  bool force = 2;
+  int32 min = 3;
+  int32 max = 4;
+  GroupReferenceProto group = 5;
+}
+
+// Serialized form of com.google.gerrit.common.data.Permission.
+// Next ID: 4
+message PermissionProto {
+  string name = 1;
+  bool exclusive_group = 2;
+  repeated PermissionRuleProto rules = 3;
+}
+
+// Serialized form of com.google.gerrit.common.data.AccessSection.
+// Next ID: 3
+message AccessSectionProto {
+  string name = 1;
+  repeated PermissionProto permissions = 2;
+}
+
+// Serialized form of com.google.gerrit.server.git.BranchOrderSection.
+// Next ID: 2
+message BranchOrderSectionProto {
+  repeated string branches_in_order = 1;
+}
+
+// Serialized form of com.google.gerrit.common.data.ContributorAgreement.
+// Next ID: 8
+message ContributorAgreementProto {
+  string name = 1;
+  string description = 2;
+  repeated PermissionRuleProto accepted = 3;
+  GroupReferenceProto auto_verify = 4;
+  string url = 5;
+  repeated string exclude_regular_expressions = 6;
+  repeated string match_regular_expressions = 7;
+}
+
+// Serialized form of com.google.gerrit.entities.Address.
+// Next ID: 3
+message AddressProto {
+  string name = 1;
+  string email = 2;
+}
+
+// Serialized form of com.google.gerrit.entities.NotifyConfig.
+// Next ID: 7
+message NotifyConfigProto {
+  string name = 1;
+  repeated string type = 2; // ENUM as String
+  string filter = 3;
+  string header = 4; // ENUM as String
+  repeated GroupReferenceProto groups = 5;
+  repeated AddressProto addresses = 6;
+}
+
+// Serialized form of com.google.gerrit.entities.LabelValue.
+// Next ID: 3
+message LabelValueProto {
+  string text = 1;
+  int32 value = 2;
+}
+
+// Serialized form of com.google.gerrit.common.data.LabelType.
+// Next ID: 19
+message LabelTypeProto {
+  string name = 1;
+  string function = 2; // ENUM as String
+  bool copy_any_score = 3;
+  bool copy_min_score = 4;
+  bool copy_max_score = 5;
+  bool copy_all_scores_on_merge_first_parent_update = 6;
+  bool copy_all_scores_on_trivial_rebase = 7;
+  bool copy_all_scores_if_no_code_change = 8;
+  bool copy_all_scores_if_no_change = 9;
+  repeated int32 copy_values = 10;
+  bool allow_post_submit = 11;
+  bool ignore_self_approval = 12;
+  int32 default_value = 13;
+  repeated LabelValueProto values = 14;
+  int32 max_negative = 15;
+  int32 max_positive = 16;
+  bool can_override = 17;
+  repeated string ref_patterns = 18;
+}
+
+// Serialized form of com.google.gerrit.server.project.ConfiguredMimeTypes.
+// Next ID: 4
+message ConfiguredMimeTypeProto {
+  string type = 1;
+  string pattern = 2;
+  bool is_regular_expression = 3;
+}
+
+// Serialized form of com.google.gerrit.common.data.SubscribeSection.
+// Next ID: 4
+message SubscribeSectionProto {
+  string project_name = 1;
+  repeated string multi_match_ref_specs = 2;
+  repeated string matching_ref_specs = 3;
+}
+
+// Serialized form of com.google.gerrit.entities.StoredCommentLinkInfo.
+// Next ID: 7
+message StoredCommentLinkInfoProto {
+  string name = 1;
+  string match = 2;
+  string link = 3;
+  string html = 4;
+  bool enabled = 5;
+  bool override_only = 6;
+}
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index eba7e4b..ce858d5 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -345,7 +345,8 @@
 
 test -z "$GERRIT_USER" && GERRIT_USER=`whoami`
 RUN_ARGS="-jar $GERRIT_WAR daemon -d $GERRIT_SITE"
-if test "`get_config --bool container.slave`" = "true" ; then
+if test "`get_config --bool container.slave`" = "true" || \
+    test "`get_config --bool container.replica`" = "true"; then
   RUN_ARGS="$RUN_ARGS --replica --enable-httpd --headless"
 fi
 DAEMON_OPTS=`get_config --get-all container.daemonOpt`
diff --git a/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy b/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy
new file mode 100644
index 0000000..033d145
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/RegisterNewEmailHtml.soy
@@ -0,0 +1,48 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .RegisterNewEmailHtml}
+  {@param email: ?}
+  <p>
+    Welcome to Gerrit Code Review at {$email.gerritHost}.
+    To add a verified email address to your user account, please
+    click on the following link
+  </p>
+  {if $email.userNameEmail}
+    <p>
+        {sp}while signed in as {$email.userNameEmail}
+    </p>
+  {/if}:
+
+  <p>
+
+    {$email.gerritUrl}#/VE/{$email.emailRegistrationToken}
+  </p>
+  <p>
+    If you have received this mail in error, you do not need to take any
+    action to cancel the account.  The address will not be activated, and
+    you will not receive any further emails.
+  </p>
+  <p>
+    If clicking the link above does not work, copy and paste the URL in a
+    new browser window instead.
+
+    This is a send-only email address.  Replies to this message will not
+    be read or answered.
+  </p>
+{/template}
\ No newline at end of file
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index eeb5e6b..bbb1432 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -1,4 +1,4 @@
-load("@npm_bazel_terser//:index.bzl", "terser_minified")
+load("@npm//@bazel/terser:index.bzl", "terser_minified")
 load("//lib/js:npm.bzl", "NPM_SHA1S", "NPM_VERSIONS")
 
 NPMJS = "NPMJS"
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index ce5d62d..d445be2 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -68,7 +68,7 @@
             "export TZ",
             "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
             "cd $$TMP",
-            "unzip -q $$ROOT/$<",
+            "unzip -qo $$ROOT/$<",
             "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
             "find . -exec touch '{}' ';'",
             "zip -Xqr $$ROOT/$@ .",
diff --git a/tools/dev-hooks/pre-commit b/tools/dev-hooks/pre-commit
deleted file mode 100755
index af87b7e..0000000
--- a/tools/dev-hooks/pre-commit
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/bin/sh
-#
-# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
-#
-# 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.
-
-
-# To enable this hook:
-# - copy this file or content to ".git/hooks/pre-commit"
-# - (optional if you copied this file) make it executable: `chmod +x .git/hooks/pre-commit`
-
-set -ue
-
-# gitroot, default to .
-gitroot=$(git rev-parse --show-cdup)
-gitroot=${gitroot:-.};
-
-# eslint
-eslint=${gitroot}/node_modules/eslint/bin/eslint.js
-
-# Run eslint over changed frontend code
-CHANGED_UI_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.js' '*.html' | grep 'polygerrit-ui') && true
-if [ "${CHANGED_UI_FILES}" ]; then
-  if $eslint --fix ${CHANGED_UI_FILES}; then
-    # Add again in case lint fix modified some files
-    git add ${CHANGED_UI_FILES}
-    exit 0
-  else
-    echo "Failed to fix all linter issues.";
-    exit 1
-  fi
-else
-  echo "No UI files changed"
-  exit 0
-fi
\ No newline at end of file
diff --git a/tools/node_tools/node_modules_licenses/BUILD b/tools/node_tools/node_modules_licenses/BUILD
index bd7e854..581b3a9 100644
--- a/tools/node_tools/node_modules_licenses/BUILD
+++ b/tools/node_tools/node_modules_licenses/BUILD
@@ -1,6 +1,6 @@
-load("@npm_bazel_typescript//:index.bzl", "ts_library")
 load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
-load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
 
 package(default_visibility = ["//visibility:public"])
 
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 6fafe63..67a85a4 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": "^1.6.1",
-    "@bazel/typescript": "^1.6.1",
+    "@bazel/rollup": "^2.0.0",
+    "@bazel/typescript": "^2.0.0",
     "@types/node": "^10.17.12",
     "@types/parse5": "^4.0.0",
     "@types/parse5-html-rewriting-stream": "^5.1.2",
diff --git a/tools/node_tools/polygerrit_app_preprocessor/BUILD b/tools/node_tools/polygerrit_app_preprocessor/BUILD
index b031293..b5ee34f 100644
--- a/tools/node_tools/polygerrit_app_preprocessor/BUILD
+++ b/tools/node_tools/polygerrit_app_preprocessor/BUILD
@@ -1,6 +1,6 @@
-load("@npm_bazel_typescript//:index.bzl", "ts_library")
 load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
-load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
+load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
 
 package(default_visibility = ["//visibility:public"])
 
diff --git a/tools/node_tools/utils/BUILD b/tools/node_tools/utils/BUILD
index fca3c12..5c407ca 100644
--- a/tools/node_tools/utils/BUILD
+++ b/tools/node_tools/utils/BUILD
@@ -1,4 +1,4 @@
-load("@npm_bazel_typescript//:index.bzl", "ts_library")
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
 
 package(default_visibility = ["//visibility:public"])
 
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 78349fa..a3ac4af 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@^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/rollup@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-2.0.0.tgz#1980cb3f6922227659260bfdca99c457889a5bc1"
+  integrity sha512-mifUfCZbD1RIhfowh4N8E4881ag3FChz7F4z35wxMOP52g1q3+6Bvh5wv9iysFQopxGmS5jNEj3Dq/CWtSoOnw==
 
-"@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==
+"@bazel/typescript@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-2.0.0.tgz#2ff5615f09c733cc681ba2ada92b11c356b694cd"
+  integrity sha512-5FPkxULWIjAKLG5J1XvpXpY1/4IK39dAoWA/Hhg+16gXTES32fT8w42k96pb6BTaNnyBuYgIHBpELEAJ40OOAQ==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
@@ -950,9 +950,9 @@
   integrity sha512-N33cKXGSqhOYaPiT4xUGsYlPPDwFtQM/6QxJxuMXA/7BcySW+lkn2yigWP7vfs4daiL/7NJNU6DMCqg5N4B+xQ==
 
 "@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.27"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.27.tgz#391cb391c75646c8ad2a7b6ed3bbcee52d1bdf19"
+  integrity sha512-J0oqm9ZfAXaPdwNXMMgAhylw5fhmXkToJd06vuDUSAgEDZ/n/69/69UmyBZbc+zT34UnShuDSBqvim3SPnozJg==
 
 "@types/node@^10.17.12":
   version "10.17.24"
@@ -7834,7 +7834,12 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
-tslib@^1.8.1, tslib@^1.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==
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 96ea42c..5934512 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -23,8 +23,8 @@
 
     maven_jar(
         name = "dropwizard-core",
-        artifact = "io.dropwizard.metrics:metrics-core:4.1.9",
-        sha1 = "dd76a62b007ffea9e6aba10f64c04173ef65f895",
+        artifact = "io.dropwizard.metrics:metrics-core:4.1.10.1",
+        sha1 = "e55d1e4de0ccec6f404dbf775c62626d8b9f79a4",
     )
 
     SSHD_VERS = "2.4.0"
diff --git a/yarn.lock b/yarn.lock
index 0c4383f..a20b1c7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -485,20 +485,20 @@
     lodash "^4.17.11"
     to-fast-properties "^2.0.0"
 
-"@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/rollup@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-2.0.0.tgz#1980cb3f6922227659260bfdca99c457889a5bc1"
+  integrity sha512-mifUfCZbD1RIhfowh4N8E4881ag3FChz7F4z35wxMOP52g1q3+6Bvh5wv9iysFQopxGmS5jNEj3Dq/CWtSoOnw==
 
-"@bazel/terser@^1.7.0":
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-1.7.0.tgz#c43e711e13b9a71c7abd3ade04fb4650d547ad01"
-  integrity sha512-u/UXk0WUinvkk1g5xxfqGieBz3r12Bj2y2m25lC5GjHBgCpGk7DyeGGi9H3QQNO1Wmpw51QSE9gaPzKzjUVGug==
+"@bazel/terser@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-2.0.0.tgz#a841db8aefd7c51c216b34a26bc02a6c93d5e56a"
+  integrity sha512-6mBYcfzP6pWxycYZ8r4Lz5kgiWZ7n08bVHZBIRExFeqs7Yy92dD92LPeA9FZIzFiX00IuR9Q1Lqy23xH5q7FeQ==
 
-"@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==
+"@bazel/typescript@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-2.0.0.tgz#2ff5615f09c733cc681ba2ada92b11c356b694cd"
+  integrity sha512-5FPkxULWIjAKLG5J1XvpXpY1/4IK39dAoWA/Hhg+16gXTES32fT8w42k96pb6BTaNnyBuYgIHBpELEAJ40OOAQ==
   dependencies:
     protobufjs "6.8.8"
     semver "5.6.0"
@@ -711,13 +711,6 @@
   dependencies:
     chalk "*"
 
-"@types/cheerio@^0.22.2":
-  version "0.22.15"
-  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.15.tgz#69040ffa92c309beeeeb7e92db66ac3f80700c0b"
-  integrity sha512-UGiiVtJK5niCqMKYmLEFz1Wl/3L5zF/u78lu8CwoUywWXRr9LDimeYuOzXVLXBMO758fcTdFtgjvqlztMH90MA==
-  dependencies:
-    "@types/node" "*"
-
 "@types/clean-css@*":
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.1.tgz#cb0134241ec5e6ede1b5344bc829668fd9871a8d"
@@ -1367,11 +1360,6 @@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-amdefine@>=0.0.4:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
-  integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
-
 ansi-align@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-1.1.0.tgz#2f0c1658829739add5ebb15e6b0c6e3423f016ba"
@@ -2089,11 +2077,6 @@
     raw-body "2.4.0"
     type-is "~1.6.17"
 
-boolbase@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
-  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
-
 bower-config@^1.4.0, bower-config@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.1.tgz#85fd9df367c2b8dbbd0caa4c5f2bad40cd84c2cc"
@@ -2441,18 +2424,6 @@
   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
   integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
 
-cheerio@^1.0.0-rc.2:
-  version "1.0.0-rc.3"
-  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
-  integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
-  dependencies:
-    css-select "~1.2.0"
-    dom-serializer "~0.1.1"
-    entities "~1.1.1"
-    htmlparser2 "^3.9.1"
-    lodash "^4.15.0"
-    parse5 "^3.0.1"
-
 chokidar@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -2967,16 +2938,6 @@
   resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
   integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
 
-css-select@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
-  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
-  dependencies:
-    boolbase "~1.0.0"
-    css-what "2.1"
-    domutils "1.5.1"
-    nth-check "~1.0.1"
-
 css-slam@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/css-slam/-/css-slam-2.1.2.tgz#3d35b1922cb3e0002a45c89ab189492508c493e5"
@@ -2988,7 +2949,7 @@
     parse5 "^4.0.0"
     shady-css-parser "^0.1.0"
 
-css-what@2.1, css-what@^2.1.0:
+css-what@^2.1.0:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
   integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
@@ -3265,14 +3226,6 @@
     domelementtype "^2.0.1"
     entities "^2.0.0"
 
-dom-serializer@~0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
-  integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
-  dependencies:
-    domelementtype "^1.3.0"
-    entities "^1.1.1"
-
 dom-urls@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/dom-urls/-/dom-urls-1.1.0.tgz#001ddf81628cd1e706125c7176f53ccec55d918e"
@@ -3289,7 +3242,7 @@
     clone "^2.1.0"
     parse5 "^4.0.0"
 
-domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+domelementtype@1, domelementtype@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
   integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@@ -3306,14 +3259,6 @@
   dependencies:
     domelementtype "1"
 
-domutils@1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
-  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
-  dependencies:
-    dom-serializer "0"
-    domelementtype "1"
-
 domutils@^1.5.1:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
@@ -3477,7 +3422,7 @@
     engine.io-parser "~2.2.0"
     ws "^7.1.2"
 
-entities@^1.1.1, entities@~1.1.1:
+entities@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
   integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@@ -4282,19 +4227,6 @@
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
 
-fried-twinkie@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/fried-twinkie/-/fried-twinkie-0.2.2.tgz#fafa52b3b7957dc78d7867b28f74b6e01bdb2aee"
-  integrity sha512-rb/i+7VXEToBjrYZ2jSew4bI9znWF15P52dAGuoJcxpaBibWz2PI5tRx0ZSjDM+a/gZI2Pgr2XHT6wwNVZQ7/g==
-  dependencies:
-    "@types/cheerio" "^0.22.2"
-    chalk "^2.1.0"
-    google-closure-compiler-js "^20170626.0.0"
-    tmp "^0.0.31"
-    tsickle "^0.23.4"
-    twinkie "0.0.11"
-    typescript "^2.4.1"
-
 fs-constants@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -4605,15 +4537,6 @@
     pify "^3.0.0"
     slash "^1.0.0"
 
-google-closure-compiler-js@^20170626.0.0:
-  version "20170626.0.0"
-  resolved "https://registry.yarnpkg.com/google-closure-compiler-js/-/google-closure-compiler-js-20170626.0.0.tgz#5df265b277d1ec6fdea12eed131d1491cd8a8d71"
-  integrity sha512-LQvWXN3yS2l88TsXiHZ0aWtGR51tep/bNvS7cuUldnKkppgknTo35jThYwE+JOU9lviERZjMHhiqxz2CXzIRuw==
-  dependencies:
-    minimist "^1.2.0"
-    vinyl "^2.0.1"
-    webpack-core "^0.6.8"
-
 got@^5.0.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
@@ -4933,7 +4856,7 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
-htmlparser2@^3.10.1, htmlparser2@^3.9.1:
+htmlparser2@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
   integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@@ -6009,7 +5932,7 @@
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
   integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
 
-lodash@^4.0.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.14, lodash@^4.17.15:
+lodash@^4.0.0, lodash@^4.16.6, lodash@^4.17.14, lodash@^4.17.15:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -6675,13 +6598,6 @@
     gauge "~2.7.3"
     set-blocking "~2.0.0"
 
-nth-check@~1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
-  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
-  dependencies:
-    boolbase "~1.0.0"
-
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -7055,13 +6971,6 @@
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
   integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
 
-parse5@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
-  integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
-  dependencies:
-    "@types/node" "*"
-
 parse5@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
@@ -8615,11 +8524,6 @@
   dependencies:
     is-plain-obj "^1.0.0"
 
-source-list-map@~0.1.7:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
-  integrity sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=
-
 source-map-resolve@^0.5.0:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
@@ -8639,13 +8543,6 @@
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
-source-map-support@^0.4.2:
-  version "0.4.18"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
-  integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
-  dependencies:
-    source-map "^0.5.6"
-
 source-map-support@~0.5.12:
   version "0.5.19"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
@@ -8669,13 +8566,6 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
 
-source-map@~0.4.1:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
-  integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
-  dependencies:
-    amdefine ">=0.0.4"
-
 spawn-sync@^1.0.15:
   version "1.0.15"
   resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
@@ -9253,13 +9143,6 @@
   dependencies:
     os-tmpdir "~1.0.1"
 
-tmp@^0.0.31:
-  version "0.0.31"
-  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
-  integrity sha1-jzirlDjhcxXl29izZX6L+yd65Kc=
-  dependencies:
-    os-tmpdir "~1.0.1"
-
 tmp@^0.0.33:
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -9364,16 +9247,6 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
-tsickle@^0.23.4:
-  version "0.23.6"
-  resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.23.6.tgz#fcee57a5cb7f92a8c3a9e578ee0a286427dcfacd"
-  integrity sha1-/O5Xpct/kqjDqeV47gooZCfc+s0=
-  dependencies:
-    minimist "^1.2.0"
-    mkdirp "^0.5.1"
-    source-map "^0.5.6"
-    source-map-support "^0.4.2"
-
 tslib@^1.8.1:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
@@ -9410,13 +9283,6 @@
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
-twinkie@0.0.11:
-  version "0.0.11"
-  resolved "https://registry.yarnpkg.com/twinkie/-/twinkie-0.0.11.tgz#013c5e4b6b23ac8dec5d4eb8f9f84858bc143a74"
-  integrity sha1-ATxeS2sjrI3sXU64+fhIWLwUOnQ=
-  dependencies:
-    cheerio "^1.0.0-rc.2"
-
 type-check@~0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -9469,11 +9335,6 @@
   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==
-
 typical@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
@@ -9930,14 +9791,6 @@
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
   integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
 
-webpack-core@^0.6.8:
-  version "0.6.9"
-  resolved "https://registry.yarnpkg.com/webpack-core/-/webpack-core-0.6.9.tgz#fc571588c8558da77be9efb6debdc5a3b172bdc2"
-  integrity sha1-/FcViMhVjad76e+23r3Fo7FyvcI=
-  dependencies:
-    source-list-map "~0.1.7"
-    source-map "~0.4.1"
-
 whatwg-url@^6.4.0:
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"